diff --git a/CHANGES.md b/CHANGES.md index 71b0c61b..6c63da61 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,9 @@ # Changes +* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating. * 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall. * June 29, 2013 - Removed rails\_admin because it was causing deployment issues. Better to have people install their favorite admin tool if they want one. * June, 2013 - A number of new agents have been contributed, including interfaces to Weibo, Twitter, and Twilio, as well as Agents for translation, sentiment analysis, and for posting and receiving webhooks. -* March 24, 2013 (0.1) - Refactored loading of Agents for `check` and `receive` to use ids instead of full objects. This should fix the too-large delayed_job issues. Added `system_timer` and `fastercsv` to the Gemfile for the Ruby 1.8 platform. +* March 24, 2013 (0.1) - Refactored loading of Agents for `check` and `receive` to use ids instead of full objects. This should fix the too-large delayed\_job issues. Added `system_timer` and `fastercsv` to the Gemfile for the Ruby 1.8 platform. * March 18, 2013 - Added Wiki page about the [Agent API](https://github.com/cantino/huginn/wiki/Creating-a-new-agent). * March 17, 2013 - Switched to JSONPath for defining paths through JSON structures. The WebsiteAgent can now scrape and parse JSON. diff --git a/README.md b/README.md index ea10af61..6c9fbc25 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ Huginn is a system for building agents that perform automated tasks for you onli * Track the weather and get an email when it's going to rain (or snow) tomorrow * Follow your project names on Twitter and get updates when people mention them * Scrape websites and receive emails when they change +* Compose digest emails about things you care about to be sent at specific times of the day +* Track counts of high frequency events and SMS on changes, such as the term "san francisco emergency" * Track your location over time +* Create Amazon Mechanical Turk tasks as the input, or output, of events Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, and join us in \#huginn on Freenode IRC to discuss the project. @@ -84,7 +87,7 @@ In order to use the WeatherAgent you need an [API key with Wunderground](http:// You can use [Post Location](https://github.com/cantino/post_location) on your iPhone to post your location to an instance of the UserLocationAgent. Make a new one to see instructions. -#### Enable DelayedJobWeb for handy delayed_job monitoring and control +#### Enable DelayedJobWeb for handy delayed\_job monitoring and control * Edit `config.ru`, uncomment the DelayedJobWeb section, and change the DelayedJobWeb username and password. * Uncomment `match "/delayed_job" => DelayedJobWeb, :anchor => false` in `config/routes.rb`. @@ -110,6 +113,5 @@ Please fork, add specs, and send pull requests! [![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Code Climate](https://codeclimate.com/github/cantino/huginn.png)](https://codeclimate.com/github/cantino/huginn) - [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") diff --git a/VERSION b/VERSION index 3b04cfb6..be586341 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2 +0.3 diff --git a/app/assets/javascripts/application.js.coffee.erb b/app/assets/javascripts/application.js.coffee.erb index 5cb719a2..55afb965 100644 --- a/app/assets/javascripts/application.js.coffee.erb +++ b/app/assets/javascripts/application.js.coffee.erb @@ -102,6 +102,7 @@ $(document).ready -> $("#logs .refresh, #logs .clear").hide() $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) => $("#logs .logs").html html + $("#show-tabs li a.recent-errors").removeClass 'recent-errors' $("#logs .spinner").stop(true, true).fadeOut -> $("#logs .refresh, #logs .clear").show() diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index c72938ca..588debf3 100644 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -122,3 +122,7 @@ span.not-applicable:after { margin: 0 10px; } } + +#show-tabs li a.recent-errors { + font-weight: bold; +} diff --git a/app/concerns/email_concern.rb b/app/concerns/email_concern.rb index 9a311b80..01cca474 100644 --- a/app/concerns/email_concern.rb +++ b/app/concerns/email_concern.rb @@ -1,18 +1,18 @@ module EmailConcern extend ActiveSupport::Concern - MAIN_KEYS = %w[title message text main value].map(&:to_sym) + MAIN_KEYS = %w[title message text main value] included do self.validate :validate_email_options end def validate_email_options - errors.add(:base, "subject and expected_receive_period_in_days are required") unless options[:subject].present? && options[:expected_receive_period_in_days].present? + errors.add(:base, "subject and expected_receive_period_in_days are required") unless options['subject'].present? && options['expected_receive_period_in_days'].present? end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def present(payload) diff --git a/app/concerns/twitter_concern.rb b/app/concerns/twitter_concern.rb index 0a596af3..aedc351a 100644 --- a/app/concerns/twitter_concern.rb +++ b/app/concerns/twitter_concern.rb @@ -7,20 +7,20 @@ module TwitterConcern end def validate_twitter_options - unless options[:consumer_key].present? && - options[:consumer_secret].present? && - options[:oauth_token].present? && - options[:oauth_token_secret].present? + unless options['consumer_key'].present? && + options['consumer_secret'].present? && + options['oauth_token'].present? && + options['oauth_token_secret'].present? errors.add(:base, "consumer_key, consumer_secret, oauth_token and oauth_token_secret are required to authenticate with the Twitter API") end end def configure_twitter Twitter.configure do |config| - config.consumer_key = options[:consumer_key] - config.consumer_secret = options[:consumer_secret] - config.oauth_token = options[:oauth_token] || options[:access_key] - config.oauth_token_secret = options[:oauth_token_secret] || options[:access_secret] + config.consumer_key = options['consumer_key'] + config.consumer_secret = options['consumer_secret'] + config.oauth_token = options['oauth_token'] || options['access_key'] + config.oauth_token_secret = options['oauth_token_secret'] || options['access_secret'] end end diff --git a/app/concerns/weibo_concern.rb b/app/concerns/weibo_concern.rb index 029071e7..eea56237 100644 --- a/app/concerns/weibo_concern.rb +++ b/app/concerns/weibo_concern.rb @@ -6,19 +6,19 @@ module WeiboConcern end def validate_weibo_options - unless options[:app_key].present? && - options[:app_secret].present? && - options[:access_token].present? + unless options['app_key'].present? && + options['app_secret'].present? && + options['access_token'].present? errors.add(:base, "app_key, app_secret and access_token are required") end end def weibo_client unless @weibo_client - WeiboOAuth2::Config.api_key = options[:app_key] # WEIBO_APP_KEY - WeiboOAuth2::Config.api_secret = options[:app_secret] # WEIBO_APP_SECRET + WeiboOAuth2::Config.api_key = options['app_key'] # WEIBO_APP_KEY + WeiboOAuth2::Config.api_secret = options['app_secret'] # WEIBO_APP_SECRET @weibo_client = WeiboOAuth2::Client.new - @weibo_client.get_token_from_hash :access_token => options[:access_token] + @weibo_client.get_token_from_hash :access_token => options['access_token'] end @weibo_client end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index e1860a26..20635378 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -6,7 +6,7 @@ class EventsController < ApplicationController @agent = current_user.agents.find(params[:agent]) @events = @agent.events.page(params[:page]) else - @events = current_user.events.page(params[:page]) + @events = current_user.events.preload(:agent).page(params[:page]) end respond_to do |format| diff --git a/app/controllers/logs_controller.rb b/app/controllers/logs_controller.rb index c3fd4dfc..45da4177 100644 --- a/app/controllers/logs_controller.rb +++ b/app/controllers/logs_controller.rb @@ -7,7 +7,7 @@ class LogsController < ApplicationController end def clear - @agent.logs.delete_all + @agent.delete_logs! index end diff --git a/app/models/agent.rb b/app/models/agent.rb index 8ee4f9cf..216d0fca 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -1,14 +1,13 @@ -require 'serialize_and_symbolize' +require 'json_serialized_field' require 'assignable_types' require 'markdown_class_attributes' require 'utils' class Agent < ActiveRecord::Base - include SerializeAndSymbolize include AssignableTypes include MarkdownClassAttributes + include JSONSerializedField - serialize_and_symbolize :options, :memory markdown_class_attributes :description, :event_description load_types_in "Agents" @@ -18,9 +17,12 @@ class Agent < ActiveRecord::Base attr_accessible :options, :memory, :name, :type, :schedule, :source_ids + json_serialize :options, :memory + validates_presence_of :name, :user validate :sources_are_owned validate :validate_schedule + validate :validate_options after_initialize :set_default_schedule before_validation :set_default_schedule @@ -32,7 +34,6 @@ class Agent < ActiveRecord::Base has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" - has_one :most_recent_log, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc" has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver @@ -74,13 +75,16 @@ class Agent < ActiveRecord::Base raise "Implement me in your subclass" end - def event_created_within(days) - event = most_recent_event - event && event.created_at > days.to_i.days.ago && event.payload.present? && event + def validate_options + # Implement me in your subclass to test for valid options. + end + + def event_created_within?(days) + last_event_at && last_event_at > days.to_i.days.ago end def recent_error_logs? - most_recent_log.try(:level) == 4 + last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) end def sources_are_owned @@ -120,10 +124,6 @@ class Agent < ActiveRecord::Base self.schedule = nil if cannot_be_scheduled? end - def last_event_at - @memoized_last_event_at ||= most_recent_event.try(:created_at) - end - def default_schedule self.class.default_schedule end @@ -158,6 +158,11 @@ class Agent < ActiveRecord::Base end end + def delete_logs! + logs.delete_all + update_column :last_error_log_at, nil + end + def log(message, options = {}) puts "Agent##{id}: #{message}" unless Rails.env.test? AgentLog.log_for_agent(self, message, options) diff --git a/app/models/agent_log.rb b/app/models/agent_log.rb index 7d10f6fd..ce9c000c 100644 --- a/app/models/agent_log.rb +++ b/app/models/agent_log.rb @@ -14,6 +14,9 @@ class AgentLog < ActiveRecord::Base oldest_id_to_keep = agent.logs.limit(1).offset(log_length - 1).pluck("agent_logs.id") agent.logs.where("agent_logs.id < ?", oldest_id_to_keep).delete_all end + + agent.update_column :last_error_log_at, Time.now if log.level >= 4 + log end diff --git a/app/models/agents/adioso_agent.rb b/app/models/agents/adioso_agent.rb index 2b08feec..7cb64f1f 100644 --- a/app/models/agents/adioso_agent.rb +++ b/app/models/agents/adioso_agent.rb @@ -29,22 +29,22 @@ module Agents def default_options { - :start_date => Date.today.httpdate[0..15], - :end_date => Date.today.plus_with_duration(100).httpdate[0..15], - :from => "New York", - :to => "Chicago", - :username => "xx", - :password => "xx", - :expected_update_period_in_days => "1" + 'start_date' => Date.today.httpdate[0..15], + 'end_date' => Date.today.plus_with_duration(100).httpdate[0..15], + 'from' => "New York", + 'to' => "Chicago", + 'username' => "xx", + 'password' => "xx", + 'expected_update_period_in_days' => "1" } end def working? - event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? end def validate_options - unless %w[start_date end_date from to username password expected_update_period_in_days].all? { |field| options[field.to_sym].present? } + unless %w[start_date end_date from to username password expected_update_period_in_days].all? { |field| options[field].present? } errors.add(:base, "All fields are required") end end @@ -54,9 +54,9 @@ module Agents end def check - auth_options = {:basic_auth => {:username =>options[:username], :password=>options[:password]}} - parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(options[:from])}+to+#{URI.encode(options[:to])}", auth_options - fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(options[:end_date])}\\3#{date_to_unix_epoch(options[:start_date])}" + auth_options = {:basic_auth => {:username =>options[:username], :password=>options['password']}} + parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?q=#{URI.encode(options['from'])}+to+#{URI.encode(options['to'])}", auth_options + fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(options['end_date'])}\\3#{date_to_unix_epoch(options['start_date'])}" fare = HTTParty.get fare_request, auth_options if fare["warnings"] @@ -64,7 +64,7 @@ module Agents else event = fare["results"].min {|a,b| a["cost"] <=> b["cost"]} event["date"] = Time.at(event["date"]).to_date.httpdate[0..15] - event["route"] = "#{options[:from]} to #{options[:to]}" + event["route"] = "#{options['from']} to #{options['to']}" create_event :payload => event end end diff --git a/app/models/agents/digest_email_agent.rb b/app/models/agents/digest_email_agent.rb index 009564df..62204d68 100644 --- a/app/models/agents/digest_email_agent.rb +++ b/app/models/agents/digest_email_agent.rb @@ -9,7 +9,7 @@ module Agents description <<-MD The DigestEmailAgent collects any Events sent to it and sends them all via email when run. The email will be sent to your account's address and will have a `subject` and an optional `headline` before - listing the Events. If the Events' payloads contain a `:message`, that will be highlighted, otherwise everything in + listing the Events. If the Events' payloads contain a `message`, that will be highlighted, otherwise everything in their payloads will be shown. Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. @@ -17,29 +17,29 @@ module Agents def default_options { - :subject => "You have some notifications!", - :headline => "Your notifications:", - :expected_receive_period_in_days => "2" + 'subject' => "You have some notifications!", + 'headline' => "Your notifications:", + 'expected_receive_period_in_days' => "2" } end def receive(incoming_events) incoming_events.each do |event| - self.memory[:queue] ||= [] - self.memory[:queue] << event.payload - self.memory[:events] ||= [] - self.memory[:events] << event.id + self.memory['queue'] ||= [] + self.memory['queue'] << event.payload + self.memory['events'] ||= [] + self.memory['events'] << event.id end end def check - if self.memory[:queue] && self.memory[:queue].length > 0 - ids = self.memory[:events].join(",") - groups = self.memory[:queue].map { |payload| present(payload) } + if self.memory['queue'] && self.memory['queue'].length > 0 + ids = self.memory['events'].join(",") + groups = self.memory['queue'].map { |payload| present(payload) } log "Sending digest mail to #{user.email} with events [#{ids}]" - SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => groups) - self.memory[:queue] = [] - self.memory[:events] = [] + SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => groups) + self.memory['queue'] = [] + self.memory['events'] = [] end end end diff --git a/app/models/agents/email_agent.rb b/app/models/agents/email_agent.rb index e1c79dce..64f81164 100644 --- a/app/models/agents/email_agent.rb +++ b/app/models/agents/email_agent.rb @@ -16,16 +16,16 @@ module Agents def default_options { - :subject => "You have a notification!", - :headline => "Your notification:", - :expected_receive_period_in_days => "2" + 'subject' => "You have a notification!", + 'headline' => "Your notification:", + 'expected_receive_period_in_days' => "2" } end def receive(incoming_events) incoming_events.each do |event| log "Sending digest mail to #{user.email} with event #{event.id}" - SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => [present(event.payload)]) + SystemMailer.delay.send_message(:to => user.email, :subject => options['subject'], :headline => options['headline'], :groups => [present(event.payload)]) end end end diff --git a/app/models/agents/event_formatting_agent.rb b/app/models/agents/event_formatting_agent.rb index cc27c5da..947c00bb 100644 --- a/app/models/agents/event_formatting_agent.rb +++ b/app/models/agents/event_formatting_agent.rb @@ -8,20 +8,20 @@ module Agents For example, here is a possible Event: { - :high => { - :celsius => "18", - :fahreinheit => "64" + "high": { + "celsius": "18", + "fahreinheit": "64" }, - :conditions => "Rain showers", - :data => "This is some data" + "conditions": "Rain showers", + "data": "This is some data" } You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key. You can use an Event Formatting Agent's `instructions` setting to do this in the following way: - instructions: { - message: "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", - subject: "$.data" + "instructions": { + "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", + "subject": "$.data" } JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else. @@ -29,8 +29,8 @@ module Agents Events generated by this possible Event Formatting Agent will look like: { - :message => "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.", - :subject => "This is some data" + "message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.", + "subject": "This is some data" } If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. @@ -40,25 +40,25 @@ module Agents To CGI escape output (for example when creating a link), prefix with `escape`, like so: { - :message => "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=" + "message": "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=" } MD event_description "User defined" def validate_options - errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options[:instructions].present? and options[:mode].present? and options[:skip_agent].present? and options[:skip_created_at].present? + errors.add(:base, "instructions, mode, skip_agent, and skip_created_at all need to be present.") unless options['instructions'].present? and options['mode'].present? and options['skip_agent'].present? and options['skip_created_at'].present? end def default_options { - :instructions => { - :message => "You received a text <$.text> from <$.fields.from>", - :some_other_field => "Looks like the weather is going to be <$.fields.weather>" + 'instructions' => { + 'message' => "You received a text <$.text> from <$.fields.from>", + 'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" }, - :mode => "clean", - :skip_agent => "false", - :skip_created_at => "false" + 'mode' => "clean", + 'skip_agent' => "false", + 'skip_created_at' => "false" } end @@ -68,10 +68,10 @@ module Agents def receive(incoming_events) incoming_events.each do |event| - formatted_event = options[:mode].to_s == "merge" ? event.payload : {} - options[:instructions].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } - formatted_event[:agent] = Agent.find(event.agent_id).type.slice!(8..-1) unless options[:skip_agent].to_s == "true" - formatted_event[:created_at] = event.created_at unless options[:skip_created_at].to_s == "true" + formatted_event = options['mode'].to_s == "merge" ? event.payload : {} + options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, event.payload) } + formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" + formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" create_event :payload => formatted_event end end diff --git a/app/models/agents/human_task_agent.rb b/app/models/agents/human_task_agent.rb index 66197b39..29786dab 100644 --- a/app/models/agents/human_task_agent.rb +++ b/app/models/agents/human_task_agent.rb @@ -128,73 +128,73 @@ module Agents MD def validate_options - options[:hit] ||= {} - options[:hit][:questions] ||= [] + options['hit'] ||= {} + options['hit']['questions'] ||= [] - errors.add(:base, "'trigger_on' must be one of 'schedule' or 'event'") unless %w[schedule event].include?(options[:trigger_on]) - errors.add(:base, "'hit.assignments' should specify the number of HIT assignments to create") unless options[:hit][:assignments].present? && options[:hit][:assignments].to_i > 0 - errors.add(:base, "'hit.title' must be provided") unless options[:hit][:title].present? - errors.add(:base, "'hit.description' must be provided") unless options[:hit][:description].present? - errors.add(:base, "'hit.questions' must be provided") unless options[:hit][:questions].present? && options[:hit][:questions].length > 0 + errors.add(:base, "'trigger_on' must be one of 'schedule' or 'event'") unless %w[schedule event].include?(options['trigger_on']) + errors.add(:base, "'hit.assignments' should specify the number of HIT assignments to create") unless options['hit']['assignments'].present? && options['hit']['assignments'].to_i > 0 + errors.add(:base, "'hit.title' must be provided") unless options['hit']['title'].present? + errors.add(:base, "'hit.description' must be provided") unless options['hit']['description'].present? + errors.add(:base, "'hit.questions' must be provided") unless options['hit']['questions'].present? && options['hit']['questions'].length > 0 - if options[:trigger_on] == "event" - errors.add(:base, "'expected_receive_period_in_days' is required when 'trigger_on' is set to 'event'") unless options[:expected_receive_period_in_days].present? - elsif options[:trigger_on] == "schedule" - errors.add(:base, "'submission_period' must be set to a positive number of hours when 'trigger_on' is set to 'schedule'") unless options[:submission_period].present? && options[:submission_period].to_i > 0 + if options['trigger_on'] == "event" + errors.add(:base, "'expected_receive_period_in_days' is required when 'trigger_on' is set to 'event'") unless options['expected_receive_period_in_days'].present? + elsif options['trigger_on'] == "schedule" + errors.add(:base, "'submission_period' must be set to a positive number of hours when 'trigger_on' is set to 'schedule'") unless options['submission_period'].present? && options['submission_period'].to_i > 0 end - if options[:hit][:questions].any? { |question| [:key, :name, :required, :type, :question].any? {|k| !question[k].present? } } + if options['hit']['questions'].any? { |question| %w[key name required type question].any? {|k| !question[k].present? } } errors.add(:base, "all questions must set 'key', 'name', 'required', 'type', and 'question'") end - if options[:hit][:questions].any? { |question| question[:type] == "selection" && (!question[:selections].present? || question[:selections].length == 0 || !question[:selections].all? {|s| s[:key].present? } || !question[:selections].all? { |s| s[:text].present? })} + if options['hit']['questions'].any? { |question| question['type'] == "selection" && (!question['selections'].present? || question['selections'].length == 0 || !question['selections'].all? {|s| s['key'].present? } || !question['selections'].all? { |s| s['text'].present? })} errors.add(:base, "all questions of type 'selection' must have a selections array with selections that set 'key' and 'name'") end - if take_majority? && options[:hit][:questions].any? { |question| question[:type] != "selection" } + if take_majority? && options['hit']['questions'].any? { |question| question['type'] != "selection" } errors.add(:base, "all questions must be of type 'selection' to use the 'take_majority' option") end if create_poll? - errors.add(:base, "poll_options is required when combination_mode is set to 'poll' and must have the keys 'title', 'instructions', 'row_template', and 'assignments'") unless options[:poll_options].is_a?(Hash) && options[:poll_options][:title].present? && options[:poll_options][:instructions].present? && options[:poll_options][:row_template].present? && options[:poll_options][:assignments].to_i > 0 + errors.add(:base, "poll_options is required when combination_mode is set to 'poll' and must have the keys 'title', 'instructions', 'row_template', and 'assignments'") unless options['poll_options'].is_a?(Hash) && options['poll_options']['title'].present? && options['poll_options']['instructions'].present? && options['poll_options']['row_template'].present? && options['poll_options']['assignments'].to_i > 0 end end def default_options { - :expected_receive_period_in_days => 2, - :trigger_on => "event", - :hit => + 'expected_receive_period_in_days' => 2, + 'trigger_on' => "event", + 'hit' => { - :assignments => 1, - :title => "Sentiment evaluation", - :description => "Please rate the sentiment of this message: '<$.message>'", - :reward => 0.05, - :lifetime_in_seconds => 24 * 60 * 60, - :questions => + 'assignments' => 1, + 'title' => "Sentiment evaluation", + 'description' => "Please rate the sentiment of this message: '<$.message>'", + 'reward' => 0.05, + 'lifetime_in_seconds' => 24 * 60 * 60, + 'questions' => [ { - :type => "selection", - :key => "sentiment", - :name => "Sentiment", - :required => "true", - :question => "Please select the best sentiment value:", - :selections => + 'type' => "selection", + 'key' => "sentiment", + 'name' => "Sentiment", + 'required' => "true", + 'question' => "Please select the best sentiment value:", + 'selections' => [ - { :key => "happy", :text => "Happy" }, - { :key => "sad", :text => "Sad" }, - { :key => "neutral", :text => "Neutral" } + { 'key' => "happy", 'text' => "Happy" }, + { 'key' => "sad", 'text' => "Sad" }, + { 'key' => "neutral", 'text' => "Neutral" } ] }, { - :type => "free_text", - :key => "feedback", - :name => "Have any feedback for us?", - :required => "false", - :question => "Feedback", - :default => "Type here...", - :min_length => "2", - :max_length => "2000" + 'type' => "free_text", + 'key' => "feedback", + 'name' => "Have any feedback for us?", + 'required' => "false", + 'question' => "Feedback", + 'default' => "Type here...", + 'min_length' => "2", + 'max_length' => "2000" } ] } @@ -202,20 +202,20 @@ module Agents end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def check review_hits - if options[:trigger_on] == "schedule" && (memory[:last_schedule] || 0) <= Time.now.to_i - options[:submission_period].to_i * 60 * 60 - memory[:last_schedule] = Time.now.to_i + if options['trigger_on'] == "schedule" && (memory['last_schedule'] || 0) <= Time.now.to_i - options['submission_period'].to_i * 60 * 60 + memory['last_schedule'] = Time.now.to_i create_basic_hit end end def receive(incoming_events) - if options[:trigger_on] == "event" + if options['trigger_on'] == "event" incoming_events.each do |event| create_basic_hit event end @@ -225,33 +225,32 @@ module Agents protected def take_majority? - options[:combination_mode] == "take_majority" || options[:take_majority] == "true" + options['combination_mode'] == "take_majority" || options['take_majority'] == "true" end def create_poll? - options[:combination_mode] == "poll" + options['combination_mode'] == "poll" end def event_for_hit(hit_id) - if memory[:hits][hit_id.to_sym].is_a?(Hash) - Event.find_by_id(memory[:hits][hit_id.to_sym][:event_id]) + if memory['hits'][hit_id].is_a?(Hash) + Event.find_by_id(memory['hits'][hit_id]['event_id']) else nil end end def hit_type(hit_id) - # Fix this: the Ruby process will slowly run out of RAM by symbolizing these unique keys. - if memory[:hits][hit_id.to_sym].is_a?(Hash) && memory[:hits][hit_id.to_sym][:type] - memory[:hits][hit_id.to_sym][:type].to_sym + if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] + memory['hits'][hit_id]['type'] else - :user + 'user' end end def review_hits reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids - my_reviewed_hit_ids = reviewable_hit_ids & (memory[:hits] || {}).keys.map(&:to_s) + my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys if reviewable_hit_ids.length > 0 log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]" end @@ -264,7 +263,7 @@ module Agents if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" } inbound_event = event_for_hit(hit_id) - if hit_type(hit_id) == :poll + if hit_type(hit_id) == 'poll' # handle completed polls log "Handling a poll: #{hit_id}" @@ -280,35 +279,35 @@ module Agents top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first payload = { - :answers => memory[:hits][hit_id.to_sym][:answers], - :poll => assignments.map(&:answers), - :best_answer => memory[:hits][hit_id.to_sym][:answers][top_answer.to_i - 1] + 'answers' => memory['hits'][hit_id]['answers'], + 'poll' => assignments.map(&:answers), + 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] } event = create_event :payload => payload log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event else # handle normal completed HITs - payload = { :answers => assignments.map(&:answers) } + payload = { 'answers' => assignments.map(&:answers) } if take_majority? counts = {} - options[:hit][:questions].each do |question| - question_counts = question[:selections].inject({}) { |memo, selection| memo[selection[:key]] = 0; memo } + options['hit']['questions'].each do |question| + question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo } assignments.each do |assignment| answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) - answer = answers[question[:key]] + answer = answers[question['key']] question_counts[answer] += 1 end - counts[question[:key]] = question_counts + counts[question['key']] = question_counts end - payload[:counts] = counts + payload['counts'] = counts majority_answer = counts.inject({}) do |memo, (key, question_counts)| memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first memo end - payload[:majority_answer] = majority_answer + payload['majority_answer'] = majority_answer if all_questions_are_numeric? average_answer = counts.inject({}) do |memo, (key, question_counts)| @@ -320,35 +319,35 @@ module Agents memo[key] = sum / divisor.to_f memo end - payload[:average_answer] = average_answer + payload['average_answer'] = average_answer end end if create_poll? questions = [] - selections = 5.times.map { |i| { :key => i+1, :text => i+1 } }.reverse + selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse assignments.length.times do |index| questions << { - :type => "selection", - :name => "Item #{index + 1}", - :key => index, - :required => "true", - :question => Utils.interpolate_jsonpaths(options[:poll_options][:row_template], assignments[index].answers), - :selections => selections + 'type' => "selection", + 'name' => "Item #{index + 1}", + 'key' => index, + 'required' => "true", + 'question' => Utils.interpolate_jsonpaths(options['poll_options']['row_template'], assignments[index].answers), + 'selections' => selections } end - poll_hit = create_hit :title => options[:poll_options][:title], - :description => options[:poll_options][:instructions], - :questions => questions, - :assignments => options[:poll_options][:assignments], - :lifetime_in_seconds => options[:poll_options][:lifetime_in_seconds], - :reward => options[:poll_options][:reward], - :payload => inbound_event && inbound_event.payload, - :metadata => { :type => :poll, - :original_hit => hit_id, - :answers => assignments.map(&:answers), - :event_id => inbound_event && inbound_event.id } + poll_hit = create_hit 'title' => options['poll_options']['title'], + 'description' => options['poll_options']['instructions'], + 'questions' => questions, + 'assignments' => options['poll_options']['assignments'], + 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], + 'reward' => options['poll_options']['reward'], + 'payload' => inbound_event && inbound_event.payload, + 'metadata' => { 'type' => 'poll', + 'original_hit' => hit_id, + 'answers' => assignments.map(&:answers), + 'event_id' => inbound_event && inbound_event.id } log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event else @@ -360,47 +359,47 @@ module Agents assignments.each(&:approve!) hit.dispose! - memory[:hits].delete(hit_id.to_sym) + memory['hits'].delete(hit_id) end end end def all_questions_are_numeric? - options[:hit][:questions].all? do |question| - question[:selections].all? do |selection| - selection[:key] == selection[:key].to_f.to_s || selection[:key] == selection[:key].to_i.to_s + options['hit']['questions'].all? do |question| + question['selections'].all? do |selection| + selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s end end end def create_basic_hit(event = nil) - hit = create_hit :title => options[:hit][:title], - :description => options[:hit][:description], - :questions => options[:hit][:questions], - :assignments => options[:hit][:assignments], - :lifetime_in_seconds => options[:hit][:lifetime_in_seconds], - :reward => options[:hit][:reward], - :payload => event && event.payload, - :metadata => { :event_id => event && event.id } + hit = create_hit 'title' => options['hit']['title'], + 'description' => options['hit']['description'], + 'questions' => options['hit']['questions'], + 'assignments' => options['hit']['assignments'], + 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], + 'reward' => options['hit']['reward'], + 'payload' => event && event.payload, + 'metadata' => { 'event_id' => event && event.id } log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event end def create_hit(opts = {}) - payload = opts[:payload] || {} - title = Utils.interpolate_jsonpaths(opts[:title], payload).strip - description = Utils.interpolate_jsonpaths(opts[:description], payload).strip - questions = Utils.recursively_interpolate_jsonpaths(opts[:questions], payload) + payload = opts['payload'] || {} + title = Utils.interpolate_jsonpaths(opts['title'], payload).strip + description = Utils.interpolate_jsonpaths(opts['description'], payload).strip + questions = Utils.recursively_interpolate_jsonpaths(opts['questions'], payload) hit = RTurk::Hit.create(:title => title) do |hit| - hit.max_assignments = (opts[:assignments] || 1).to_i + hit.max_assignments = (opts['assignments'] || 1).to_i hit.description = description - hit.lifetime = (opts[:lifetime_in_seconds] || 24 * 60 * 60).to_i + hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) - hit.reward = (opts[:reward] || 0.05).to_f + hit.reward = (opts['reward'] || 0.05).to_f #hit.qualifications.add :approval_rate, { :gt => 80 } end - memory[:hits] ||= {} - memory[:hits][hit.id] = opts[:metadata] || {} + memory['hits'] ||= {} + memory['hits'][hit.id] = opts['metadata'] || {} hit end @@ -422,34 +421,34 @@ module Agents @questions.each.with_index do |question, index| Question do QuestionIdentifier do - text question[:key] || "question_#{index}" + text question['key'] || "question_#{index}" end DisplayName do - text question[:name] || "Question ##{index}" + text question['name'] || "Question ##{index}" end IsRequired do - text question[:required] || 'true' + text question['required'] || 'true' end QuestionContent do Text do - text question[:question] + text question['question'] end end AnswerSpecification do - if question[:type] == "selection" + if question['type'] == "selection" SelectionAnswer do StyleSuggestion do text 'radiobutton' end Selections do - question[:selections].each do |selection| + question['selections'].each do |selection| Selection do SelectionIdentifier do - text selection[:key] + text selection['key'] end Text do - text selection[:text] + text selection['text'] end end end @@ -459,18 +458,18 @@ module Agents else FreeTextAnswer do - if question[:min_length].present? || question[:max_length].present? + if question['min_length'].present? || question['max_length'].present? Constraints do lengths = {} - lengths[:minLength] = question[:min_length].to_s if question[:min_length].present? - lengths[:maxLength] = question[:max_length].to_s if question[:max_length].present? + lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? + lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? Length lengths end end - if question[:default].present? + if question['default'].present? DefaultText do - text question[:default] + text question['default'] end end end @@ -482,4 +481,4 @@ module Agents end end end -end \ No newline at end of file +end diff --git a/app/models/agents/manual_event_agent.rb b/app/models/agents/manual_event_agent.rb index 7b9f6f14..7c045a97 100644 --- a/app/models/agents/manual_event_agent.rb +++ b/app/models/agents/manual_event_agent.rb @@ -14,8 +14,8 @@ module Agents end def handle_details_post(params) - if params[:payload] - create_event(:payload => params[:payload]) + if params['payload'] + create_event(:payload => params['payload']) { :success => true } else { :success => false, :error => "You must provide a JSON payload" } diff --git a/app/models/agents/peak_detector_agent.rb b/app/models/agents/peak_detector_agent.rb index a7f3c4e3..d8e058db 100644 --- a/app/models/agents/peak_detector_agent.rb +++ b/app/models/agents/peak_detector_agent.rb @@ -28,22 +28,22 @@ module Agents MD def validate_options - unless options[:expected_receive_period_in_days].present? && options[:message].present? && options[:value_path].present? + unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['value_path'].present? errors.add(:base, "expected_receive_period_in_days, value_path, and message are required") end end def default_options { - :expected_receive_period_in_days => "2", - :group_by_path => "filter", - :value_path => "count", - :message => "A peak was found" + 'expected_receive_period_in_days' => "2", + 'group_by_path' => "filter", + 'value_path' => "count", + 'message' => "A peak was found" } end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def receive(incoming_events) @@ -57,25 +57,23 @@ module Agents private def check_for_peak(group, event) - memory[:peaks] ||= {} - memory[:peaks][group] ||= [] + memory['peaks'] ||= {} + memory['peaks'][group] ||= [] - if memory[:data][group].length > 4 && (memory[:peaks][group].empty? || memory[:peaks][group].last < event.created_at.to_i - peak_spacing) + if memory['data'][group].length > 4 && (memory['peaks'][group].empty? || memory['peaks'][group].last < event.created_at.to_i - peak_spacing) average_value, standard_deviation = stats_for(group, :skip_last => 1) - newest_value, newest_time = memory[:data][group][-1].map(&:to_f) - - #p [newest_value, average_value, average_value + std_multiple * standard_deviation, standard_deviation] + newest_value, newest_time = memory['data'][group][-1].map(&:to_f) if newest_value > average_value + std_multiple * standard_deviation - memory[:peaks][group] << newest_time - memory[:peaks][group].reject! { |p| p <= newest_time - window_duration } - create_event :payload => {:message => options[:message], :peak => newest_value, :peak_time => newest_time, :grouped_by => group.to_s} + memory['peaks'][group] << newest_time + memory['peaks'][group].reject! { |p| p <= newest_time - window_duration } + create_event :payload => { 'message' => options['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s } end end end def stats_for(group, options = {}) - data = memory[:data][group].map { |d| d.first.to_f } + data = memory['data'][group].map { |d| d.first.to_f } data = data[0...(data.length - (options[:skip_last] || 0))] length = data.length.to_f mean = 0 @@ -94,39 +92,39 @@ module Agents end def window_duration - if options[:window_duration].present? # The older option - options[:window_duration].to_i + if options['window_duration'].present? # The older option + options['window_duration'].to_i else - (options[:window_duration_in_days] || 14).to_f.days + (options['window_duration_in_days'] || 14).to_f.days end end def std_multiple - (options[:std_multiple] || 3).to_f + (options['std_multiple'] || 3).to_f end def peak_spacing - if options[:peak_spacing].present? # The older option - options[:peak_spacing].to_i + if options['peak_spacing'].present? # The older option + options['peak_spacing'].to_i else - (options[:min_peak_spacing_in_days] || 2).to_f.days + (options['min_peak_spacing_in_days'] || 2).to_f.days end end def group_for(event) - ((options[:group_by_path].present? && Utils.value_at(event.payload, options[:group_by_path])) || 'no_group').to_sym + ((options['group_by_path'].present? && Utils.value_at(event.payload, options['group_by_path'])) || 'no_group') end def remember(group, event) - memory[:data] ||= {} - memory[:data][group] ||= [] - memory[:data][group] << [Utils.value_at(event.payload, options[:value_path]), event.created_at.to_i] + memory['data'] ||= {} + memory['data'][group] ||= [] + memory['data'][group] << [ Utils.value_at(event.payload, options['value_path']), event.created_at.to_i ] cleanup group end def cleanup(group) - newest_time = memory[:data][group].last.last - memory[:data][group].reject! { |value, time| time <= newest_time - window_duration } + newest_time = memory['data'][group].last.last + memory['data'][group].reject! { |value, time| time <= newest_time - window_duration } end end end \ No newline at end of file diff --git a/app/models/agents/post_agent.rb b/app/models/agents/post_agent.rb index d3582e25..3d5ae824 100644 --- a/app/models/agents/post_agent.rb +++ b/app/models/agents/post_agent.rb @@ -11,17 +11,17 @@ module Agents def default_options { - :post_url => "http://www.example.com", - :expected_receive_period_in_days => 1 + 'post_url' => "http://www.example.com", + 'expected_receive_period_in_days' => 1 } end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def validate_options - unless options[:post_url].present? && options[:expected_receive_period_in_days].present? + unless options['post_url'].present? && options['expected_receive_period_in_days'].present? errors.add(:base, "post_url and expected_receive_period_in_days are required fields") end end diff --git a/app/models/agents/sentiment_agent.rb b/app/models/agents/sentiment_agent.rb index c6a917b0..c0f6c5be 100644 --- a/app/models/agents/sentiment_agent.rb +++ b/app/models/agents/sentiment_agent.rb @@ -28,31 +28,31 @@ module Agents def default_options { - :content => "$.message.text[*]", - :expected_receive_period_in_days => 1 + 'content' => "$.message.text[*]", + 'expected_receive_period_in_days' => 1 } end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def receive(incoming_events) anew = self.class.sentiment_hash incoming_events.each do |event| - Utils.values_at(event.payload, options[:content]).each do |content| + Utils.values_at(event.payload, options['content']).each do |content| sent_values = sentiment_values anew, content - create_event :payload => { :content => content, - :valence => sent_values[0], - :arousal => sent_values[1], - :dominance => sent_values[2], - :original_event => event.payload } + create_event :payload => { 'content' => content, + 'valence' => sent_values[0], + 'arousal' => sent_values[1], + 'dominance' => sent_values[2], + 'original_event' => event.payload } end end end def validate_options - errors.add(:base, "content and expected_receive_period_in_days must be present") unless options[:content].present? && options[:expected_receive_period_in_days].present? + errors.add(:base, "content and expected_receive_period_in_days must be present") unless options['content'].present? && options['expected_receive_period_in_days'].present? end def self.sentiment_hash diff --git a/app/models/agents/translation_agent.rb b/app/models/agents/translation_agent.rb index 2fc073f3..7bbdb05d 100644 --- a/app/models/agents/translation_agent.rb +++ b/app/models/agents/translation_agent.rb @@ -17,26 +17,26 @@ module Agents def default_options { - :client_id => "xxxxxx", - :client_secret => "xxxxxx", - :to => "fi", - :expected_receive_period_in_days => 1, - :content => { - :text => "$.message.text", - :content => "$.xyz" + 'client_id' => "xxxxxx", + 'client_secret' => "xxxxxx", + 'to' => "fi", + 'expected_receive_period_in_days' => 1, + 'content' => { + 'text' => "$.message.text", + 'content' => "$.xyz" } } end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def translate(text, to, access_token) translate_uri = URI 'http://api.microsofttranslator.com/v2/Ajax.svc/Translate' params = { - :text => text, - :to => to + 'text' => text, + 'to' => to } translate_uri.query = URI.encode_www_form params request = Net::HTTP::Get.new translate_uri.request_uri @@ -47,7 +47,7 @@ module Agents end def validate_options - unless options[:client_id].present? && options[:client_secret].present? && options[:to].present? && options[:content].present? && options[:expected_receive_period_in_days].present? + unless options['client_id'].present? && options['client_secret'].present? && options['to'].present? && options['content'].present? && options['expected_receive_period_in_days'].present? errors.add :base, "client_id,client_secret,to,expected_receive_period_in_days and content are all required" end end @@ -60,16 +60,16 @@ module Agents def receive(incoming_events) auth_uri = URI "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13" - response = postform auth_uri, :client_id => options[:client_id], - :client_secret => options[:client_secret], + response = postform auth_uri, :client_id => options['client_id'], + :client_secret => options['client_secret'], :scope => "http://api.microsofttranslator.com", :grant_type => "client_credentials" access_token = JSON.parse(response.body)["access_token"] incoming_events.each do |event| translated_event = {} - options[:content].each_pair do |key, value| + options['content'].each_pair do |key, value| to_be_translated = Utils.values_at event.payload, value - translated_event[key] = translate to_be_translated.first, options[:to], access_token + translated_event[key] = translate to_be_translated.first, options['to'], access_token end create_event :payload => translated_event end diff --git a/app/models/agents/trigger_agent.rb b/app/models/agents/trigger_agent.rb index f8190c33..b32fff6e 100644 --- a/app/models/agents/trigger_agent.rb +++ b/app/models/agents/trigger_agent.rb @@ -23,57 +23,57 @@ module Agents MD def validate_options - unless options[:expected_receive_period_in_days].present? && options[:message].present? && options[:rules].present? && - options[:rules].all? { |rule| rule[:type].present? && VALID_COMPARISON_TYPES.include?(rule[:type]) && rule[:value].present? && rule[:path].present? } + unless options['expected_receive_period_in_days'].present? && options['message'].present? && options['rules'].present? && + options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? } errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required") end end def default_options { - :expected_receive_period_in_days => "2", - :rules => [{ - :type => "regex", - :value => "foo\\d+bar", - :path => "topkey.subkey.subkey.goal", - }], - :message => "Looks like your pattern matched in ''!" + 'expected_receive_period_in_days' => "2", + 'rules' => [{ + 'type' => "regex", + 'value' => "foo\\d+bar", + 'path' => "topkey.subkey.subkey.goal", + }], + 'message' => "Looks like your pattern matched in ''!" } end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def receive(incoming_events) incoming_events.each do |event| - match = options[:rules].all? do |rule| - value_at_path = Utils.value_at(event[:payload], rule[:path]) - case rule[:type] + match = options['rules'].all? do |rule| + value_at_path = Utils.value_at(event['payload'], rule['path']) + case rule['type'] when "regex" - value_at_path.to_s =~ Regexp.new(rule[:value], Regexp::IGNORECASE) + value_at_path.to_s =~ Regexp.new(rule['value'], Regexp::IGNORECASE) when "!regex" - value_at_path.to_s !~ Regexp.new(rule[:value], Regexp::IGNORECASE) + value_at_path.to_s !~ Regexp.new(rule['value'], Regexp::IGNORECASE) when "field>value" - value_at_path.to_f > rule[:value].to_f + value_at_path.to_f > rule['value'].to_f when "field>=value" - value_at_path.to_f >= rule[:value].to_f + value_at_path.to_f >= rule['value'].to_f when "field { :message => make_message(event[:payload]) } # Maybe this should include the - # original event as well? + create_event :payload => { 'message' => make_message(event[:payload]) } # Maybe this should include the + # original event as well? end end end diff --git a/app/models/agents/twilio_agent.rb b/app/models/agents/twilio_agent.rb index f22ef31b..9cec3268 100644 --- a/app/models/agents/twilio_agent.rb +++ b/app/models/agents/twilio_agent.rb @@ -9,7 +9,7 @@ module Agents description <<-MD The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled. - It is assumed that events have a `:message`, `:text`, or `:sms` key, the value of which is sent as the content of the text message/call. You can use Event Formatting Agent if your event does not provide these keys. + It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use Event Formatting Agent if your event does not provide these keys. Set `receiver_cell` to the number to receive text messages/call and `sender_cell` to the number sending them. @@ -22,35 +22,35 @@ module Agents def default_options { - :account_sid => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - :auth_token => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - :sender_cell => 'xxxxxxxxxx', - :receiver_cell => 'xxxxxxxxxx', - :server_url => 'http://somename.com:3000', - :receive_text => 'true', - :receive_call => 'false', - :expected_receive_period_in_days => '1' + 'account_sid' => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + 'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + 'sender_cell' => 'xxxxxxxxxx', + 'receiver_cell' => 'xxxxxxxxxx', + 'server_url' => 'http://somename.com:3000', + 'receive_text' => 'true', + 'receive_call' => 'false', + 'expected_receive_period_in_days' => '1' } end def validate_options - unless options[:account_sid].present? && options[:auth_token].present? && options[:sender_cell].present? && options[:receiver_cell].present? && options[:expected_receive_period_in_days].present? && options[:receive_call].present? && options[:receive_text].present? + unless options['account_sid'].present? && options['auth_token'].present? && options['sender_cell'].present? && options['receiver_cell'].present? && options['expected_receive_period_in_days'].present? && options['receive_call'].present? && options['receive_text'].present? errors.add(:base, 'account_sid, auth_token, sender_cell, receiver_cell, receive_text, receive_call and expected_receive_period_in_days are all required') end end def receive(incoming_events) - @client = Twilio::REST::Client.new options[:account_sid], options[:auth_token] - memory[:pending_calls] ||= {} + @client = Twilio::REST::Client.new options['account_sid'], options['auth_token'] + memory['pending_calls'] ||= {} incoming_events.each do |event| - message = (event.payload[:message] || event.payload[:text] || event.payload[:sms]).to_s + message = (event.payload['message'] || event.payload['text'] || event.payload['sms']).to_s if message != "" - if options[:receive_call].to_s == 'true' + if options['receive_call'].to_s == 'true' secret = SecureRandom.hex 3 - memory[:pending_calls][secret] = message + memory['pending_calls'][secret] = message make_call secret end - if options[:receive_text].to_s == 'true' + if options['receive_text'].to_s == 'true' message = message.slice 0..160 send_message message end @@ -59,19 +59,19 @@ module Agents end def working? - last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? + last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? end def send_message(message) - @client.account.sms.messages.create :from => options[:sender_cell], - :to => options[:receiver_cell], + @client.account.sms.messages.create :from => options['sender_cell'], + :to => options['receiver_cell'], :body => message end def make_call(secret) - @client.account.calls.create :from => options[:sender_cell], - :to => options[:receiver_cell], - :url => post_url(options[:server_url],secret) + @client.account.calls.create :from => options['sender_cell'], + :to => options['receiver_cell'], + :url => post_url(options['server_url'],secret) end def post_url(server_url,secret) @@ -79,9 +79,9 @@ module Agents end def receive_webhook(params) - if memory[:pending_calls].has_key? params[:secret].to_sym - response = Twilio::TwiML::Response.new {|r| r.Say memory[:pending_calls][params[:secret].to_sym], :voice => 'woman'} - memory[:pending_calls].delete params[:secret].to_sym + if memory['pending_calls'].has_key? params['secret'] + response = Twilio::TwiML::Response.new {|r| r.Say memory['pending_calls'][params['secret']], :voice => 'woman'} + memory['pending_calls'].delete params['secret'] [response.text, 200] end end diff --git a/app/models/agents/twitter_publish_agent.rb b/app/models/agents/twitter_publish_agent.rb index 823b504e..0576c469 100644 --- a/app/models/agents/twitter_publish_agent.rb +++ b/app/models/agents/twitter_publish_agent.rb @@ -19,25 +19,25 @@ module Agents MD def validate_options - unless options[:username].present? && - options[:expected_update_period_in_days].present? + unless options['username'].present? && + options['expected_update_period_in_days'].present? errors.add(:base, "username and expected_update_period_in_days are required") end end def working? - (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? end def default_options { - :username => "", - :expected_update_period_in_days => "10", - :consumer_key => "---", - :consumer_secret => "---", - :oauth_token => "---", - :oauth_token_secret => "---", - :message_path => "text" + 'username' => "", + 'expected_update_period_in_days' => "10", + 'consumer_key' => "---", + 'consumer_secret' => "---", + 'oauth_token' => "---", + 'oauth_token_secret' => "---", + 'message_path' => "text" } end @@ -47,22 +47,22 @@ module Agents incoming_events = incoming_events.first(20) end incoming_events.each do |event| - tweet_text = Utils.value_at(event.payload, options[:message_path]) + tweet_text = Utils.value_at(event.payload, options['message_path']) begin publish_tweet tweet_text create_event :payload => { - :success => true, - :published_tweet => tweet_text, - :agent_id => event.agent_id, - :event_id => event.id + 'success' => true, + 'published_tweet' => tweet_text, + 'agent_id' => event.agent_id, + 'event_id' => event.id } rescue Twitter::Error => e create_event :payload => { - :success => false, - :error => e.message, - :failed_tweet => tweet_text, - :agent_id => event.agent_id, - :event_id => event.id + 'success' => false, + 'error' => e.message, + 'failed_tweet' => tweet_text, + 'agent_id' => event.agent_id, + 'event_id' => event.id } end end diff --git a/app/models/agents/twitter_stream_agent.rb b/app/models/agents/twitter_stream_agent.rb index d21bcf75..eb0bbd26 100644 --- a/app/models/agents/twitter_stream_agent.rb +++ b/app/models/agents/twitter_stream_agent.rb @@ -54,26 +54,26 @@ module Agents default_schedule "11pm" def validate_options - unless options[:filters].present? && - options[:expected_update_period_in_days].present? && - options[:generate].present? + unless options['filters'].present? && + options['expected_update_period_in_days'].present? && + options['generate'].present? errors.add(:base, "expected_update_period_in_days, generate, and filters are required fields") end end def working? - event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? end def default_options { - :consumer_key => "---", - :consumer_secret => "---", - :oauth_token => "---", - :oauth_token_secret => "---", - :filters => %w[keyword1 keyword2], - :expected_update_period_in_days => "2", - :generate => "events" + 'consumer_key' => "---", + 'consumer_secret' => "---", + 'oauth_token' => "---", + 'oauth_token_secret' => "---", + 'filters' => %w[keyword1 keyword2], + 'expected_update_period_in_days' => "2", + 'generate' => "events" } end @@ -81,33 +81,33 @@ module Agents filter = lookup_filter(filter) if filter - if options[:generate] == "counts" + if options['generate'] == "counts" # Avoid memory pollution by reloading the Agent. agent = Agent.find(id) - agent.memory[:filter_counts] ||= {} - agent.memory[:filter_counts][filter.to_sym] ||= 0 - agent.memory[:filter_counts][filter.to_sym] += 1 - remove_unused_keys!(agent, :filter_counts) + agent.memory['filter_counts'] ||= {} + agent.memory['filter_counts'][filter] ||= 0 + agent.memory['filter_counts'][filter] += 1 + remove_unused_keys!(agent, 'filter_counts') agent.save! else - create_event :payload => status.merge(:filter => filter.to_s) + create_event :payload => status.merge('filter' => filter) end end end def check - if options[:generate] == "counts" && memory[:filter_counts] && memory[:filter_counts].length > 0 - memory[:filter_counts].each do |filter, count| - create_event :payload => { :filter => filter.to_s, :count => count, :time => Time.now.to_i } + if options['generate'] == "counts" && memory['filter_counts'] && memory['filter_counts'].length > 0 + memory['filter_counts'].each do |filter, count| + create_event :payload => { 'filter' => filter, 'count' => count, 'time' => Time.now.to_i } end end - memory[:filter_counts] = {} + memory['filter_counts'] = {} end protected def lookup_filter(filter) - options[:filters].each do |known_filter| + options['filters'].each do |known_filter| if known_filter == filter return filter elsif known_filter.is_a?(Array) @@ -120,7 +120,7 @@ module Agents def remove_unused_keys!(agent, base) if agent.memory[base] - (agent.memory[base].keys - agent.options[:filters].map {|f| f.is_a?(Array) ? f.first.to_sym : f.to_sym }).each do |removed_key| + (agent.memory[base].keys - agent.options['filters'].map {|f| f.is_a?(Array) ? f.first.to_s : f.to_s }).each do |removed_key| agent.memory[base].delete(removed_key) end end diff --git a/app/models/agents/twitter_user_agent.rb b/app/models/agents/twitter_user_agent.rb index 5f3cc627..0d7f57d2 100644 --- a/app/models/agents/twitter_user_agent.rb +++ b/app/models/agents/twitter_user_agent.rb @@ -41,36 +41,36 @@ module Agents default_schedule "every_1h" def validate_options - unless options[:username].present? && - options[:expected_update_period_in_days].present? + unless options['username'].present? && + options['expected_update_period_in_days'].present? errors.add(:base, "username and expected_update_period_in_days are required") end end def working? - event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? end def default_options { - :username => "tectonic", - :expected_update_period_in_days => "2", - :consumer_key => "---", - :consumer_secret => "---", - :oauth_token => "---", - :oauth_token_secret => "---" + 'username' => "tectonic", + 'expected_update_period_in_days' => "2", + 'consumer_key' => "---", + 'consumer_secret' => "---", + 'oauth_token' => "---", + 'oauth_token_secret' => "---" } end def check - since_id = memory[:since_id] || nil + since_id = memory['since_id'] || nil opts = {:count => 200, :include_rts => true, :exclude_replies => false, :include_entities => true, :contributor_details => true} opts.merge! :since_id => since_id unless since_id.nil? - tweets = Twitter.user_timeline(options[:username], opts) + tweets = Twitter.user_timeline(options['username'], opts) tweets.each do |tweet| - memory[:since_id] = tweet.id if !memory[:since_id] || (tweet.id > memory[:since_id]) + memory['since_id'] = tweet.id if !memory['since_id'] || (tweet.id > memory['since_id']) create_event :payload => tweet.attrs end diff --git a/app/models/agents/user_location_agent.rb b/app/models/agents/user_location_agent.rb index f1a6a601..e1b53ffd 100644 --- a/app/models/agents/user_location_agent.rb +++ b/app/models/agents/user_location_agent.rb @@ -30,15 +30,15 @@ module Agents MD def working? - event_created_within(2) && !recent_error_logs? + event_created_within?(2) && !recent_error_logs? end def default_options - { :secret => SecureRandom.hex(7) } + { 'secret' => SecureRandom.hex(7) } end def validate_options - errors.add(:base, "secret is required and must be longer than 4 characters") unless options[:secret].present? && options[:secret].length > 4 + errors.add(:base, "secret is required and must be longer than 4 characters") unless options['secret'].present? && options['secret'].length > 4 end end end \ No newline at end of file diff --git a/app/models/agents/weather_agent.rb b/app/models/agents/weather_agent.rb index 34058e04..44db74f0 100644 --- a/app/models/agents/weather_agent.rb +++ b/app/models/agents/weather_agent.rb @@ -41,34 +41,34 @@ module Agents default_schedule "8pm" def working? - event_created_within(2) && !recent_error_logs? + event_created_within?(2) && !recent_error_logs? end def wunderground - Wunderground.new(options[:api_key]) if key_setup? + Wunderground.new(options['api_key']) if key_setup? end def key_setup? - options[:api_key] && options[:api_key] != "your-key" + options['api_key'] && options['api_key'] != "your-key" end def default_options { - :api_key => "your-key", - :location => "94103" + 'api_key' => "your-key", + 'location' => "94103" } end def validate_options - errors.add(:base, "location is required") unless options[:location].present? || options[:zipcode].present? - errors.add(:base, "api_key is required") unless options[:api_key].present? + errors.add(:base, "location is required") unless options['location'].present? || options['zipcode'].present? + errors.add(:base, "api_key is required") unless options['api_key'].present? end def check if key_setup? - wunderground.forecast_for(options[:location] || options[:zipcode])["forecast"]["simpleforecast"]["forecastday"].each do |day| + wunderground.forecast_for(options['location'] || options['zipcode'])["forecast"]["simpleforecast"]["forecastday"].each do |day| if is_tomorrow?(day) - create_event :payload => day.merge(:location => options[:location] || options[:zipcode]) + create_event :payload => day.merge('location' => options['location'] || options['zipcode']) end end end diff --git a/app/models/agents/webhook_agent.rb b/app/models/agents/webhook_agent.rb index ce103d13..635d477a 100644 --- a/app/models/agents/webhook_agent.rb +++ b/app/models/agents/webhook_agent.rb @@ -18,7 +18,7 @@ module Agents * `secret` - A token that the host will provide for authentication. * `expected_receive_period_in_days` - How often you expect to receive events this way. Used to determine if the agent is working. - * `payload_path` - JSONPath of the attribute of the POST body to be + * `payload_path` - JSONPath of the attribute in the POST body to be used as the Event payload. MD end @@ -26,7 +26,7 @@ module Agents event_description do <<-MD The event payload is base on the value of the `payload_path` option, - which is set to `#{options[:payload_path]}`. + which is set to `#{options['payload_path']}`. MD end @@ -37,8 +37,8 @@ module Agents end def receive_webhook(params) - secret = params.delete(:secret) - return ["Not Authorized", 401] unless secret == options[:secret] + secret = params.delete('secret') + return ["Not Authorized", 401] unless secret == options['secret'] create_event(:payload => payload_for(params)) @@ -46,17 +46,17 @@ module Agents end def working? - event_created_within(options[:expected_receive_period_in_days]) && !recent_error_logs? + event_created_within(options['expected_receive_period_in_days']) && !recent_error_logs? end def validate_options - unless options[:secret].present? - errors.add(:base, "Must specify a :secret for 'Authenticating' requests") + unless options['secret'].present? + errors.add(:base, "Must specify a secret for 'Authenticating' requests") end end def payload_for(params) - Utils.values_at(params, options[:payload_path]) || {} + Utils.value_at(params, options['payload_path']) || {} end end end diff --git a/app/models/agents/website_agent.rb b/app/models/agents/website_agent.rb index 096baa40..d171e85b 100644 --- a/app/models/agents/website_agent.rb +++ b/app/models/agents/website_agent.rb @@ -15,19 +15,19 @@ module Agents To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes. - When parsing HTML or XML, these sub-hashes specify how to extract with a `:css` CSS selector and either `:text => true` or `attr` pointing to an attribute name to grab. An example: + When parsing HTML or XML, these sub-hashes specify how to extract with a `css` CSS selector and either `'text': true` or `attr` pointing to an attribute name to grab. An example: - :extract => { - :url => { :css => "#comic img", :attr => "src" }, - :title => { :css => "#comic img", :attr => "title" }, - :body_text => { :css => "div.main", :text => true } + 'extract': { + 'url': { 'css': "#comic img", 'attr': "src" }, + 'title': { 'css': "#comic img", 'attr': "title" }, + 'body_text': { 'css': "div.main", 'text': true } } When parsing JSON, these sub-hashes specify [JSONPaths](http://goessner.net/articles/JsonPath/) to the values that you care about. For example: - :extract => { - :title => { :path => "results.data[*].title" }, - :description => { :path => "results.data[*].description" } + 'extract': { + 'title': { 'path': "results.data[*].title" }, + 'description': { 'path': "results.data[*].description" } } Note that for all of the formats, whatever you extract MUST have the same number of matches for each extractor. E.g., if you're extracting rows, all extractors must match all rows. For generating CSS selectors, something like [SelectorGadget](http://selectorgadget.com) may be helpful. @@ -36,7 +36,7 @@ module Agents MD event_description do - "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print options[:extract]}" + "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print options['extract']}" end default_schedule "every_12h" @@ -44,33 +44,33 @@ module Agents UNIQUENESS_LOOK_BACK = 30 def working? - event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? end def default_options { - :expected_update_period_in_days => "2", - :url => "http://xkcd.com", - :type => "html", - :mode => :on_change, - :extract => { - :url => {:css => "#comic img", :attr => "src"}, - :title => {:css => "#comic img", :attr => "title"} + 'expected_update_period_in_days' => "2", + 'url' => "http://xkcd.com", + 'type' => "html", + 'mode' => :on_change, + 'extract' => { + 'url' => {'css' => "#comic img", 'attr' => "src"}, + 'title' => {'css' => "#comic img", 'attr' => "title"} } } end def validate_options - errors.add(:base, "url and expected_update_period_in_days are required") unless options[:expected_update_period_in_days].present? && options[:url].present? - if !options[:extract].present? && extraction_type != "json" + errors.add(:base, "url and expected_update_period_in_days are required") unless options['expected_update_period_in_days'].present? && options['url'].present? + if !options['extract'].present? && extraction_type != "json" errors.add(:base, "extract is required for all types except json") end end def check hydra = Typhoeus::Hydra.new - log "Fetching #{options[:url]}" - request = Typhoeus::Request.new(options[:url], :followlocation => true) + log "Fetching #{options['url']}" + request = Typhoeus::Request.new(options['url'], :followlocation => true) request.on_failure do |response| error "Failed: #{response.inspect}" end @@ -85,37 +85,37 @@ module Agents end else output = {} - options[:extract].each do |name, extraction_details| + options['extract'].each do |name, extraction_details| result = if extraction_type == "json" - output[name] = Utils.values_at(doc, extraction_details[:path]) + output[name] = Utils.values_at(doc, extraction_details['path']) else - output[name] = doc.css(extraction_details[:css]).map { |node| - if extraction_details[:attr] - node.attr(extraction_details[:attr]) - elsif extraction_details[:text] + output[name] = doc.css(extraction_details['css']).map { |node| + if extraction_details['attr'] + node.attr(extraction_details['attr']) + elsif extraction_details['text'] node.text() else - error ":attr or :text is required on HTML or XML extraction patterns" + error "'attr' or 'text' is required on HTML or XML extraction patterns" return end } end - log "Extracting #{extraction_type} at #{extraction_details[:path] || extraction_details[:css]}: #{result}" + log "Extracting #{extraction_type} at #{extraction_details['path'] || extraction_details['css']}: #{result}" end - num_unique_lengths = options[:extract].keys.map { |name| output[name].length }.uniq + num_unique_lengths = options['extract'].keys.map { |name| output[name].length }.uniq if num_unique_lengths.length != 1 - error "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}" + error "Got an uneven number of matches for #{options['name']}: #{options['extract'].inspect}" return end num_unique_lengths.first.times do |index| result = {} - options[:extract].keys.each do |name| + options['extract'].keys.each do |name| result[name] = output[name][index] if name.to_s == 'url' - result[name] = URI.join(options[:url], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? + result[name] = URI.join(options['url'], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? end end @@ -133,22 +133,22 @@ module Agents private def store_payload? result - !options[:mode] || options[:mode].to_s == "all" || (options[:mode].to_s == "on_change" && !previous_payloads.include?(result.to_json)) + !options['mode'] || options['mode'].to_s == "all" || (options['mode'].to_s == "on_change" && !previous_payloads.include?(result.to_json)) end def previous_payloads - events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options[:mode].to_s == "on_change" + events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options['mode'].to_s == "on_change" end def extract_full_json? - (!options[:extract].present? && extraction_type == "json") + (!options['extract'].present? && extraction_type == "json") end def extraction_type - (options[:type] || begin - if options[:url] =~ /\.(rss|xml)$/i + (options['type'] || begin + if options['url'] =~ /\.(rss|xml)$/i "xml" - elsif options[:url] =~ /\.json$/i + elsif options['url'] =~ /\.json$/i "json" else "html" diff --git a/app/models/agents/weibo_publish_agent.rb b/app/models/agents/weibo_publish_agent.rb index bc0fc07e..d95f97b9 100644 --- a/app/models/agents/weibo_publish_agent.rb +++ b/app/models/agents/weibo_publish_agent.rb @@ -20,24 +20,24 @@ module Agents MD def validate_options - unless options[:uid].present? && - options[:expected_update_period_in_days].present? + unless options['uid'].present? && + options['expected_update_period_in_days'].present? errors.add(:base, "expected_update_period_in_days and uid are required") end end def working? - (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && most_recent_event.payload['success'] == true && !recent_error_logs? end def default_options { - :uid => "", - :access_token => "---", - :app_key => "---", - :app_secret => "---", - :expected_update_period_in_days => "10", - :message_path => "text" + 'uid' => "", + 'access_token' => "---", + 'app_key' => "---", + 'app_secret' => "---", + 'expected_update_period_in_days' => "10", + 'message_path' => "text" } end @@ -47,25 +47,25 @@ module Agents incoming_events = incoming_events.first(20) end incoming_events.each do |event| - tweet_text = Utils.value_at(event.payload, options[:message_path]) + tweet_text = Utils.value_at(event.payload, options['message_path']) if event.agent.type == "Agents::TwitterUserAgent" tweet_text = unwrap_tco_urls(tweet_text, event.payload) end begin publish_tweet tweet_text create_event :payload => { - :success => true, - :published_tweet => tweet_text, - :agent_id => event.agent_id, - :event_id => event.id + 'success' => true, + 'published_tweet' => tweet_text, + 'agent_id' => event.agent_id, + 'event_id' => event.id } rescue OAuth2::Error => e create_event :payload => { - :success => false, - :error => e.message, - :failed_tweet => tweet_text, - :agent_id => event.agent_id, - :event_id => event.id + 'success' => false, + 'error' => e.message, + 'failed_tweet' => tweet_text, + 'agent_id' => event.agent_id, + 'event_id' => event.id } end end diff --git a/app/models/agents/weibo_user_agent.rb b/app/models/agents/weibo_user_agent.rb index 27c94b29..6c4de7ff 100644 --- a/app/models/agents/weibo_user_agent.rb +++ b/app/models/agents/weibo_user_agent.rb @@ -70,29 +70,29 @@ module Agents default_schedule "every_1h" def validate_options - unless options[:uid].present? && - options[:expected_update_period_in_days].present? + unless options['uid'].present? && + options['expected_update_period_in_days'].present? errors.add(:base, "expected_update_period_in_days and uid are required") end end def working? - event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? + event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? end def default_options { - :uid => "", - :access_token => "---", - :app_key => "---", - :app_secret => "---", - :expected_update_period_in_days => "2" + 'uid' => "", + 'access_token' => "---", + 'app_key' => "---", + 'app_secret' => "---", + 'expected_update_period_in_days' => "2" } end def check - since_id = memory[:since_id] || nil - opts = {:uid => options[:uid].to_i} + since_id = memory['since_id'] || nil + opts = {:uid => options['uid'].to_i} opts.merge! :since_id => since_id unless since_id.nil? # http://open.weibo.com/wiki/2/statuses/user_timeline/en @@ -101,7 +101,7 @@ module Agents resp[:statuses].each do |status| - memory[:since_id] = status.id if !memory[:since_id] || (status.id > memory[:since_id]) + memory['since_id'] = status.id if !memory['since_id'] || (status.id > memory['since_id']) create_event :payload => status.as_json end diff --git a/app/models/event.rb b/app/models/event.rb index 4aaddf1c..f9a7baad 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,23 +1,21 @@ +require 'json_serialized_field' + class Event < ActiveRecord::Base + include JSONSerializedField + attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at acts_as_mappable - serialize :payload + json_serialize :payload belongs_to :user - belongs_to :agent, :counter_cache => true - - before_save :symbolize_payload + belongs_to :agent, :counter_cache => true, :touch => :last_event_at scope :recent, lambda { |timespan = 12.hours.ago| where("events.created_at > ?", timespan) } - def symbolize_payload - self.payload = payload.recursively_symbolize_keys if payload.is_a?(Hash) - end - def reemit! agent.create_event :payload => payload, :lat => lat, :lng => lng end diff --git a/app/views/agents/show.html.erb b/app/views/agents/show.html.erb index a07d03a4..6f1b6359 100644 --- a/app/views/agents/show.html.erb +++ b/app/views/agents/show.html.erb @@ -11,7 +11,7 @@
  • Summary
  • Details
  • <% end %> -
  • Logs
  • +
  • '> Logs
  • <% if @agent.can_create_events? && @agent.events.count > 0 %>
  • <%= link_to ' Events'.html_safe, events_path(:agent => @agent.to_param) %>
  • diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index fcfc0d56..2ab0e63e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -36,7 +36,7 @@