Browse Source

Reformat code with Rubocop and manually adjust styles

Akinori MUSHA 1 year ago
parent
commit
930da10cea
89 changed files with 1685 additions and 1392 deletions
  1. 9 6
      app/concerns/email_concern.rb
  2. 3 3
      app/concerns/event_headers_concern.rb
  3. 18 14
      app/concerns/web_request_concern.rb
  4. 9 6
      app/controllers/agents/dry_runs_controller.rb
  5. 1 3
      app/helpers/scenario_helper.rb
  6. 2 2
      app/models/agent.rb
  7. 5 5
      app/models/agent_log.rb
  8. 35 29
      app/models/agents/adioso_agent.rb
  9. 18 12
      app/models/agents/aftership_agent.rb
  10. 6 3
      app/models/agents/attribute_difference_agent.rb
  11. 16 15
      app/models/agents/change_detector_agent.rb
  12. 1 1
      app/models/agents/commander_agent.rb
  13. 48 30
      app/models/agents/csv_agent.rb
  14. 68 58
      app/models/agents/data_output_agent.rb
  15. 6 6
      app/models/agents/de_duplication_agent.rb
  16. 3 2
      app/models/agents/delay_agent.rb
  17. 8 7
      app/models/agents/digest_agent.rb
  18. 38 37
      app/models/agents/dropbox_file_url_agent.rb
  19. 10 8
      app/models/agents/dropbox_watch_agent.rb
  20. 17 19
      app/models/agents/email_agent.rb
  21. 16 18
      app/models/agents/email_digest_agent.rb
  22. 6 5
      app/models/agents/event_formatting_agent.rb
  23. 33 25
      app/models/agents/evernote_agent.rb
  24. 14 7
      app/models/agents/ftpsite_agent.rb
  25. 6 4
      app/models/agents/gap_detector_agent.rb
  26. 18 17
      app/models/agents/google_calendar_publish_agent.rb
  27. 2 2
      app/models/agents/google_translation_agent.rb
  28. 12 12
      app/models/agents/growl_agent.rb
  29. 15 10
      app/models/agents/hipchat_agent.rb
  30. 10 11
      app/models/agents/http_status_agent.rb
  31. 158 119
      app/models/agents/human_task_agent.rb
  32. 38 31
      app/models/agents/imap_folder_agent.rb
  33. 11 10
      app/models/agents/jabber_agent.rb
  34. 24 24
      app/models/agents/java_script_agent.rb
  35. 47 38
      app/models/agents/jira_agent.rb
  36. 4 3
      app/models/agents/jq_agent.rb
  37. 8 10
      app/models/agents/json_parse_agent.rb
  38. 1 1
      app/models/agents/key_value_store_agent.rb
  39. 91 69
      app/models/agents/liquid_output_agent.rb
  40. 26 22
      app/models/agents/local_file_agent.rb
  41. 6 5
      app/models/agents/manual_event_agent.rb
  42. 17 22
      app/models/agents/mqtt_agent.rb
  43. 21 22
      app/models/agents/pdf_info_agent.rb
  44. 14 10
      app/models/agents/peak_detector_agent.rb
  45. 3 2
      app/models/agents/phantom_js_cloud_agent.rb
  46. 18 15
      app/models/agents/post_agent.rb
  47. 43 33
      app/models/agents/public_transport_agent.rb
  48. 15 10
      app/models/agents/pushbullet_agent.rb
  49. 9 10
      app/models/agents/pushover_agent.rb
  50. 8 5
      app/models/agents/read_file_agent.rb
  51. 26 25
      app/models/agents/rss_agent.rb
  52. 29 25
      app/models/agents/s3_agent.rb
  53. 1 1
      app/models/agents/scheduler_agent.rb
  54. 21 16
      app/models/agents/sentiment_agent.rb
  55. 18 19
      app/models/agents/shell_command_agent.rb
  56. 4 4
      app/models/agents/slack_agent.rb
  57. 16 17
      app/models/agents/stubhub_agent.rb
  58. 29 12
      app/models/agents/telegram_agent.rb
  59. 36 22
      app/models/agents/trigger_agent.rb
  60. 8 7
      app/models/agents/tumblr_likes_agent.rb
  61. 13 14
      app/models/agents/tumblr_publish_agent.rb
  62. 15 12
      app/models/agents/twilio_agent.rb
  63. 18 18
      app/models/agents/twilio_receive_text_agent.rb
  64. 1 1
      app/models/agents/twitter_action_agent.rb
  65. 1 1
      app/models/agents/twitter_favorites.rb
  66. 22 20
      app/models/agents/twitter_publish_agent.rb
  67. 1 1
      app/models/agents/twitter_search_agent.rb
  68. 5 5
      app/models/agents/twitter_stream_agent.rb
  69. 1 1
      app/models/agents/twitter_user_agent.rb
  70. 19 12
      app/models/agents/user_location_agent.rb
  71. 20 20
      app/models/agents/weather_agent.rb
  72. 22 12
      app/models/agents/webhook_agent.rb
  73. 62 45
      app/models/agents/website_agent.rb
  74. 15 19
      app/models/agents/weibo_publish_agent.rb
  75. 7 10
      app/models/agents/weibo_user_agent.rb
  76. 36 36
      app/models/agents/witai_agent.rb
  77. 26 20
      app/models/event.rb
  78. 2 2
      app/models/link.rb
  79. 10 12
      app/models/scenario.rb
  80. 2 2
      app/models/scenario_membership.rb
  81. 17 15
      app/models/service.rb
  82. 32 23
      app/models/user.rb
  83. 1 1
      app/models/user_credential.rb
  84. 14 8
      app/presenters/form_configurable_agent_presenter.rb
  85. 2 1
      lib/location.rb
  86. 18 17
      spec/controllers/agents/dry_runs_controller_spec.rb
  87. 3 1
      spec/features/create_an_agent_spec.rb
  88. 82 72
      spec/models/agent_spec.rb
  89. 15 5
      spec/models/agents/shell_command_agent_spec.rb

+ 9 - 6
app/concerns/email_concern.rb

@@ -8,12 +8,15 @@ module EmailConcern
   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?
 
     if options['recipients'].present?
       emails = options['recipients']
       emails = [emails] if emails.is_a?(String)
-      unless emails.all? { |email| email =~ Devise.email_regexp || email =~ /\{/ }
+      unless emails.all? { |email| Devise.email_regexp === email || /\{/ === email }
         errors.add(:base, "'when provided, 'recipients' should be an email address or an array of email addresses")
       end
     end
@@ -40,16 +43,16 @@ module EmailConcern
     if payload.is_a?(Hash)
       payload = ActiveSupport::HashWithIndifferentAccess.new(payload)
       MAIN_KEYS.each do |key|
-        return { :title => payload[key].to_s, :entries => present_hash(payload, key) } if payload.has_key?(key)
+        return { title: payload[key].to_s, entries: present_hash(payload, key) } if payload.has_key?(key)
       end
 
-      { :title => "Event", :entries => present_hash(payload) }
+      { title: "Event", entries: present_hash(payload) }
     else
-      { :title => payload.to_s, :entries => [] }
+      { title: payload.to_s, entries: [] }
     end
   end
 
   def present_hash(hash, skip_key = nil)
-    hash.to_a.sort_by {|a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless k.to_s == skip_key.to_s }.compact
+    hash.to_a.sort_by { |a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless k.to_s == skip_key.to_s }.compact
   end
 end

+ 3 - 3
app/concerns/event_headers_concern.rb

@@ -14,11 +14,11 @@ module EventHeadersConcern
   def event_headers_normalizer
     case interpolated['event_headers_style']
     when nil, '', 'capitalized'
-      ->name { name.gsub(/[^-]+/, &:capitalize) }
+      ->(name) { name.gsub(/[^-]+/, &:capitalize) }
     when 'downcased'
       :downcase.to_proc
-    when 'snakecased', nil
-      ->name { name.tr('A-Z-', 'a-z_') }
+    when 'snakecased'
+      ->(name) { name.tr('A-Z-', 'a-z_') }
     when 'raw'
       :itself.to_proc
     else

+ 18 - 14
app/concerns/web_request_concern.rb

@@ -38,8 +38,12 @@ module WebRequestConcern
           # Not all Faraday adapters support automatic charset
           # detection, so we do that.
           case env[:response_headers][:content_type]
-          when /;\s*charset\s*=\s*([^()<>@,;:\\\"\/\[\]?={}\s]+)/i
-            encoding = Encoding.find($1) rescue @default_encoding
+          when /;\s*charset\s*=\s*([^()<>@,;:\\"\/\[\]?={}\s]+)/i
+            encoding = begin
+              Encoding.find($1)
+            rescue StandardError
+              @default_encoding
+            end
           when /\A\s*(?:text\/[^\s;]+|application\/(?:[^\s;]+\+)?(?:xml|json))\s*(?:;|\z)/i
             encoding = @default_encoding
           else
@@ -62,11 +66,11 @@ module WebRequestConcern
     if options['user_agent'].present?
       errors.add(:base, "user_agent must be a string") unless options['user_agent'].is_a?(String)
     end
-    
+
     if options['proxy'].present?
       errors.add(:base, "proxy must be a string") unless options['proxy'].is_a?(String)
     end
-    
+
     if options['disable_ssl_verification'].present? && boolify(options['disable_ssl_verification']).nil?
       errors.add(:base, "if provided, disable_ssl_verification must be true or false")
     end
@@ -112,13 +116,13 @@ module WebRequestConcern
     @faraday ||= Faraday.new(faraday_options) { |builder|
       builder.response :character_encoding,
                        force_encoding: interpolated['force_encoding'].presence,
-                       default_encoding: default_encoding,
+                       default_encoding:,
                        unzip: interpolated['unzip'].presence
 
       builder.headers = headers if headers.length > 0
 
       builder.headers[:user_agent] = user_agent
-      
+
       builder.proxy interpolated['proxy'].presence
 
       unless boolify(interpolated['disable_redirect_follow'])
@@ -140,8 +144,8 @@ module WebRequestConcern
       builder.use FaradayMiddleware::Gzip
 
       case backend = faraday_backend
-        when :typhoeus
-          require 'typhoeus/adapters/faraday'
+      when :typhoeus
+        require 'typhoeus/adapters/faraday'
       end
       builder.adapter backend
     }
@@ -153,12 +157,12 @@ module WebRequestConcern
 
   def basic_auth_credentials(value = interpolated['basic_auth'])
     case value
-      when nil, ''
-        return nil
-      when Array
-        return value if value.size == 2
-      when /:/
-        return value.split(/:/, 2)
+    when nil, ''
+      return nil
+    when Array
+      return value if value.size == 2
+    when /:/
+      return value.split(/:/, 2)
     end
     raise ArgumentError.new("bad value for basic_auth: #{value.inspect}")
   end

+ 9 - 6
app/controllers/agents/dry_runs_controller.rb

@@ -7,7 +7,7 @@ module Agents
                   current_user.agents.find_by(id: params[:agent_id]).received_events.limit(5)
                 elsif params[:source_ids]
                   Event.where(agent_id: current_user.agents.where(id: params[:source_ids]).pluck(:id))
-                       .order("id DESC").limit(5)
+                    .order("id DESC").limit(5)
                 else
                   []
                 end
@@ -40,11 +40,14 @@ module Agents
 
         @results = agent.dry_run!(event)
       else
-        @results = { events: [], memory: [],
-                     log:  [
-                       "#{pluralize(agent.errors.count, "error")} prohibited this Agent from being saved:",
-                       *agent.errors.full_messages
-                     ].join("\n- ") }
+        @results = {
+          events: [],
+          memory: [],
+          log: [
+            "#{pluralize(agent.errors.count, "error")} prohibited this Agent from being saved:",
+            *agent.errors.full_messages
+          ].join("\n- ")
+        }
       end
 
       render layout: false

+ 1 - 3
app/helpers/scenario_helper.rb

@@ -1,7 +1,6 @@
 module ScenarioHelper
-
   def style_colors(scenario)
-    colors = {
+    {
       color: scenario.tag_fg_color || default_scenario_fg_color,
       background_color: scenario.tag_bg_color || default_scenario_bg_color
     }.map { |key, value| "#{key.to_s.dasherize}:#{value}" }.join(';')
@@ -19,5 +18,4 @@ module ScenarioHelper
   def default_scenario_fg_color
     '#FFFFFF'
   end
-
 end

+ 2 - 2
app/models/agent.rb

@@ -325,7 +325,7 @@ class Agent < ActiveRecord::Base
         # Give it a unique name
         2.step do |i|
           name = '%s (%d)' % [original.name, i]
-          unless exists?(name: name)
+          unless exists?(name:)
             clone.name = name
             break
           end
@@ -454,7 +454,7 @@ class Agent < ActiveRecord::Base
     def run_schedule(schedule)
       return if schedule == 'never'
 
-      types = where(schedule: schedule).group(:type).pluck(:type)
+      types = where(schedule:).group(:type).pluck(:type)
       types.each do |type|
         next unless valid_type?(type)
 

+ 5 - 5
app/models/agent_log.rb

@@ -3,11 +3,11 @@
 # Agents' `last_error_log_at` column.  These are often used to determine if an Agent is `working?`.
 class AgentLog < ActiveRecord::Base
   belongs_to :agent
-  belongs_to :inbound_event, :class_name => "Event", optional: true
-  belongs_to :outbound_event, :class_name => "Event", optional: true
+  belongs_to :inbound_event, class_name: "Event", optional: true
+  belongs_to :outbound_event, class_name: "Event", optional: true
 
   validates_presence_of :message
-  validates_numericality_of :level, :only_integer => true, :greater_than_or_equal_to => 0, :less_than => 5
+  validates_numericality_of :level, only_integer: true, greater_than_or_equal_to: 0, less_than: 5
 
   before_validation :scrub_message
   before_save :truncate_message
@@ -15,7 +15,7 @@ class AgentLog < ActiveRecord::Base
   def self.log_for_agent(agent, message, options = {})
     puts "Agent##{agent.id}: #{message}" unless Rails.env.test?
 
-    log = agent.logs.create! options.merge(:message => message)
+    log = agent.logs.create! options.merge(message:)
     if agent.logs.count > log_length
       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
@@ -35,7 +35,7 @@ class AgentLog < ActiveRecord::Base
   def scrub_message
     if message_changed? && !message.nil?
       self.message = message.inspect unless message.is_a?(String)
-      self.message.scrub!{ |bytes| "<#{bytes.unpack('H*')[0]}>" }
+      self.message.scrub! { |bytes| "<#{bytes.unpack1('H*')}>" }
     end
     true
   end

+ 35 - 29
app/models/agents/adioso_agent.rb

@@ -2,26 +2,25 @@ module Agents
   class AdiosoAgent < Agent
     cannot_receive_events!
 
-  	default_schedule "every_1d"
+    default_schedule "every_1d"
 
-    description <<-MD
-  		The Adioso Agent will tell you the minimum airline prices between a pair of cities, and within a certain period of time.
+    description <<~MD
+      The Adioso Agent will tell you the minimum airline prices between a pair of cities, and within a certain period of time.
 
-      The currency is USD. Please make sure that the difference between `start_date` and `end_date` is less than 150 days. You will need to contact [Adioso](http://adioso.com/)
-  		for a `username` and `password`.
+      The currency is USD. Please make sure that the difference between `start_date` and `end_date` is less than 150 days. You will need to contact [Adioso](http://adioso.com/) for a `username` and `password`.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       If flights are present then events look like:
 
           {
             "cost": 75.23,
             "date": "June 25, 2013",
-  			    "route": "New York to Chicago"
+            "route": "New York to Chicago"
           }
 
       otherwise
-    
+
           {
             "nonetodest": "No flights found to the specified destination"
           }
@@ -30,12 +29,12 @@ 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"
+        '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
 
@@ -44,10 +43,12 @@ module Agents
     end
 
     def validate_options
-			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
+      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
 
     def date_to_unix_epoch(date)
       date.to_time.to_i
@@ -60,19 +61,24 @@ module Agents
           password: interpolated[:password]
         }
       }
-      parse_response = HTTParty.get "http://api.adioso.com/v2/search/parse?#{{ q: "#{interpolated[:from]} to #{interpolated[:to]}" }.to_query}", auth_options
-      fare_request = parse_response["search_url"].gsub /(end=)(\d*)([^\d]*)(\d*)/, "\\1#{date_to_unix_epoch(interpolated['end_date'])}\\3#{date_to_unix_epoch(interpolated['start_date'])}"
+      parse_response = HTTParty.get(
+        "http://api.adioso.com/v2/search/parse?#{{ q: "#{interpolated[:from]} to #{interpolated[:to]}" }.to_query}",
+        auth_options
+      )
+      fare_request = parse_response["search_url"].gsub(
+        /(end=)(\d*)([^\d]*)(\d*)/,
+        "\\1#{date_to_unix_epoch(interpolated['end_date'])}\\3#{date_to_unix_epoch(interpolated['start_date'])}"
+      )
       fare = HTTParty.get fare_request, auth_options
 
-			if fare["warnings"]
-				create_event :payload => fare["warnings"]
-			else
-				event = fare["results"].min {|a,b| a["cost"] <=> b["cost"]}
-				event["date"]  = Time.at(event["date"]).to_date.httpdate[0..15]
-				event["route"] = "#{interpolated['from']} to #{interpolated['to']}"
-				create_event :payload => event
-			end
+      if fare["warnings"]
+        create_event payload: fare["warnings"]
+      else
+        event = fare["results"].min_by { |x| x["cost"] }
+        event["date"]  = Time.at(event["date"]).to_date.httpdate[0..15]
+        event["route"] = "#{interpolated['from']} to #{interpolated['to']}"
+        create_event payload: event
+      end
     end
   end
 end
-

+ 18 - 12
app/models/agents/aftership_agent.rb

@@ -2,21 +2,20 @@ require 'uri'
 
 module Agents
   class AftershipAgent < Agent
-
     cannot_receive_events!
 
     default_schedule "every_10m"
 
-    description <<-MD
+    description <<~MD
       The Aftership agent allows you to track your shipment from aftership and emit them into events.
 
       To be able to use the Aftership API, you need to generate an `API Key`. You need a paying plan to use their tracking feature.
 
       You can use this agent to retrieve tracking data.
- 
-      Provide the `path` for the API endpoint that you'd like to hit. For example, for all active packages, enter `trackings` 
-      (see https://www.aftership.com/docs/api/4/trackings), for a specific package, use `trackings/SLUG/TRACKING_NUMBER` 
-      and replace `SLUG` with a courier code and `TRACKING_NUMBER` with the tracking number. You can request last checkpoint of a package 
+
+      Provide the `path` for the API endpoint that you'd like to hit. For example, for all active packages, enter `trackings`
+      (see https://www.aftership.com/docs/api/4/trackings), for a specific package, use `trackings/SLUG/TRACKING_NUMBER`
+      and replace `SLUG` with a courier code and `TRACKING_NUMBER` with the tracking number. You can request last checkpoint of a package
       by providing `last_checkpoint/SLUG/TRACKING_NUMBER` instead.
 
       You can get a list of courier information here `https://www.aftership.com/courier`
@@ -27,7 +26,7 @@ module Agents
       * `path request and its full path`
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       A typical tracking event have 2 important objects (tracking, and checkpoint) and the tracking/checkpoint looks like this.
 
           "trackings": [
@@ -87,8 +86,9 @@ module Agents
     MD
 
     def default_options
-      { 'api_key' => 'YOUR_API_KEY',
-        'path' => 'trackings'
+      {
+        'api_key' => 'YOUR_API_KEY',
+        'path' => 'trackings',
       }
     end
 
@@ -104,10 +104,11 @@ module Agents
     def check
       response = HTTParty.get(event_url, request_options)
       events = JSON.parse response.body
-      create_event :payload => events
+      create_event payload: events
     end
 
-  private
+    private
+
     def base_url
       "https://api.aftership.com/v4/"
     end
@@ -117,7 +118,12 @@ module Agents
     end
 
     def request_options
-      {:headers => {"aftership-api-key" => interpolated['api_key'], "Content-Type"=>"application/json"} }
+      {
+        headers: {
+          "aftership-api-key" => interpolated['api_key'],
+          "Content-Type" => "application/json",
+        }
+      }
     end
   end
 end

+ 6 - 3
app/models/agents/attribute_difference_agent.rb

@@ -2,7 +2,7 @@ module Agents
   class AttributeDifferenceAgent < Agent
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Attribute Difference Agent receives events and emits a new event with
       the difference or change of a specific attribute in comparison to the previous
       event received.
@@ -30,7 +30,7 @@ module Agents
       All configuration options will be liquid interpolated based on the incoming event.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       This will change based on the source event.
     MD
 
@@ -80,23 +80,26 @@ module Agents
         payload[opts['output']] = difference
       end
 
-      created_event = create_event(payload: payload)
+      created_event = create_event(payload:)
       log('Propagating new event', outbound_event: created_event, inbound_event: event)
       update_memory(attribute_value)
     end
 
     def calculate_integer_difference(new_value)
       return 0 if last_value.nil?
+
       (new_value.to_i - last_value.to_i)
     end
 
     def calculate_decimal_difference(new_value, dec_pre)
       return 0.0 if last_value.nil?
+
       (new_value.to_f - last_value.to_f).round(dec_pre.to_i)
     end
 
     def calculate_percentage_change(new_value, dec_pre)
       return 0.0 if last_value.nil?
+
       (((new_value.to_f / last_value.to_f) * 100) - 100).round(dec_pre.to_i)
     end
 

+ 16 - 15
app/models/agents/change_detector_agent.rb

@@ -2,7 +2,7 @@ module Agents
   class ChangeDetectorAgent < Agent
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Change Detector Agent receives a stream of events and emits a new event when a property of the received event changes.
 
       `property` specifies a [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) template that expands to the property to be watched, where you can use a variable `last_property` for the last property value.  If you want to detect a new lowest price, try this: `{% assign drop = last_property | minus: price %}{% if last_property == blank or drop > 0 %}{{ price | default: last_property }}{% else %}{{ last_property }}{% endif %}`
@@ -12,22 +12,22 @@ module Agents
       The resulting event will be a copy of the received event.
     MD
 
-    event_description <<-MD
-    This will change based on the source event. If you were event from the ShellCommandAgent, your outbound event might look like:
+    event_description <<~MD
+      This will change based on the source event. If you were event from the ShellCommandAgent, your outbound event might look like:
 
-      {
-        'command' => 'pwd',
-        'path' => '/home/Huginn',
-        'exit_status' => '0',
-        'errors' => '',
-        'output' => '/home/Huginn'
-      }
+          {
+            'command' => 'pwd',
+            'path' => '/home/Huginn',
+            'exit_status' => '0',
+            'errors' => '',
+            'output' => '/home/Huginn'
+          }
     MD
 
     def default_options
       {
-          'property' => '{{output}}',
-          'expected_update_period_in_days' => 1
+        'property' => '{{output}}',
+        'expected_update_period_in_days' => 1
       }
     end
 
@@ -55,12 +55,13 @@ module Agents
     def handle(opts, event = nil)
       property = opts['property']
       if has_changed?(property)
-        created_event = create_event :payload => event.payload
+        created_event = create_event payload: event.payload
 
-        log("Propagating new event as property has changed to #{property} from #{last_property}", :outbound_event => created_event, :inbound_event => event )
+        log("Propagating new event as property has changed to #{property} from #{last_property}",
+            outbound_event: created_event, inbound_event: event)
         update_memory(property)
       else
-        log("Not propagating as incoming event has not changed from #{last_property}.", :inbound_event => event )
+        log("Not propagating as incoming event has not changed from #{last_property}.", inbound_event: event)
       end
     end
 

+ 1 - 1
app/models/agents/commander_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     cannot_create_events!
 
-    description <<-MD
+    description <<~MD
       The Commander Agent is triggered by schedule or an incoming event, and commands other agents ("targets") to run, disable, configure, or enable themselves.
 
       # Action types

+ 48 - 30
app/models/agents/csv_agent.rb

@@ -19,7 +19,7 @@ module Agents
     end
 
     description do
-      <<-MD
+      <<~MD
         The `CsvAgent` parses or serializes CSV data. When parsing, events can either be emitted for the entire CSV, or one per row.
 
         Set `mode` to `parse` to parse CSV from incoming event, when set to `serialize` the agent serilizes the data of events to CSV.
@@ -53,28 +53,44 @@ module Agents
     end
 
     event_description do
-      "Events will looks like this:\n\n    %s" % if interpolated['mode'] == 'parse'
-        rows = if boolify(interpolated['with_header'])
-          [{'column' => 'row1 value1', 'column2' => 'row1 value2'}, {'column' => 'row2 value3', 'column2' => 'row2 value4'}]
-        else
-          [['row1 value1', 'row1 value2'], ['row2 value1', 'row2 value2']]
-        end
-        if interpolated['output'] == 'event_per_row'
-          Utils.pretty_print(interpolated['data_key'] => rows[0])
+      data =
+        if interpolated['mode'] == 'parse'
+          rows =
+            if boolify(interpolated['with_header'])
+              [
+                { 'column' => 'row1 value1', 'column2' => 'row1 value2' },
+                { 'column' => 'row2 value3', 'column2' => 'row2 value4' },
+              ]
+            else
+              [
+                ['row1 value1', 'row1 value2'],
+                ['row2 value1', 'row2 value2'],
+              ]
+            end
+          if interpolated['output'] == 'event_per_row'
+            rows[0]
+          else
+            rows
+          end
         else
-          Utils.pretty_print(interpolated['data_key'] => rows)
+          <<~EOS
+            "generated","csv","data"
+            "column1","column2","column3"
+          EOS
         end
-      else
-        Utils.pretty_print(interpolated['data_key'] => '"generated","csv","data"' + "\n" + '"column1","column2","column3"')
-      end
+
+      "Events will looks like this:\n\n    " +
+        Utils.pretty_print({
+          interpolated['data_key'] => data
+        })
     end
 
-    form_configurable :mode, type: :array, values: %w(parse serialize)
+    form_configurable :mode, type: :array, values: %w[parse serialize]
     form_configurable :separator, type: :string
     form_configurable :data_key, type: :string
     form_configurable :with_header, type: :boolean
     form_configurable :use_fields, type: :string
-    form_configurable :output, type: :array, values: %w(event_per_row event_per_file)
+    form_configurable :output, type: :array, values: %w[event_per_row event_per_file]
     form_configurable :data_path, type: :string
 
     def validate_options
@@ -100,27 +116,28 @@ module Agents
     end
 
     private
+
     def serialize(incoming_events)
       mo = interpolated(incoming_events.first)
       rows = rows_from_events(incoming_events, mo)
-      csv = CSV.generate(col_sep: separator(mo), force_quotes: true ) do |csv|
+      csv = CSV.generate(col_sep: separator(mo), force_quotes: true) do |csv|
         if boolify(mo['with_header']) && rows.first.is_a?(Hash)
-          if mo['use_fields'].present?
-            csv << extract_options(mo)
-          else
-            csv << rows.first.keys
-          end
+          csv << if mo['use_fields'].present?
+                   extract_options(mo)
+                 else
+                   rows.first.keys
+                 end
         end
         rows.each do |data|
-          if data.is_a?(Hash)
-            if mo['use_fields'].present?
-              csv << data.extract!(*extract_options(mo)).values
-            else
-              csv << data.values
-            end
-          else
-            csv << data
-          end
+          csv << if data.is_a?(Hash)
+                   if mo['use_fields'].present?
+                     data.extract!(*extract_options(mo)).values
+                   else
+                     data.values
+                   end
+                 else
+                   data
+                 end
         end
       end
       create_event payload: { mo['data_key'] => csv }
@@ -143,6 +160,7 @@ module Agents
       incoming_events.each do |event|
         mo = interpolated(event)
         next unless io = local_get_io(event)
+
         if mo['output'] == 'event_per_row'
           parse_csv(io, mo) do |payload|
             create_event payload: { mo['data_key'] => payload }

+ 68 - 58
app/models/agents/data_output_agent.rb

@@ -5,13 +5,13 @@ module Agents
     cannot_be_scheduled!
     cannot_create_events!
 
-    description  do
-      <<-MD
+    description do
+      <<~MD
         The Data Output Agent outputs received events as either RSS or JSON.  Use it to output a public or private stream of Huginn data.
 
         This Agent will output data at:
 
-        `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id: user_id, secret: ':secret', format: :xml)}`
+        `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id:, secret: ':secret', format: :xml)}`
 
         where `:secret` is one of the allowed secrets specified in your options and the extension can be `xml` or `json`.
 
@@ -104,7 +104,8 @@ module Agents
       end
 
       unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
-        errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
+        errors.add(:base,
+                   "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
       end
 
       unless options['template'].present? && options['template']['item'].present? && options['template']['item'].is_a?(Hash)
@@ -153,11 +154,12 @@ module Agents
 
     def feed_url(options = {})
       interpolated['template']['self'].presence ||
-        feed_link + Rails.application.routes.url_helpers.
-                    web_requests_path(agent_id: id || ':id',
-                                      user_id: user_id,
-                                      secret: options[:secret],
-                                      format: options[:format])
+        feed_link + Rails.application.routes.url_helpers.web_requests_path(
+          agent_id: id || ':id',
+          user_id:,
+          secret: options[:secret],
+          format: options[:format]
+        )
     end
 
     def feed_icon
@@ -165,9 +167,9 @@ module Agents
     end
 
     def itunes_icon
-      if(boolify(interpolated['ns_itunes']))
+      if boolify(interpolated['ns_itunes'])
         "<itunes:image href=#{feed_icon.encode(xml: :attr)} />"
-      end  
+      end
     end
 
     def feed_description
@@ -181,13 +183,13 @@ module Agents
     def xml_namespace
       namespaces = ['xmlns:atom="http://www.w3.org/2005/Atom"']
 
-      if (boolify(interpolated['ns_dc']))
+      if boolify(interpolated['ns_dc'])
         namespaces << 'xmlns:dc="http://purl.org/dc/elements/1.1/"'
       end
-      if (boolify(interpolated['ns_media']))
+      if boolify(interpolated['ns_media'])
         namespaces << 'xmlns:media="http://search.yahoo.com/mrss/"'
       end
-      if (boolify(interpolated['ns_itunes']))
+      if boolify(interpolated['ns_itunes'])
         namespaces << 'xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"'
       end
       namespaces.join(' ')
@@ -211,8 +213,8 @@ module Agents
 
       events =
         if (event_ids = memory[:event_ids]) &&
-           memory[:events_order] == events_order &&
-           memory[:events_to_show] >= events_to_show
+            memory[:events_order] == events_order &&
+            memory[:events_to_show] >= events_to_show
           received_events.where(id: event_ids).to_a
         else
           memory[:last_event_id] = nil
@@ -231,8 +233,8 @@ module Agents
             source_ids.flat_map { |source_id|
               # dig twice as many events as the number of
               # `events_to_show`
-              received_events.where(agent_id: source_id).
-                last(2 * events_to_show)
+              received_events.where(agent_id: source_id)
+                .last(2 * events_to_show)
             }.sort_by(&:id)
           end
 
@@ -260,18 +262,20 @@ module Agents
         end
       end
 
-      source_events = sort_events(latest_events(), 'events_list_order')
+      source_events = sort_events(latest_events, 'events_list_order')
 
       interpolate_with('events' => source_events) do
         items = source_events.map do |event|
           interpolated = interpolate_options(options['template']['item'], event)
-          interpolated['guid'] = {'_attributes' => {'isPermaLink' => 'false'},
-                                  '_contents' => interpolated['guid'].presence || event.id}
+          interpolated['guid'] = {
+            '_attributes' => { 'isPermaLink' => 'false' },
+            '_contents' => interpolated['guid'].presence || event.id
+          }
           date_string = interpolated['pubDate'].to_s
           date =
             begin
-              Time.zone.parse(date_string)  # may return nil
-            rescue => e
+              Time.zone.parse(date_string) # may return nil
+            rescue StandardError => e
               error "Error parsing a \"pubDate\" value \"#{date_string}\": #{e.message}"
               nil
             end || event.created_at
@@ -299,23 +303,23 @@ module Agents
 
           items = items_to_xml(items)
 
-          return [<<-XML, 200, rss_content_type, interpolated['response_headers'].presence]
-<?xml version="1.0" encoding="UTF-8" ?>
-<rss version="2.0" #{xml_namespace}>
-<channel>
- <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" />
- <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon>
- #{itunes_icon}
-#{hub_links}
- <title>#{feed_title.encode(xml: :text)}</title>
- <description>#{feed_description.encode(xml: :text)}</description>
- <link>#{feed_link.encode(xml: :text)}</link>
- <lastBuildDate>#{now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate>
- <pubDate>#{now.rfc2822.to_s.encode(xml: :text)}</pubDate>
- <ttl>#{feed_ttl}</ttl>
-#{items}
-</channel>
-</rss>
+          return [<<~XML, 200, rss_content_type, interpolated['response_headers'].presence]
+            <?xml version="1.0" encoding="UTF-8" ?>
+            <rss version="2.0" #{xml_namespace}>
+            <channel>
+             <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" />
+             <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon>
+             #{itunes_icon}
+            #{hub_links}
+             <title>#{feed_title.encode(xml: :text)}</title>
+             <description>#{feed_description.encode(xml: :text)}</description>
+             <link>#{feed_link.encode(xml: :text)}</link>
+             <lastBuildDate>#{now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate>
+             <pubDate>#{now.rfc2822.to_s.encode(xml: :text)}</pubDate>
+             <ttl>#{feed_ttl}</ttl>
+            #{items}
+            </channel>
+            </rss>
           XML
         end
       end
@@ -336,13 +340,17 @@ module Agents
 
     class XMLNode
       def initialize(tag_name, attributes, contents)
-        @tag_name, @attributes, @contents = tag_name, attributes, contents
+        @tag_name = tag_name
+        @attributes = attributes
+        @contents = contents
       end
 
       def to_xml(options)
         if @contents.is_a?(Hash)
           options[:builder].tag! @tag_name, @attributes do
-            @contents.each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options.merge(skip_instruct: true)) }
+            @contents.each { |key, value|
+              ActiveSupport::XmlMini.to_tag(key, value, options.merge(skip_instruct: true))
+            }
           end
         else
           options[:builder].tag! @tag_name, @attributes, @contents
@@ -353,15 +361,16 @@ module Agents
     def simplify_item_for_xml(item)
       if item.is_a?(Hash)
         item.each.with_object({}) do |(key, value), memo|
-          if value.is_a?(Hash)
-            if value.key?('_attributes') || value.key?('_contents')
-              memo[key] = XMLNode.new(key, value['_attributes'], simplify_item_for_xml(value['_contents']))
+          memo[key] =
+            if value.is_a?(Hash)
+              if value.key?('_attributes') || value.key?('_contents')
+                XMLNode.new(key, value['_attributes'], simplify_item_for_xml(value['_contents']))
+              else
+                simplify_item_for_xml(value)
+              end
             else
-              memo[key] = simplify_item_for_xml(value)
+              value
             end
-          else
-            memo[key] = value
-          end
         end
       elsif item.is_a?(Array)
         item.map { |value| simplify_item_for_xml(value) }
@@ -375,13 +384,14 @@ module Agents
         item.each.with_object({}) do |(key, value), memo|
           if value.is_a?(Hash)
             if value.key?('_attributes') || value.key?('_contents')
-              contents = if value['_contents'] && value['_contents'].is_a?(Hash)
-                           simplify_item_for_json(value['_contents'])
-                         elsif value['_contents']
-                           { "contents" => value['_contents'] }
-                         else
-                           {}
-                         end
+              contents =
+                if value['_contents'] && value['_contents'].is_a?(Hash)
+                  simplify_item_for_json(value['_contents'])
+                elsif value['_contents']
+                  { "contents" => value['_contents'] }
+                else
+                  {}
+                end
 
               memo[key] = contents.merge(value['_attributes'] || {})
             else
@@ -436,8 +446,8 @@ module Agents
           'hub.mode' => 'publish',
           'hub.url' => url
         }
-     rescue => e
-       error "Push failed: #{e.message}"
+      rescue StandardError => e
+        error "Push failed: #{e.message}"
       end
     end
   end

+ 6 - 6
app/models/agents/de_duplication_agent.rb

@@ -3,7 +3,7 @@ module Agents
     include FormConfigurable
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The De-duplication Agent receives a stream of events and remits the event if it is not a duplicate.
 
       `property` the value that should be used to determine the uniqueness of the event (empty to use the whole payload)
@@ -13,7 +13,7 @@ module Agents
       `expected_update_period_in_days` is used to determine if the Agent is working.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       The DeDuplicationAgent just reemits events it received.
     MD
 
@@ -56,18 +56,18 @@ module Agents
     def handle(opts, event = nil)
       property = get_hash(options['property'].blank? ? JSON.dump(event.payload) : opts['property'])
       if is_unique?(property)
-        created_event = create_event :payload => event.payload
+        created_event = create_event payload: event.payload
 
-        log("Propagating new event as '#{property}' is a new unique property.", :inbound_event => event )
+        log("Propagating new event as '#{property}' is a new unique property.", inbound_event: event)
         update_memory(property, opts['lookback'].to_i)
       else
-        log("Not propagating as incoming event is a duplicate.", :inbound_event => event )
+        log("Not propagating as incoming event is a duplicate.", inbound_event: event)
       end
     end
 
     def get_hash(property)
       if property.to_s.length > 10
-        Zlib::crc32(property).to_s
+        Zlib.crc32(property).to_s
       else
         property
       end

+ 3 - 2
app/models/agents/delay_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     default_schedule 'every_12h'
 
-    description <<-MD
+    description <<~MD
       The DelayAgent stores received Events and emits copies of them on a schedule. Use this as a buffer or queue of Events.
 
       `max_events` should be set to the maximum number of events that you'd like to hold in the buffer. When this number is
@@ -33,7 +33,8 @@ module Agents
 
     def validate_options
       unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
-        errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
+        errors.add(:base,
+                   "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
       end
 
       unless options['keep'].present? && options['keep'].in?(%w[newest oldest])

+ 8 - 7
app/models/agents/digest_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     default_schedule "6am"
 
-    description <<-MD
+    description <<~MD
       The Digest Agent collects any Events sent to it and emits them as a single event.
 
       The resulting Event will have a payload message of `message`. You can use liquid templating in the `message`, have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) for details.
@@ -16,7 +16,7 @@ module Agents
       For instance, say `retained_events` is set to 3 and the Agent has received Events `5`, `4`, and `3`. When a digest is sent, Events `5`, `4`, and `3` are retained for a future digest. After Event `6` is received, the next digest will contain Events `6`, `5`, and `4`.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
 
           {
@@ -27,9 +27,9 @@ module Agents
 
     def default_options
       {
-          "expected_receive_period_in_days" => "2",
-          "message" => "{{ events | map: 'message' | join: ',' }}",
-          "retained_events" => "0"
+        "expected_receive_period_in_days" => "2",
+        "message" => "{{ events | map: 'message' | join: ',' }}",
+        "retained_events" => "0"
       }
     end
 
@@ -38,7 +38,8 @@ module Agents
     form_configurable :retained_events
 
     def validate_options
-      errors.add(:base, 'retained_events must be 0 to 999') unless options['retained_events'].to_i >= 0 && options['retained_events'].to_i < 1000
+      errors.add(:base,
+                 'retained_events must be 0 to 999') unless options['retained_events'].to_i >= 0 && options['retained_events'].to_i < 1000
     end
 
     def working?
@@ -60,7 +61,7 @@ module Agents
         events = received_events.where(id: self.memory["queue"]).order(id: :asc).to_a
         payload = { "events" => events.map { |event| event.payload } }
         payload["message"] = interpolated(payload)["message"]
-        create_event :payload => payload
+        create_event(payload:)
         if interpolated["retained_events"].to_i == 0
           self.memory["queue"] = []
         end

+ 38 - 37
app/models/agents/dropbox_file_url_agent.rb

@@ -6,7 +6,7 @@ module Agents
     no_bulk_receive!
     can_dry_run!
 
-    description <<-MD
+    description <<~MD
       The _DropboxFileUrlAgent_ is used to work with Dropbox. It takes a file path (or multiple files paths) and emits events with either [temporary links](https://www.dropbox.com/developers/core/docs#media) or [permanent links](https://www.dropbox.com/developers/core/docs#shares).
 
       #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?}
@@ -34,39 +34,42 @@ module Agents
     MD
 
     event_description do
-      "Events will looks like this:\n\n    %s" % if options['link_type'] == 'permanent'
-        Utils.pretty_print({
-          url: "https://www.dropbox.com/s/abcde3/example?dl=1",
-          :".tag" => "file",
-          id: "id:abcde3",
-          name: "hi",
-          path_lower: "/huginn/hi",
-          link_permissions:          {
-            resolved_visibility: {:".tag"=>"public"},
-            requested_visibility: {:".tag"=>"public"},
-            can_revoke: true
-          },
-          client_modified: "2017-10-14T18:38:39Z",
-          server_modified: "2017-10-14T18:38:45Z",
-          rev: "31db0615354b",
-          size: 0
-        })
-      else
-        Utils.pretty_print({
-          url: "https://dl.dropboxusercontent.com/apitl/1/somelongurl",
-          metadata: {
-            name: "hi",
-            path_lower: "/huginn/hi",
-            path_display: "/huginn/hi",
-            id: "id:abcde3",
-            client_modified: "2017-10-14T18:38:39Z",
-            server_modified: "2017-10-14T18:38:45Z",
-            rev: "31db0615354b",
-            size: 0,
-            content_hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
-          }
-        })
-      end
+      "Events will looks like this:\n\n    " +
+        Utils.pretty_print(
+          if options['link_type'] == 'permanent'
+            {
+              url: "https://www.dropbox.com/s/abcde3/example?dl=1",
+              ".tag": "file",
+              id: "id:abcde3",
+              name: "hi",
+              path_lower: "/huginn/hi",
+              link_permissions: {
+                resolved_visibility: { ".tag": "public" },
+                requested_visibility: { ".tag": "public" },
+                can_revoke: true
+              },
+              client_modified: "2017-10-14T18:38:39Z",
+              server_modified: "2017-10-14T18:38:45Z",
+              rev: "31db0615354b",
+              size: 0
+            }
+          else
+            {
+              url: "https://dl.dropboxusercontent.com/apitl/1/somelongurl",
+              metadata: {
+                name: "hi",
+                path_lower: "/huginn/hi",
+                path_display: "/huginn/hi",
+                id: "id:abcde3",
+                client_modified: "2017-10-14T18:38:39Z",
+                server_modified: "2017-10-14T18:38:45Z",
+                rev: "31db0615354b",
+                size: 0,
+                content_hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+              }
+            }
+          end
+        )
     end
 
     def default_options
@@ -96,10 +99,8 @@ module Agents
 
     def permanent_url_for(path)
       dropbox.find(path).share_url.response.tap do |response|
-        response['url'].gsub!('?dl=0','?dl=1')
+        response['url'].gsub!('?dl=0', '?dl=1')
       end
     end
-
   end
-
 end

+ 10 - 8
app/models/agents/dropbox_watch_agent.rb

@@ -5,13 +5,13 @@ module Agents
     cannot_receive_events!
     default_schedule "every_1m"
 
-    description <<-MD
+    description <<~MD
       The Dropbox Watch Agent watches the given `dir_to_watch` and emits events with the detected changes.
-      
+
       #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?}
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       The event payload will contain the following fields:
 
           {
@@ -34,7 +34,8 @@ module Agents
 
     def validate_options
       errors.add(:base, 'The `dir_to_watch` property is required.') unless options['dir_to_watch'].present?
-      errors.add(:base, 'Invalid `expected_update_period_in_days` format.') unless options['expected_update_period_in_days'].present? && is_positive_integer?(options['expected_update_period_in_days'])
+      errors.add(:base,
+                 'Invalid `expected_update_period_in_days` format.') unless options['expected_update_period_in_days'].present? && is_positive_integer?(options['expected_update_period_in_days'])
     end
 
     def working?
@@ -52,7 +53,9 @@ module Agents
     private
 
     def ls(dir_to_watch)
-      dropbox.ls(dir_to_watch).map { |file| { 'path' => file.path, 'rev' => file.rev, 'modified' => file.server_modified } }
+      dropbox.ls(dir_to_watch).map { |file|
+        { 'path' => file.path, 'rev' => file.rev, 'modified' => file.server_modified }
+      }
     end
 
     def previous_contents
@@ -67,7 +70,8 @@ module Agents
 
     class DropboxDirDiff
       def initialize(previous, current)
-        @previous, @current = [previous || [], current || []]
+        @previous = previous || []
+        @current = current || []
       end
 
       def empty?
@@ -99,7 +103,5 @@ module Agents
         array.find { |entry| entry['path'] == path }
       end
     end
-
   end
-
 end

+ 17 - 19
app/models/agents/email_agent.rb

@@ -9,7 +9,7 @@ module Agents
     cannot_create_events!
     no_bulk_receive!
 
-    description <<-MD
+    description <<~MD
       The Email Agent sends any events it receives via email immediately.
 
       You can specify the email's subject line by providing a `subject` option, which can contain [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) formatting.  E.g.,
@@ -35,9 +35,9 @@ 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
 
@@ -48,21 +48,19 @@ module Agents
     def receive(incoming_events)
       incoming_events.each do |event|
         recipients(event.payload).each do |recipient|
-          begin
-            SystemMailer.send_message(
-              to: recipient,
-              from: interpolated(event)['from'],
-              subject: interpolated(event)['subject'],
-              headline: interpolated(event)['headline'],
-              body: interpolated(event)['body'],
-              content_type: interpolated(event)['content_type'],
-              groups: [present(event.payload)]
-            ).deliver_now
-            log "Sent mail to #{recipient} with event #{event.id}"
-          rescue => e
-            error("Error sending mail to #{recipient} with event #{event.id}: #{e.message}")
-            raise
-          end
+          SystemMailer.send_message(
+            to: recipient,
+            from: interpolated(event)['from'],
+            subject: interpolated(event)['subject'],
+            headline: interpolated(event)['headline'],
+            body: interpolated(event)['body'],
+            content_type: interpolated(event)['content_type'],
+            groups: [present(event.payload)]
+          ).deliver_now
+          log "Sent mail to #{recipient} with event #{event.id}"
+        rescue StandardError => e
+          error("Error sending mail to #{recipient} with event #{event.id}: #{e.message}")
+          raise
         end
       end
     end

+ 16 - 18
app/models/agents/email_digest_agent.rb

@@ -8,7 +8,7 @@ module Agents
 
     cannot_create_events!
 
-    description <<-MD
+    description <<~MD
       The Email Digest Agent collects any Events sent to it and sends them all via email when scheduled. The number of
       used events also relies on the `Keep events` option of the emitting Agent, meaning that if events expire before
       this agent is scheduled to run, they will not appear in the email.
@@ -30,9 +30,9 @@ 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
 
@@ -52,21 +52,19 @@ module Agents
         payloads = received_events.reorder("events.id ASC").where(id: self.memory['events']).pluck(:payload).to_a
         groups = payloads.map { |payload| present(payload) }
         recipients.each do |recipient|
-          begin
-            SystemMailer.send_message(
-              to: recipient,
-              from: interpolated['from'],
-              subject: interpolated['subject'],
-              headline: interpolated['headline'],
-              content_type: interpolated['content_type'],
-              groups: groups
-            ).deliver_now
+          SystemMailer.send_message(
+            to: recipient,
+            from: interpolated['from'],
+            subject: interpolated['subject'],
+            headline: interpolated['headline'],
+            content_type: interpolated['content_type'],
+            groups:
+          ).deliver_now
 
-            log "Sent digest mail to #{recipient}"
-          rescue => e
-            error("Error sending digest mail to #{recipient}: #{e.message}")
-            raise
-          end
+          log "Sent digest mail to #{recipient}"
+        rescue StandardError => e
+          error("Error sending digest mail to #{recipient}: #{e.message}")
+          raise
         end
         self.memory['events'] = []
       end

+ 6 - 5
app/models/agents/event_formatting_agent.rb

@@ -3,7 +3,7 @@ module Agents
     cannot_be_scheduled!
     can_dry_run!
 
-    description <<-MD
+    description <<~MD
       The Event Formatting Agent allows you to format incoming Events, adding new fields as needed.
 
       For example, here is a possible Event:
@@ -96,9 +96,10 @@ module Agents
     end
 
     def validate_options
-      errors.add(:base, "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present?
+      errors.add(:base,
+                 "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present?
 
-      if options['mode'].present? && !options['mode'].to_s.include?('{{') && !%[clean merge].include?(options['mode'].to_s)
+      if options['mode'].present? && !options['mode'].to_s.include?('{{') && !%(clean merge).include?(options['mode'].to_s)
         errors.add(:base, "mode must be 'clean' or 'merge'")
       end
 
@@ -108,7 +109,7 @@ module Agents
     def default_options
       {
         'instructions' => {
-          'message' =>  "You received a text {{text}} from {{fields.from}}",
+          'message' => "You received a text {{text}} from {{fields.from}}",
           'agent' => "{{agent.type}}",
           'some_other_field' => "Looks like the weather is going to be {{fields.weather}}"
         },
@@ -156,7 +157,7 @@ module Agents
         if regexp.present?
           begin
             Regexp.new(regexp)
-          rescue
+          rescue StandardError
             errors.add(:base, "bad regexp found in matchers: #{regexp}")
           end
         else

+ 33 - 25
app/models/agents/evernote_agent.rb

@@ -2,7 +2,7 @@ module Agents
   class EvernoteAgent < Agent
     include EvernoteConcern
 
-    description <<-MD
+    description <<~MD
       The Evernote Agent connects with a user's Evernote note store.
 
       Visit [Evernote](https://dev.evernote.com/doc/) to set up an Evernote app and receive an api key and secret.
@@ -53,7 +53,7 @@ module Agents
                 }
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       When `mode` is `update`, events look like:
 
           {
@@ -106,7 +106,7 @@ module Agents
     end
 
     def validate_options
-      errors.add(:base, "mode must be 'update' or 'read'") unless %w(read update).include?(options[:mode])
+      errors.add(:base, "mode must be 'update' or 'read'") unless %w[read update].include?(options[:mode])
 
       if options[:mode] == "update" && schedule != "never"
         errors.add(:base, "when mode is set to 'update', schedule must be 'never'")
@@ -116,7 +116,8 @@ module Agents
         errors.add(:base, "when mode is set to 'read', agent must have a schedule")
       end
 
-      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
 
       if options[:mode] == "update" && options[:note].values.all?(&:empty?)
         errors.add(:base, "you must specify at least one note parameter to create or update a note")
@@ -131,7 +132,7 @@ module Agents
       if options[:mode] == "update"
         incoming_events.each do |event|
           note = note_store.create_or_update_note(note_params(event))
-          create_event :payload => note.attr(include_content: include_xhtml_content?)
+          create_event payload: note.attr(include_content: include_xhtml_content?)
         end
       end
     end
@@ -146,15 +147,17 @@ module Agents
         opts.merge!(last_checked_at: (memory[:last_checked_at] ||= created_at.to_i * 1000))
 
         if opts[:tagNames]
-          opts.merge!(notes_with_tags: (memory[:notes_with_tags] ||=
-            NoteStore::Search.new(note_store, {tagNames: opts[:tagNames]}).note_guids))
+          notes_with_tags =
+            memory[:notes_with_tags] ||=
+              NoteStore::Search.new(note_store, { tagNames: opts[:tagNames] }).note_guids
+          opts.merge!(notes_with_tags:)
         end
 
         notes = NoteStore::Search.new(note_store, opts).notes
         notes.each do |note|
           memory[:notes_with_tags] << note.guid unless memory[:notes_with_tags].include?(note.guid)
 
-          create_event :payload => note.attr(include_resources: true, include_content: include_xhtml_content?)
+          create_event payload: note.attr(include_resources: true, include_content: include_xhtml_content?)
         end
 
         memory[:last_checked_at] = Time.now.to_i * 1000
@@ -185,19 +188,20 @@ module Agents
     # https://dev.evernote.com/doc/reference/
     class NoteStore
       attr_reader :en_note_store
+
       delegate :createNote, :updateNote, :getNote, :listNotebooks, :listTags, :getNotebook,
-               :createNotebook, :findNotesMetadata, :getNoteTagNames, :to => :en_note_store
+               :createNotebook, :findNotesMetadata, :getNoteTagNames, to: :en_note_store
 
       def initialize(en_note_store)
         @en_note_store = en_note_store
       end
 
       def create_or_update_note(params)
-        search = Search.new(self, {title: params[:title], notebook: params[:notebook]})
+        search = Search.new(self, { title: params[:title], notebook: params[:notebook] })
 
         # evernote search can only filter notes with titles containing a substring;
         # this finds a note with the exact title
-        note = search.notes.detect {|note| note.title == params[:title]}
+        note = search.notes.detect { |note| note.title == params[:title] }
 
         if note
           # a note with specified title and notebook exists, so update it
@@ -227,7 +231,8 @@ module Agents
         # evernote will create any new tags
         tags = getNoteTagNames(params[:guid])
         tags.each { |tag|
-          params[:tagNames] << tag unless params[:tagNames].include?(tag) }
+          params[:tagNames] << tag unless params[:tagNames].include?(tag)
+        }
 
         note = Evernote::EDAM::Type::Note.new(params)
         updateNote(note)
@@ -247,19 +252,19 @@ module Agents
       end
 
       def find_tags(guids)
-        listTags.select {|tag| guids.include?(tag.guid)}
+        listTags.select { |tag| guids.include?(tag.guid) }
       end
 
       def find_notebook(params)
         if params[:guid]
-          listNotebooks.detect {|notebook| notebook.guid == params[:guid]}
+          listNotebooks.detect { |notebook| notebook.guid == params[:guid] }
         elsif params[:name]
-          listNotebooks.detect {|notebook| notebook.name == params[:name]}
+          listNotebooks.detect { |notebook| notebook.name == params[:name] }
         end
       end
 
       def create_notebook(name)
-        notebook = Evernote::EDAM::Type::Notebook.new(name: name)
+        notebook = Evernote::EDAM::Type::Notebook.new(name:)
         createNotebook(notebook)
       end
 
@@ -270,7 +275,7 @@ module Agents
           params[:content] =
             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
             "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \
-            "<en-note>#{params[:content].encode(:xml => :text)}</en-note>"
+            "<en-note>#{params[:content].encode(xml: :text)}</en-note>"
         end
 
         params
@@ -278,6 +283,7 @@ module Agents
 
       class Search
         attr_reader :note_store, :opts
+
         def initialize(note_store, opts)
           @note_store = note_store
           @opts = opts
@@ -297,7 +303,7 @@ module Agents
             # and notes that recently had the specified tags added
             metadata.select! do |note_data|
               note_data.updated > opts[:last_checked_at] ||
-              !opts[:notes_with_tags].include?(note_data.guid)
+                !opts[:notes_with_tags].include?(note_data.guid)
             end
 
           elsif opts[:last_checked_at]
@@ -326,7 +332,8 @@ module Agents
         private
 
         def filtered_metadata
-          filter, spec = create_filter, create_spec
+          filter = create_filter
+          spec = create_spec
           metadata = note_store.findNotesMetadata(filter, 0, 100, spec).notes
         end
 
@@ -346,8 +353,9 @@ module Agents
     class Note
       attr_accessor :en_note
       attr_reader :notebook, :tags
+
       delegate :guid, :notebookGuid, :title, :tagGuids, :content, :resources,
-               :attributes, :to => :en_note
+               :attributes, to: :en_note
 
       def initialize(en_note, notebook, tags)
         @en_note = en_note
@@ -357,11 +365,11 @@ module Agents
 
       def attr(opts = {})
         return_attr = {
-          title:        title,
-          notebook:     notebook,
-          tags:         tags,
-          source:       attributes.source,
-          source_url:   attributes.sourceURL
+          title:,
+          notebook:,
+          tags:,
+          source: attributes.source,
+          source_url: attributes.sourceURL
         }
 
         return_attr[:content] = content if opts[:include_content]

+ 14 - 7
app/models/agents/ftpsite_agent.rb

@@ -11,7 +11,7 @@ module Agents
     emits_file_pointer!
 
     description do
-      <<-MD
+      <<~MD
         The Ftp Site Agent checks an FTP site and creates Events based on newly uploaded files in a directory. When receiving events it creates files on the configured FTP server.
 
         #{'## Include `net-ftp-list` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -40,7 +40,7 @@ module Agents
       MD
     end
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
 
           {
@@ -83,7 +83,7 @@ module Agents
           URI::FTP === uri or raise
           errors.add(:base, "url must end with a slash") if uri.path.present? && !uri.path.end_with?('/')
         end
-      rescue
+      rescue StandardError
         errors.add(:base, "url must be a valid FTP URL")
       end
 
@@ -118,18 +118,20 @@ module Agents
       if (timestamp = options['timestamp']).present?
         begin
           Time.parse(timestamp)
-        rescue
+        rescue StandardError
           errors.add(:base, "timestamp cannot be parsed as time")
         end
       end
 
       if options['expected_update_period_in_days'].present?
-        errors.add(:base, "Invalid expected_update_period_in_days format") unless is_positive_integer?(options['expected_update_period_in_days'])
+        errors.add(:base,
+                   "Invalid expected_update_period_in_days format") unless is_positive_integer?(options['expected_update_period_in_days'])
       end
     end
 
     def check
       return if interpolated['mode'] != 'read'
+
       saving_entries do |found|
         each_entry { |filename, mtime|
           found[filename, mtime]
@@ -139,9 +141,14 @@ module Agents
 
     def receive(incoming_events)
       return if interpolated['mode'] != 'write'
+
       incoming_events.each do |event|
         mo = interpolated(event)
-        mo['data'].encode!(interpolated['force_encoding'], invalid: :replace, undef: :replace) if interpolated['force_encoding'].present?
+        mo['data'].encode!(
+          interpolated['force_encoding'],
+          invalid: :replace,
+          undef: :replace
+        ) if interpolated['force_encoding'].present?
         open_ftp(base_uri) do |ftp|
           ftp.storbinary("STOR #{mo['filename']}", StringIO.new(mo['data']), Net::FTP::DEFAULT_BLOCKSIZE)
         end
@@ -168,7 +175,7 @@ module Agents
           entry = Net::FTP::List.parse line
           filename = entry.basename
           mtime = Time.parse(entry.mtime.to_s).utc
-          
+
           patterns.any? { |pattern|
             File.fnmatch?(pattern, filename)
           } or next

+ 6 - 4
app/models/agents/gap_detector_agent.rb

@@ -2,7 +2,7 @@ module Agents
   class GapDetectorAgent < Agent
     default_schedule "every_10m"
 
-    description <<-MD
+    description <<~MD
       The Gap Detector Agent will watch for holes or gaps in a stream of incoming Events and generate "no data alerts".
 
       The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to a value of interest. If either
@@ -10,7 +10,7 @@ module Agents
       a payload of `message`.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like:
 
           {
@@ -58,8 +58,10 @@ module Agents
       if memory['newest_event_created_at'].present? && Time.at(memory['newest_event_created_at']) < window
         unless memory['alerted_at']
           memory['alerted_at'] = Time.now.to_i
-          create_event payload: { message: interpolated['message'],
-                                  gap_started_at: memory['newest_event_created_at'] }
+          create_event payload: {
+            message: interpolated['message'],
+            gap_started_at: memory['newest_event_created_at']
+          }
         end
       end
     end

+ 18 - 17
app/models/agents/google_calendar_publish_agent.rb

@@ -8,7 +8,7 @@ module Agents
 
     gem_dependency_check { defined?(Google) && defined?(Google::Apis::CalendarV3) }
 
-    description <<-MD
+    description <<~MD
       The Google Calendar Publish Agent creates events on your Google Calendar.
 
       #{'## Include `google-api-client` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -73,19 +73,22 @@ module Agents
       }</code></pre>
     MD
 
-    event_description <<-MD
-      {
-        'success' => true,
-        'published_calendar_event' => {
-           ....
-        },
-        'agent_id' => 1234,
-        'event_id' => 3432
-      }
+    event_description <<~MD
+      Events look like:
+
+          {
+            'success' => true,
+            'published_calendar_event' => {
+               ....
+            },
+            'agent_id' => 1234,
+            'event_id' => 3432
+          }
     MD
 
     def validate_options
-      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
     end
 
     def working?
@@ -108,7 +111,6 @@ module Agents
       require 'google_calendar'
       incoming_events.each do |event|
         GoogleCalendar.open(interpolate_options(options, event), Rails.logger) do |calendar|
-
           cal_message = event.payload["message"]
           if cal_message["start"].present? && cal_message["start"]["dateTime"].present? && !cal_message["start"]["date_time"].present?
             cal_message["start"]["date_time"] = cal_message["start"].delete "dateTime"
@@ -118,11 +120,11 @@ module Agents
           end
 
           calendar_event = calendar.publish_as(
-                interpolated(event)['calendar_id'],
-                cal_message
-              )
+            interpolated(event)['calendar_id'],
+            cal_message
+          )
 
-          create_event :payload => {
+          create_event payload: {
             'success' => true,
             'published_calendar_event' => calendar_event,
             'agent_id' => event.agent_id,
@@ -133,4 +135,3 @@ module Agents
     end
   end
 end
-

+ 2 - 2
app/models/agents/google_translation_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     gem_dependency_check { defined?(Google) && defined?(Google::Cloud::Translate) }
 
-    description <<-MD
+    description <<~MD
       The Translation Agent will attempt to translate text between natural languages.
 
       #{'## Include `google-api-client` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -76,7 +76,7 @@ module Agents
     end
 
     def translate_service
-      @translate_service ||= google_client.discovered_api('translate','v2')
+      @translate_service ||= google_client.discovered_api('translate', 'v2')
     end
 
     def cloud_translate_service

+ 12 - 12
app/models/agents/growl_agent.rb

@@ -9,7 +9,7 @@ module Agents
 
     gem_dependency_check { defined?(Growl) }
 
-    description <<-MD
+    description <<~MD
       The Growl Agent sends any events it receives to a Growl GNTP server immediately.
 
       #{'## Include `ruby-growl` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -27,15 +27,15 @@ module Agents
 
     def default_options
       {
-          'growl_server' => 'localhost',
-          'growl_password' => '',
-          'growl_app_name' => 'HuginnGrowl',
-          'growl_notification_name' => 'Notification',
-          'expected_receive_period_in_days' => "2",
-          'subject' => '{{subject}}',
-          'message' => '{{message}}',
-          'sticky' => 'false',
-          'priority' => '0'
+        'growl_server' => 'localhost',
+        'growl_password' => '',
+        'growl_app_name' => 'HuginnGrowl',
+        'growl_notification_name' => 'Notification',
+        'expected_receive_period_in_days' => "2",
+        'subject' => '{{subject}}',
+        'message' => '{{message}}',
+        'sticky' => 'false',
+        'priority' => '0'
       }
     end
 
@@ -78,8 +78,8 @@ module Agents
           subject = interpolated[:subject]
           if message.present? && subject.present?
             log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event)['growl_server']} with event #{event.id}"
-            notify_growl(subject: subject,
-                         message: message,
+            notify_growl(subject:,
+                         message:,
                          priority: interpolated[:priority].to_i,
                          sticky: boolify(interpolated[:sticky]) || false,
                          callback_url: interpolated[:callback_url].presence)

+ 15 - 10
app/models/agents/hipchat_agent.rb

@@ -8,7 +8,7 @@ module Agents
 
     gem_dependency_check { defined?(HipChat) }
 
-    description <<-MD
+    description <<~MD
       The Hipchat Agent sends messages to a Hipchat Room
 
       #{'## Include `hipchat` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -52,16 +52,18 @@ module Agents
       client.rooms
       true
     rescue HipChat::UnknownResponseCode
-      return false
+      false
     end
 
     def complete_room_name
-      client.rooms.collect { |room| {text: room.name, id: room.name} }
+      client.rooms.collect { |room| { text: room.name, id: room.name } }
     end
 
     def validate_options
-      errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present?
-      errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank?
+      errors.add(:base,
+                 "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present?
+      errors.add(:base,
+                 "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank?
     end
 
     def working?
@@ -71,15 +73,18 @@ module Agents
     def receive(incoming_events)
       incoming_events.each do |event|
         mo = interpolated(event)
-        client[mo[:room_name]].send(mo[:username][0..14], mo[:message],
-                                      notify: boolify(mo[:notify]),
-                                      color: mo[:color],
-                                      message_format: mo[:format].presence || 'html'
-                                    )
+        client[mo[:room_name]].send(
+          mo[:username][0..14],
+          mo[:message],
+          notify: boolify(mo[:notify]),
+          color: mo[:color],
+          message_format: mo[:format].presence || 'html'
+        )
       end
     end
 
     private
+
     def client
       @client ||= HipChat::Client.new(interpolated[:auth_token].presence || credential('hipchat_auth_token'))
     end

+ 10 - 11
app/models/agents/http_status_agent.rb

@@ -1,9 +1,7 @@
 require 'time_tracker'
 
 module Agents
-
   class HttpStatusAgent < Agent
-
     include WebRequestConcern
     include FormConfigurable
 
@@ -17,7 +15,7 @@ module Agents
     form_configurable :changes_only, type: :boolean
     form_configurable :headers_to_save
 
-    description <<-MD
+    description <<~MD
       The HttpStatusAgent will check a url and emit the resulting HTTP status code with the time that it waited for a reply. Additionally, it will optionally emit the value of one or more specified headers.
 
       Specify a `Url` and the Http Status Agent will produce an event with the HTTP status code. If you specify one or more `Headers to save` (comma-delimited) as well, that header or headers' value(s) will be included in the event.
@@ -27,7 +25,7 @@ module Agents
       The `changes only` option causes the Agent to report an event only when the status changes. If set to false, an event will be created for every check.  If set to true, an event will only be created when the status changes (like if your site goes from 200 to 500).
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events will have the following fields:
 
           {
@@ -86,27 +84,28 @@ module Agents
       # Deal with failures
       if measured_result.result
         final_url = boolify(interpolated['disable_redirect_follow']) ? url : measured_result.result.env.url.to_s
-        payload.merge!({ 'final_url' => final_url, 'redirected' => (url != final_url), 'response_received' => true, 'status' => current_status })
+        payload.merge!({ 'final_url' => final_url, 'redirected' => (url != final_url), 'response_received' => true,
+                         'status' => current_status })
         # Deal with headers
         if local_headers.present?
-          header_results = local_headers.each_with_object({}) { |header, hash| hash[header] = measured_result.result.headers[header] }
+          header_results = local_headers.each_with_object({}) { |header, hash|
+            hash[header] = measured_result.result.headers[header]
+          }
           payload.merge!({ 'headers' => header_results })
         end
-        create_event payload: payload
+        create_event(payload:)
         memory['last_status'] = measured_result.status.to_s
       else
-        create_event payload: payload
+        create_event(payload:)
         memory['last_status'] = nil
       end
-
     end
 
     def ping(url)
       result = faraday.get url
       result.status > 0 ? result : nil
-    rescue
+    rescue StandardError
       nil
     end
   end
-
 end

+ 158 - 119
app/models/agents/human_task_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     gem_dependency_check { defined?(RTurk) }
 
-    description <<-MD
+    description <<~MD
       The Human Task Agent is used to create Human Intelligence Tasks (HITs) on Mechanical Turk.
 
       #{'## Include `rturk` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -118,7 +118,7 @@ module Agents
       As with most Agents, `expected_receive_period_in_days` is required if `trigger_on` is set to `event`.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like:
 
           {
@@ -135,24 +135,45 @@ module Agents
       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, "'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, "'hit.questions' must be provided") unless options['hit']['questions'].present?
 
       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?
+        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
+        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| %w[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].blank? }
+         }
         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? })}
-        errors.add(:base, "all questions of type 'selection' must have a selections array with selections that set 'key' and 'name'")
+      if options['hit']['questions'].any? { |question|
+           question['type'] == "selection" && (
+             question['selections'].blank? ||
+               question['selections'].any? { |s| s['key'].blank? || s['text'].blank? }
+           )
+         }
+        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" }
@@ -160,7 +181,14 @@ module Agents
       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
 
@@ -229,7 +257,6 @@ module Agents
     protected
 
     if defined?(RTurk)
-
       def take_majority?
         interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true"
       end
@@ -266,139 +293,151 @@ module Agents
           assignments = hit.assignments
 
           log "Looking at HIT #{hit_id}.  I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}"
-          if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" }
-            inbound_event = event_for_hit(hit_id)
+          next unless assignments.length == hit.max_assignments &&
+            assignments.all? { |assignment|
+              assignment.status == "Submitted"
+            }
 
-            if hit_type(hit_id) == 'poll'
-              # handle completed polls
+          inbound_event = event_for_hit(hit_id)
 
-              log "Handling a poll: #{hit_id}"
+          if hit_type(hit_id) == 'poll'
+            # handle completed polls
 
-              scores = {}
-              assignments.each do |assignment|
-                assignment.answers.each do |index, rating|
-                  scores[index] ||= 0
-                  scores[index] += rating.to_i
-                end
+            log "Handling a poll: #{hit_id}"
+
+            scores = {}
+            assignments.each do |assignment|
+              assignment.answers.each do |index, rating|
+                scores[index] ||= 0
+                scores[index] += rating.to_i
               end
+            end
 
-              top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first
+            top_answer = scores.to_a.sort { |b, a| a.last <=> b.last }.first.first
 
-              payload = {
-                'answers' => memory['hits'][hit_id]['answers'],
-                'poll' => assignments.map(&:answers),
-                'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1]
-              }
+            payload = {
+              '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) }
-
-              if take_majority?
-                counts = {}
-                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']]
-                    question_counts[answer] += 1
-                  end
-                  counts[question['key']] = question_counts
+            event = create_event(payload:)
+            log("Event emitted with answer(s) for poll", outbound_event: event, inbound_event:)
+          else
+            # handle normal completed HITs
+            payload = { 'answers' => assignments.map(&:answers) }
+
+            if take_majority?
+              counts = {}
+              options['hit']['questions'].each do |question|
+                question_counts = question['selections'].each_with_object({}) { |selection, memo|
+                  memo[selection['key']] = 0
+                }
+                assignments.each do |assignment|
+                  answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers)
+                  answer = answers[question['key']]
+                  question_counts[answer] += 1
                 end
-                payload['counts'] = counts
+                counts[question['key']] = question_counts
+              end
+              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
-
-                if all_questions_are_numeric?
-                  average_answer = counts.inject({}) do |memo, (key, question_counts)|
-                    sum = divisor = 0
-                    question_counts.to_a.each do |num, count|
-                      sum += num.to_s.to_f * count
-                      divisor += count
-                    end
-                    memo[key] = sum / divisor.to_f
-                    memo
+              majority_answer = counts.each_with_object({}) do |(key, question_counts), memo|
+                memo[key] = question_counts.to_a.sort_by(&:last).last.first
+              end
+              payload['majority_answer'] = majority_answer
+
+              if all_questions_are_numeric?
+                average_answer = counts.each_with_object({}) do |(key, question_counts), memo|
+                  sum = divisor = 0
+                  question_counts.to_a.each do |num, count|
+                    sum += num.to_s.to_f * count
+                    divisor += count
                   end
-                  payload['average_answer'] = average_answer
+                  memo[key] = sum / divisor.to_f
                 end
+                payload['average_answer'] = average_answer
               end
+            end
 
-              if create_poll?
-                questions = []
-                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' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers),
-                    'selections' => selections
-                  }
-                end
+            if create_poll?
+              questions = []
+              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' => interpolate_string(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 }
-
-                log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}.  Original HIT: #{hit_id}", :inbound_event => inbound_event
-              else
-                if options[:separate_answers]
-                  payload['answers'].each.with_index do |answer, index|
-                    sub_payload = payload.dup
-                    sub_payload.delete('answers')
-                    sub_payload['answer'] = answer
-                    event = create_event :payload => sub_payload
-                    log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event
-                  end
-                else
-                  event = create_event :payload => payload
-                  log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event
-                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
+                }
+              )
+
+              log(
+                "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}.  Original HIT: #{hit_id}",
+                inbound_event:
+              )
+            elsif options[:separate_answers]
+              payload['answers'].each.with_index do |answer, index|
+                sub_payload = payload.dup
+                sub_payload.delete('answers')
+                sub_payload['answer'] = answer
+                event = create_event payload: sub_payload
+                log("Event emitted with answer ##{index}", outbound_event: event, inbound_event:)
               end
+            else
+              event = create_event(payload:)
+              log("Event emitted with answer(s)", outbound_event: event, inbound_event:)
             end
+          end
 
-            assignments.each(&:approve!)
-            hit.dispose!
+          assignments.each(&:approve!)
+          hit.dispose!
 
-            memory['hits'].delete(hit_id)
-          end
+          memory['hits'].delete(hit_id)
         end
       end
 
       def all_questions_are_numeric?
         interpolated['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
+            value = selection['key']
+            value == value.to_f.to_s || value == value.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 }
-
-        log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event
+        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 = {})
@@ -406,13 +445,13 @@ module Agents
         title = interpolate_string(opts['title'], payload).strip
         description = interpolate_string(opts['description'], payload).strip
         questions = interpolate_options(opts['questions'], payload)
-        hit = RTurk::Hit.create(title: title) do |hit|
+        hit = RTurk::Hit.create(title:) do |hit|
           hit.max_assignments = (opts['assignments'] || 1).to_i
           hit.description = description
           hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i
-          hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions)
+          hit.question_form AgentQuestionForm.new(title:, description:, questions:)
           hit.reward = (opts['reward'] || 0.05).to_f
-          #hit.qualifications.add :approval_rate, { :gt => 80 }
+          # hit.qualifications.add :approval_rate, { gt: 80 }
         end
         memory['hits'] ||= {}
         memory['hits'][hit.id] = opts['metadata'] || {}

+ 38 - 31
app/models/agents/imap_folder_agent.rb

@@ -14,7 +14,7 @@ module Agents
 
     default_schedule "every_30m"
 
-    description <<-MD
+    description <<~MD
       The Imap Folder Agent checks an IMAP server in specified folders and creates Events based on new mails found since the last run. In the first visit to a folder, this agent only checks for the initial status and does not create events.
 
       Specify an IMAP server to connect with `host`, and set `ssl` to true if the server supports IMAP over SSL.  Specify `port` if you need to connect to a port other than standard (143 or 993 depending on the `ssl` value), and specify login credentials in `username` and `password`.
@@ -50,7 +50,7 @@ module Agents
           If this key is unspecified or set to null, it is ignored.
 
       - `has_attachment`
-      
+
           Setting this to true or false means only mails that does or does not have an attachment are selected.
 
           If this key is unspecified or set to null, it is ignored.
@@ -73,7 +73,7 @@ module Agents
       Also, in order to avoid duplicated notification it keeps a list of Message-Id's of 100 most recent mails, so if multiple mails of the same Message-Id are found, you will only see one event out of them.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
 
           {
@@ -132,10 +132,8 @@ module Agents
       end
 
       %w[ssl mark_as_read delete include_raw_mail].each { |key|
-        if options[key].present?
-          if boolify(options[key]).nil?
-            errors.add(:base, '%s must be a boolean value' % key)
-          end
+        if options[key].present? && boolify(options[key]).nil?
+          errors.add(:base, '%s must be a boolean value' % key)
         end
       }
 
@@ -175,7 +173,7 @@ module Agents
             when String
               begin
                 Regexp.new(value)
-              rescue
+              rescue StandardError
                 errors.add(:base, 'conditions.%s contains an invalid regexp' % key)
               end
             else
@@ -187,7 +185,7 @@ module Agents
               when String
                 begin
                   glob_match?(pattern, '')
-                rescue
+                rescue StandardError
                   errors.add(:base, 'conditions.%s contains an invalid glob pattern' % key)
                 end
               else
@@ -207,7 +205,10 @@ module Agents
       end
 
       if options['expected_update_period_in_days'].present?
-        errors.add(:base, "Invalid expected_update_period_in_days format") unless is_positive_integer?(options['expected_update_period_in_days'])
+        errors.add(
+          :base,
+          "Invalid expected_update_period_in_days format"
+        ) unless is_positive_integer?(options['expected_update_period_in_days'])
       end
     end
 
@@ -240,14 +241,14 @@ module Agents
             value.present? or next true
             re = Regexp.new(value)
             matched_part = body_parts.find { |part|
-               if m = re.match(part.scrubbed(:decoded))
-                 m.names.each { |name|
-                   matches[name] = m[name]
-                 }
-                 true
-               else
-                 false
-               end
+              if m = re.match(part.scrubbed(:decoded))
+                m.names.each { |name|
+                  matches[name] = m[name]
+                }
+                true
+              else
+                false
+              end
             }
           when 'from', 'to', 'cc'
             value.present? or next true
@@ -266,7 +267,7 @@ module Agents
           when 'has_attachment'
             boolify(value) == mail.has_attachment?
           when 'is_unread'
-            true  # already filtered out by each_unread_mail
+            true # already filtered out by each_unread_mail
           else
             log 'Unknown condition key ignored: %s' % key
             true
@@ -295,7 +296,12 @@ module Agents
             'from' => mail.from_addrs.first,
             'to' => mail.to_addrs,
             'cc' => mail.cc_addrs,
-            'date' => (mail.date.iso8601 rescue nil),
+            'date' =>
+              begin
+                mail.date.iso8601
+              rescue StandardError
+                nil
+              end,
             'mime_type' => mime_type,
             'body' => body,
             'matches' => matches,
@@ -309,12 +315,12 @@ module Agents
           if interpolated['event_headers'].present?
             headers = mail.header.each_with_object({}) { |field, hash|
               name = field.name
-              hash[name] = (v = hash[name]) ? "#{v}\n#{field.value.to_s}" : field.value.to_s
+              hash[name] = (v = hash[name]) ? "#{v}\n#{field.value}" : field.value.to_s
             }
             payload.update(event_headers_payload(headers))
           end
 
-          create_event payload: payload
+          create_event(payload:)
 
           notified << mail.message_id if mail.message_id
         end
@@ -346,7 +352,7 @@ module Agents
       port = (Integer(port) if port.present?)
 
       log "Connecting to #{host}#{':%d' % port if port}#{' via SSL' if ssl}"
-      Client.open(host, port: port, ssl: ssl) { |imap|
+      Client.open(host, port:, ssl:) { |imap|
         log "Logging in as #{username}"
         if service
           imap.authenticate('XOAUTH2', username, password)
@@ -355,7 +361,8 @@ module Agents
         end
 
         # 'lastseen' keeps a hash of { uidvalidity => lastseenuid, ... }
-        lastseen, seen = self.lastseen, self.make_seen
+        lastseen = self.lastseen
+        seen = self.make_seen
 
         # 'notified' keeps an array of message-ids of {IDCACHE_SIZE}
         # most recent notified mails.
@@ -384,8 +391,8 @@ module Agents
           seen[uidvalidity] = lastseenuid
           is_unread = boolify(interpolated['conditions']['is_unread'])
 
-          uids = imap.uid_fetch((lastseenuid + 1)..-1, 'FLAGS').
-                 each_with_object([]) { |data, ret|
+          uids = imap.uid_fetch((lastseenuid + 1)..-1, 'FLAGS')
+            .each_with_object([]) { |data, ret|
             uid, flags = data.attr.values_at('UID', 'FLAGS')
             seen[uidvalidity] = uid
             next if uid <= lastseenuid
@@ -430,8 +437,8 @@ module Agents
       Seen.new(memory['lastseen'])
     end
 
-    def lastseen= value
-      memory.delete('seen')  # obsolete key
+    def lastseen=(value)
+      memory.delete('seen') # obsolete key
       memory['lastseen'] = value
     end
 
@@ -443,7 +450,7 @@ module Agents
       Notified.new(memory['notified'])
     end
 
-    def notified= value
+    def notified=(value)
       memory['notified'] = value
     end
 
@@ -540,7 +547,7 @@ module Agents
       module Scrubbed
         def scrubbed(method)
           (@scrubbed ||= {})[method.to_sym] ||=
-            __send__(method).try(:scrub) { |bytes| "<#{bytes.unpack('H*')[0]}>" }
+            __send__(method).try(:scrub) { |bytes| "<#{bytes.unpack1('H*')}>" }
         end
       end
 
@@ -588,7 +595,7 @@ module Agents
           [mail]
         end.select { |part|
           if part.multipart? || part.attachment? || !part.text? ||
-             !mime_types.include?(part.mime_type)
+              !mime_types.include?(part.mime_type)
             false
           else
             part.extend(Scrubbed)

+ 11 - 10
app/models/agents/jabber_agent.rb

@@ -7,7 +7,7 @@ module Agents
 
     gem_dependency_check { defined?(Jabber) }
 
-    description <<-MD
+    description <<~MD
       The Jabber Agent will send any events it receives to your Jabber/XMPP IM account.
 
       #{'## Include `xmpp4r` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -23,7 +23,7 @@ module Agents
       Have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       `event` will be set to either `on_join`, `on_leave`, `on_message`, `on_room_message` or `on_subject`
 
           {
@@ -36,12 +36,12 @@ module Agents
 
     def default_options
       {
-        'jabber_server'   => '127.0.0.1',
-        'jabber_port'     => '5222',
-        'jabber_sender'   => 'huginn@localhost',
+        'jabber_server' => '127.0.0.1',
+        'jabber_port' => '5222',
+        'jabber_sender' => 'huginn@localhost',
         'jabber_receiver' => 'muninn@localhost',
         'jabber_password' => '',
-        'message'         => 'It will be {{temp}} out tomorrow',
+        'message' => 'It will be {{temp}} out tomorrow',
         'expected_receive_period_in_days' => "2"
       }
     end
@@ -71,7 +71,7 @@ module Agents
     end
 
     def deliver(text)
-      client.send Jabber::Message::new(interpolated['jabber_receiver'], text).set_type(:chat)
+      client.send Jabber::Message.new(interpolated['jabber_receiver'], text).set_type(:chat)
     end
 
     def start_worker?
@@ -81,7 +81,7 @@ module Agents
     private
 
     def client
-      Jabber::Client.new(Jabber::JID::new(interpolated['jabber_sender'])).tap do |sender|
+      Jabber::Client.new(Jabber::JID.new(interpolated['jabber_sender'])).tap do |sender|
         sender.connect(interpolated['jabber_server'], interpolated['jabber_port'] || '5222')
         sender.auth interpolated['jabber_password']
       end
@@ -96,7 +96,7 @@ module Agents
     end
 
     class Worker < LongRunnable::Worker
-      IGNORE_MESSAGES_FOR=5
+      IGNORE_MESSAGES_FOR = 5
 
       def setup
         require 'xmpp4r/muc/helper/simplemucclient'
@@ -124,7 +124,7 @@ module Agents
         time, nick, message = normalize_args(event, args)
 
         AgentRunner.with_connection do
-          agent.create_event(payload: {event: event, time: time, nick: nick, message: message})
+          agent.create_event(payload: { event:, time:, nick:, message: })
         end
       end
 
@@ -139,6 +139,7 @@ module Agents
       end
 
       private
+
       def normalize_args(event, args)
         case event
         when :on_join, :on_leave

+ 24 - 24
app/models/agents/java_script_agent.rb

@@ -11,7 +11,7 @@ module Agents
 
     gem_dependency_check { defined?(MiniRacer) }
 
-    description <<-MD
+    description <<~MD
       The JavaScript Agent allows you to write code in JavaScript that can create and receive events.  If other Agents aren't meeting your needs, try this one!
 
       #{'## Include `mini_racer` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -45,7 +45,8 @@ module Agents
     def validate_options
       cred_name = credential_referenced_by_code
       if cred_name
-        errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
+        errors.add(:base,
+                   "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
       else
         errors.add(:base, "The 'code' option is required") unless options['code'].present?
       end
@@ -114,22 +115,22 @@ module Agents
       context = MiniRacer::Context.new
       context.eval(setup_javascript)
 
-      context.attach("doCreateEvent", -> (y) { create_event(payload: clean_nans(JSON.parse(y))).payload.to_json })
+      context.attach("doCreateEvent", ->(y) { create_event(payload: clean_nans(JSON.parse(y))).payload.to_json })
       context.attach("getIncomingEvents", -> { incoming_events.to_json })
       context.attach("getOptions", -> { interpolated.to_json })
-      context.attach("doLog", -> (x) { log x })
-      context.attach("doError", -> (x) { error x })
+      context.attach("doLog", ->(x) { log x })
+      context.attach("doError", ->(x) { error x })
       context.attach("getMemory", -> { memory.to_json })
-      context.attach("setMemoryKey", -> (x, y) { memory[x] = clean_nans(y) })
-      context.attach("setMemory", -> (x) { memory.replace(clean_nans(x)) })
-      context.attach("deleteKey", -> (x) { memory.delete(x).to_json })
-      context.attach("escapeHtml", -> (x) { CGI.escapeHTML(x) })
-      context.attach("unescapeHtml", -> (x) { CGI.unescapeHTML(x) })
-      context.attach('getCredential', -> (k) { credential(k); })
-      context.attach('setCredential', -> (k, v) { set_credential(k, v) })
+      context.attach("setMemoryKey", ->(x, y) { memory[x] = clean_nans(y) })
+      context.attach("setMemory", ->(x) { memory.replace(clean_nans(x)) })
+      context.attach("deleteKey", ->(x) { memory.delete(x).to_json })
+      context.attach("escapeHtml", ->(x) { CGI.escapeHTML(x) })
+      context.attach("unescapeHtml", ->(x) { CGI.unescapeHTML(x) })
+      context.attach('getCredential', ->(k) { credential(k); })
+      context.attach('setCredential', ->(k, v) { set_credential(k, v) })
 
       if (options['language'] || '').downcase == 'coffeescript'
-        context.eval(CoffeeScript.compile code)
+        context.eval(CoffeeScript.compile(code))
       else
         context.eval(code)
       end
@@ -223,20 +224,19 @@ module Agents
     end
 
     def log_errors
-      begin
-        yield
-      rescue MiniRacer::Error => e
-        error "JavaScript error: #{e.message}"
-      end
+      yield
+    rescue MiniRacer::Error => e
+      error "JavaScript error: #{e.message}"
     end
 
     def clean_nans(input)
-      if input.is_a?(Array)
-        input.map {|v| clean_nans(v) }
-      elsif input.is_a?(Hash)
-        input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
-      elsif input.is_a?(Float) && input.nan?
-        'NaN'
+      case input
+      when Array
+        input.map { |v| clean_nans(v) }
+      when Hash
+        input.transform_values { |v| clean_nans(v) }
+      when Float
+        input.nan? ? 'NaN' : input
       else
         input
       end

+ 47 - 38
app/models/agents/jira_agent.rb

@@ -10,11 +10,11 @@ module Agents
 
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Jira Agent subscribes to Jira issue updates.
 
       - `jira_url` specifies the full URL of the jira installation, including https://
-      - `jql` is an optional Jira Query Language-based filter to limit the flow of events. See [JQL Docs](https://confluence.atlassian.com/display/JIRA/Advanced+Searching) for details. 
+      - `jql` is an optional Jira Query Language-based filter to limit the flow of events. See [JQL Docs](https://confluence.atlassian.com/display/JIRA/Advanced+Searching) for details.#{' '}
       - `username` and `password` are optional, and may need to be specified if your Jira instance is read-protected
       - `timeout` is an optional parameter that specifies how long the request processing may take in minutes.
 
@@ -23,18 +23,18 @@ module Agents
       NOTE: upon the first execution, the agent will fetch everything available by the JQL query. So if it's not desirable, limit the `jql` query by date.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events are the raw JSON generated by Jira REST API
 
-      {
-        "expand": "editmeta,renderedFields,transitions,changelog,operations",
-        "id": "80127",
-        "self": "https://jira.atlassian.com/rest/api/2/issue/80127",
-        "key": "BAM-3512",
-        "fields": {
-          ...
-        }
-      }
+          {
+            "expand": "editmeta,renderedFields,transitions,changelog,operations",
+            "id": "80127",
+            "self": "https://jira.atlassian.com/rest/api/2/issue/80127",
+            "key": "BAM-3512",
+            "fields": {
+              ...
+            }
+          }
     MD
 
     default_schedule "every_10m"
@@ -42,7 +42,7 @@ module Agents
 
     def default_options
       {
-        'username'  => '',
+        'username' => '',
         'password' => '',
         'jira_url' => 'https://jira.atlassian.com',
         'jql' => '',
@@ -52,9 +52,11 @@ module Agents
     end
 
     def validate_options
-      errors.add(:base, "you need to specify password if user name is set") if options['username'].present? and not options['password'].present?
+      errors.add(:base,
+                 "you need to specify password if user name is set") if options['username'].present? and !options['password'].present?
       errors.add(:base, "you need to specify your jira URL") unless options['jira_url'].present?
-      errors.add(:base, "you need to specify the expected update period") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "you need to specify the expected update period") unless options['expected_update_period_in_days'].present?
       errors.add(:base, "you need to specify request timeout") unless options['timeout'].present?
     end
 
@@ -74,41 +76,50 @@ module Agents
 
         # this check is more precise than in get_issues()
         # see get_issues() for explanation
-        if not last_run or updated > last_run
-          create_event :payload => issue
+        if !last_run or updated > last_run
+          create_event payload: issue
         end
       end
 
       memory[:last_run] = current_run
     end
 
-  private
+    private
+
     def request_url(jql, start_at)
-      "#{interpolated[:jira_url]}/rest/api/2/search?jql=#{CGI::escape(jql)}&fields=*all&startAt=#{start_at}"
+      "#{interpolated[:jira_url]}/rest/api/2/search?jql=#{CGI.escape(jql)}&fields=*all&startAt=#{start_at}"
     end
 
     def request_options
-      ropts = { headers: {"User-Agent" => user_agent} }
+      ropts = { headers: { "User-Agent" => user_agent } }
 
       if !interpolated[:username].empty?
-        ropts = ropts.merge({:basic_auth => {:username =>interpolated[:username], :password=>interpolated[:password]}})
+        ropts = ropts.merge({
+          basic_auth: {
+            username: interpolated[:username],
+            password: interpolated[:password]
+          }
+        })
       end
 
       ropts
     end
 
     def get(url, options)
-        response = HTTParty.get(url, options)
-
-        if response.code == 400
-          raise RuntimeError.new("Jira error: #{response['errorMessages']}") 
-        elsif response.code == 403
-          raise RuntimeError.new("Authentication failed: Forbidden (403)")
-        elsif response.code != 200
-          raise RuntimeError.new("Request failed: #{response}")
-        end
+      response = HTTParty.get(url, options)
+
+      case response.code
+      when 200
+        # OK
+      when 400
+        raise "Jira error: #{response['errorMessages']}"
+      when 403
+        raise "Authentication failed: Forbidden (403)"
+      else
+        raise "Request failed: #{response}"
+      end
 
-        response
+      response
     end
 
     def get_issues(since)
@@ -120,7 +131,7 @@ module Agents
       # earlier and filter out unnecessary ones at a later
       # stage. Fortunately, the 'updated' field has GMT
       # offset
-      since -= 24*60*60 if since
+      since -= 24 * 60 * 60 if since
 
       jql = ""
 
@@ -138,26 +149,24 @@ module Agents
         response = get(request_url(jql, startAt), request_options)
 
         if response['issues'].length == 0
-          request_limit+=1
+          request_limit += 1
         end
 
         if request_limit > MAX_EMPTY_REQUESTS
-          raise RuntimeError.new("There is no progress while fetching issues")
+          raise "There is no progress while fetching issues"
         end
 
         if Time.now > start_time + interpolated['timeout'].to_i * 60
-          raise RuntimeError.new("Timeout exceeded while fetching issues")
+          raise "Timeout exceeded while fetching issues"
         end
 
         issues += response['issues']
         startAt += response['issues'].length
- 
+
         break if startAt >= response['total']
       end
 
       issues
     end
-
   end
 end
-

+ 4 - 3
app/models/agents/jq_agent.rb

@@ -29,7 +29,7 @@ module Agents
 
     gem_dependency_check { jq_version }
 
-    description <<-MD
+    description <<~MD
       The Jq Agent allows you to process incoming Events with [jq](https://stedolan.github.io/jq/) the JSON processor. (#{jq_info})
 
       It allows you to filter, transform and restructure Events in the way you want using jq's powerful features.
@@ -97,7 +97,8 @@ module Agents
 
     def validate_options
       errors.add(:base, "filter needs to be present.") if !options['filter'].is_a?(String)
-      errors.add(:base, "variables must be a hash if present.") if options.key?('variables') && !options['variables'].is_a?(Hash)
+      errors.add(:base,
+                 "variables must be a hash if present.") if options.key?('variables') && !options['variables'].is_a?(Hash)
     end
 
     def default_options
@@ -196,7 +197,7 @@ module Agents
           log "Creating #{results.size} events"
 
           results.each do |payload|
-            create_event payload: payload
+            create_event(payload:)
           end
         end
       end

+ 8 - 10
app/models/agents/json_parse_agent.rb

@@ -5,7 +5,7 @@ module Agents
     cannot_be_scheduled!
     can_dry_run!
 
-    description <<-MD
+    description <<~MD
       The JSON Parse Agent parses a JSON string and emits the data in a new event or merge with with the original event.
 
       `data` is the JSON to parse. Use [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) templating to specify the JSON string.
@@ -24,7 +24,7 @@ module Agents
     end
 
     event_description do
-      "Events will looks like this:\n\n    %s" % Utils.pretty_print(interpolated['data_key'] => {parsed: 'object'})
+      "Events will looks like this:\n\n    %s" % Utils.pretty_print(interpolated['data_key'] => { parsed: 'object' })
     end
 
     form_configurable :data
@@ -34,7 +34,7 @@ module Agents
     def validate_options
       errors.add(:base, "data needs to be present") if options['data'].blank?
       errors.add(:base, "data_key needs to be present") if options['data_key'].blank?
-      if options['mode'].present? && !options['mode'].to_s.include?('{{') && !%[clean merge].include?(options['mode'].to_s)
+      if options['mode'].present? && !options['mode'].to_s.include?('{{') && !%(clean merge).include?(options['mode'].to_s)
         errors.add(:base, "mode must be 'clean' or 'merge'")
       end
     end
@@ -45,13 +45,11 @@ module Agents
 
     def receive(incoming_events)
       incoming_events.each do |event|
-        begin
-          mo = interpolated(event)
-          existing_payload = mo['mode'].to_s == 'merge' ? event.payload : {}
-          create_event payload: existing_payload.merge({ mo['data_key'] => JSON.parse(mo['data']) })
-        rescue JSON::JSONError => e
-          error("Could not parse JSON: #{e.class} '#{e.message}'")
-        end
+        mo = interpolated(event)
+        existing_payload = mo['mode'].to_s == 'merge' ? event.payload : {}
+        create_event payload: existing_payload.merge({ mo['data_key'] => JSON.parse(mo['data']) })
+      rescue JSON::JSONError => e
+        error("Could not parse JSON: #{e.class} '#{e.message}'")
       end
     end
   end

+ 1 - 1
app/models/agents/key_value_store_agent.rb

@@ -6,7 +6,7 @@ module Agents
     cannot_be_scheduled!
     cannot_create_events!
 
-    description <<-MD
+    description <<~MD
       The Key-Value Store Agent is a data storage that keeps an associative array in its memory.  It receives events to store values and provides the data to other agents as an object via Liquid Templating.
 
       Liquid templates specified in the `key` and `value` options are evaluated for each received event to be stored in the memory.

+ 91 - 69
app/models/agents/liquid_output_agent.rb

@@ -8,24 +8,24 @@ module Agents
     DATE_UNITS = %w[second seconds minute minutes hour hours day days week weeks month months year years]
 
     description  do
-      <<-MD
-        The Liquid Output Agent outputs events through a Liquid template you provide.  Use it to create a HTML page, or a json feed, or anything else that can be rendered as a string from your stream of Huginn data. 
+      <<~MD
+        The Liquid Output Agent outputs events through a Liquid template you provide.  Use it to create a HTML page, or a json feed, or anything else that can be rendered as a string from your stream of Huginn data.
 
         This Agent will output data at:
 
-        `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id: user_id, secret: ':secret', format: :any_extension)}`
+        `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id:, secret: ':secret', format: :any_extension)}`
 
         where `:secret` is the secret specified in your options.  You can use any extension you wish.
 
         Options:
 
-          * `secret` - A token that the requestor must provide for light-weight authentication.
-          * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
-          * `content` - The content to display when someone requests this page.
-          * `mime_type` - The mime type to use when someone requests this page.
-          * `response_headers` - An object with any custom response headers. (example: `{"Access-Control-Allow-Origin": "*"}`)
-          * `mode` - The behavior that determines what data is passed to the Liquid template.
-          * `event_limit` - A limit applied to the events passed to a template when in "Last X events" mode. Can be a count like "1", or an amount of time like "1 day" or "5 minutes".
+        * `secret` - A token that the requestor must provide for light-weight authentication.
+        * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
+        * `content` - The content to display when someone requests this page.
+        * `mime_type` - The mime type to use when someone requests this page.
+        * `response_headers` - An object with any custom response headers. (example: `{"Access-Control-Allow-Origin": "*"}`)
+        * `mode` - The behavior that determines what data is passed to the Liquid template.
+        * `event_limit` - A limit applied to the events passed to a template when in "Last X events" mode. Can be a count like "1", or an amount of time like "1 day" or "5 minutes".
 
         # Liquid Templating
 
@@ -35,61 +35,56 @@ module Agents
 
         ### Merge events
 
-          The data for incoming events will be merged. So if two events come in like this:
+        The data for incoming events will be merged. So if two events come in like this:
 
-```
-{ 'a' => 'b',  'c' => 'd'}
-{ 'a' => 'bb', 'e' => 'f'}
-```
+        ```
+        { 'a' => 'b',  'c' => 'd'}
+        { 'a' => 'bb', 'e' => 'f'}
+        ```
 
-          The final result will be:
+        The final result will be:
 
-```
-{ 'a' => 'bb', 'c' => 'd', 'e' => 'f'}
-```
+        ```
+        { 'a' => 'bb', 'c' => 'd', 'e' => 'f'}
+        ```
 
         This merged version will be passed to the Liquid template.
 
         ### Last event in
 
-          The data from the last event will be passed to the template.
+        The data from the last event will be passed to the template.
 
         ### Last X events
 
-          All of the events received by this agent will be passed to the template
-          as the ```events``` array.
+        All of the events received by this agent will be passed to the template as the `events` array.
 
-          The number of events can be controlled via the ```event_limit``` option.
-          If ```event_limit``` is an integer X, the last X events will be passed
-          to the template.  If ```event_limit``` is an integer with a unit of
-          measure like "1 day" or "5 minutes" or "9 years", a date filter will
-          be applied to the events passed to the template.  If no ```event_limit```
-          is provided, then all of the events for the agent will be passed to
-          the template. 
-          
-          For performance, the maximum ```event_limit``` allowed is 1000.
+        The number of events can be controlled via the `event_limit` option.
+        If `event_limit` is an integer X, the last X events will be passed to the template.
+        If `event_limit` is an integer with a unit of measure like "1 day" or "5 minutes" or "9 years", a date filter will be applied to the events passed to the template.
+        If no `event_limit` is provided, then all of the events for the agent will be passed to the template.
 
+        For performance, the maximum `event_limit` allowed is 1000.
       MD
     end
 
     def default_options
-      content = <<EOF
-When you use the "Last event in" or "Merge events" option, you can use variables from the last event received, like this:
-
-Name: {{name}}
-Url:  {{url}}
-
-If you use the "Last X Events" mode, a set of events will be passed to your Liquid template.  You can use them like this:
-
-<table class="table">
-  {% for event in events %}
-    <tr>
-      <td>{{ event.title }}</td>
-      <td><a href="{{ event.url }}">Click here to see</a></td>
-    </tr>
-  {% endfor %}
-</table>
-EOF
+      content = <<~EOF
+        When you use the "Last event in" or "Merge events" option, you can use variables from the last event received, like this:
+
+        Name: {{name}}
+        Url:  {{url}}
+
+        If you use the "Last X Events" mode, a set of events will be passed to your Liquid template.  You can use them like this:
+
+        <table class="table">
+          {% for event in events %}
+            <tr>
+              <td>{{ event.title }}</td>
+              <td><a href="{{ event.url }}">Click here to see</a></td>
+            </tr>
+          {% endfor %}
+        </table>
+      EOF
       {
         "secret" => "a-secret-key",
         "expected_receive_period_in_days" => 2,
@@ -104,7 +99,7 @@ EOF
     form_configurable :expected_receive_period_in_days
     form_configurable :content, type: :text
     form_configurable :mime_type
-    form_configurable :mode, type: :array, values: [ 'Last event in', 'Merge events', 'Last X events']
+    form_configurable :mode, type: :array, values: ['Last event in', 'Merge events', 'Last X events']
     form_configurable :event_limit
 
     def working?
@@ -125,35 +120,49 @@ EOF
       end
 
       unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
-        errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
+        errors.add(
+          :base,
+          "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working"
+        )
       end
 
-      if options['event_limit'].present?
-        if (Integer(options['event_limit']) rescue false) == false && date_limit.blank?
-          errors.add(:base, "Event limit must be an integer that is less than 1001 or an integer plus a valid unit.")
-        elsif (options['event_limit'].to_i > 1000)
-          errors.add(:base, "For performance reasons, you cannot have an event limit greater than 1000.")
+      event_limit =
+        if value = options['event_limit'].presence
+          begin
+            Integer(value)
+          rescue StandardError
+            false
+          end
         end
-      else
+
+      if event_limit == false && date_limit.blank?
+        errors.add(:base, "Event limit must be an integer that is less than 1001 or an integer plus a valid unit.")
+      elsif event_limit && event_limit > 1000
+        errors.add(:base, "For performance reasons, you cannot have an event limit greater than 1000.")
       end
     end
 
     def receive(incoming_events)
       return unless ['merge events', 'last event in'].include?(mode)
+
       memory['last_event'] ||= {}
       incoming_events.each do |event|
-        case mode
-        when 'merge events'
-          memory['last_event'] = memory['last_event'].merge(event.payload)
-        else
-          memory['last_event'] = event.payload
-        end
+        memory['last_event'] =
+          case mode
+          when 'merge events'
+            memory['last_event'].merge(event.payload)
+          else
+            event.payload
+          end
       end
     end
 
     def receive_web_request(params, method, format)
-      valid_authentication?(params) ? [liquified_content, 200, mime_type, interpolated['response_headers'].presence]
-                                    : [unauthorized_content(format), 401]
+      if valid_authentication?(params)
+        [liquified_content, 200, mime_type, interpolated['response_headers'].presence]
+      else
+        [unauthorized_content(format), 401]
+      end
     end
 
     private
@@ -163,8 +172,11 @@ EOF
     end
 
     def unauthorized_content(format)
-      format =~ /json/ ? { error: "Not Authorized" }
-                       : "Not Authorized"
+      if format =~ /json/
+        { error: "Not Authorized" }
+      else
+        "Not Authorized"
+      end
     end
 
     def valid_authentication?(params)
@@ -193,19 +205,29 @@ EOF
     end
 
     def count_limit
-      limit = Integer(options['event_limit']) rescue 1000
+      limit = begin
+        Integer(options['event_limit'])
+      rescue StandardError
+        1000
+      end
       limit <= 1000 ? limit : 1000
     end
 
     def date_limit
       return nil unless options['event_limit'].to_s.include?(' ')
+
       value, unit = options['event_limit'].split(' ')
-      value = Integer(value) rescue nil
+      value = begin
+        Integer(value)
+      rescue StandardError
+        nil
+      end
       return nil unless value
+
       unit = unit.to_s.downcase
       return nil unless DATE_UNITS.include?(unit)
+
       value.send(unit.to_sym).ago
     end
-
   end
 end

+ 26 - 22
app/models/agents/local_file_agent.rb

@@ -13,7 +13,7 @@ module Agents
     end
 
     description do
-      <<-MD
+      <<~MD
         The LocalFileAgent can watch a file/directory for changes or emit an event for every file in that directory. When receiving an event it writes the received data into a file.
 
         `mode` determines if the agent is emitting events for (changed) files or writing received event data to disk.
@@ -41,22 +41,23 @@ module Agents
     end
 
     event_description do
-      "Events will looks like this:\n\n    %s" % if boolify(interpolated['watch'])
-        Utils.pretty_print(
-          "file_pointer" => {
-            "file" => "/tmp/test/filename",
-            "agent_id" => id
-          },
-          "event_type" => "modified/added/removed"
-        )
-      else
-        Utils.pretty_print(
-          "file_pointer" => {
-            "file" => "/tmp/test/filename",
-            "agent_id" => id
-          }
-        )
-      end
+      "Events will looks like this:\n\n    " +
+        if boolify(interpolated['watch'])
+          Utils.pretty_print(
+            "file_pointer" => {
+              "file" => "/tmp/test/filename",
+              "agent_id" => id
+            },
+            "event_type" => "modified/added/removed"
+          )
+        else
+          Utils.pretty_print(
+            "file_pointer" => {
+              "file" => "/tmp/test/filename",
+              "agent_id" => id
+            }
+          )
+        end
     end
 
     def default_options
@@ -69,8 +70,8 @@ module Agents
       }
     end
 
-    form_configurable :mode, type: :array, values: %w(read write)
-    form_configurable :watch, type: :array, values: %w(true false)
+    form_configurable :mode, type: :array, values: %w[read write]
+    form_configurable :watch, type: :array, values: %w[true false]
     form_configurable :path, type: :string
     form_configurable :append, type: :boolean
     form_configurable :data, type: :string
@@ -98,6 +99,7 @@ module Agents
     def check
       return if interpolated['mode'] != 'read' || boolify(interpolated['watch']) || !should_run?
       return unless check_path_existance(true)
+
       if File.directory?(expanded_path)
         Dir.glob(File.join(expanded_path, '*')).select { |f| File.file?(f) }
       else
@@ -109,6 +111,7 @@ module Agents
 
     def receive(incoming_events)
       return if interpolated['mode'] != 'write' || !should_run?
+
       incoming_events.each do |event|
         mo = interpolated(event)
         File.open(File.expand_path(mo['path']), boolify(mo['append']) ? 'a' : 'w') do |file|
@@ -171,7 +174,7 @@ module Agents
         AgentRunner.with_connection do
           changes.zip([:modified, :added, :removed]).each do |files, event_type|
             files.each do |file|
-              agent.create_event payload: agent.get_file_pointer(file).merge(event_type: event_type)
+              agent.create_event payload: agent.get_file_pointer(file).merge(event_type:)
             end
           end
           agent.touch(:last_check_at)
@@ -180,9 +183,10 @@ module Agents
 
       def listen_options
         if File.directory?(agent.expanded_path)
-          [agent.expanded_path, ignore!: [] ]
+          [agent.expanded_path, ignore!: []]
         else
-          [File.dirname(agent.expanded_path), { ignore!: [], only: /\A#{Regexp.escape(File.basename(agent.expanded_path))}\z/ } ]
+          [File.dirname(agent.expanded_path),
+           { ignore!: [], only: /\A#{Regexp.escape(File.basename(agent.expanded_path))}\z/ }]
         end
       end
     end

+ 6 - 5
app/models/agents/manual_event_agent.rb

@@ -3,7 +3,7 @@ module Agents
     cannot_be_scheduled!
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Manual Event Agent is used to manually create Events for testing or other purposes.
 
       Connect this Agent to other Agents and create Events using the UI provided on this Agent's Summary page.
@@ -24,15 +24,16 @@ module Agents
       if params['payload']
         json = interpolate_options(JSON.parse(params['payload']))
         if json['payloads'] && (json.keys - ['payloads']).length > 0
-          { :success => false, :error => "If you provide the 'payloads' key, please do not provide any other keys at the top level." }
+          { success: false,
+            error: "If you provide the 'payloads' key, please do not provide any other keys at the top level." }
         else
           [json['payloads'] || json].flatten.each do |payload|
-            create_event(:payload => payload)
+            create_event(payload:)
           end
-          { :success => true }
+          { success: true }
         end
       else
-        { :success => false, :error => "You must provide a JSON payload" }
+        { success: false, error: "You must provide a JSON payload" }
       end
     end
 

+ 17 - 22
app/models/agents/mqtt_agent.rb

@@ -1,11 +1,10 @@
-# encoding: utf-8 
 require "json"
 
 module Agents
   class MqttAgent < Agent
     gem_dependency_check { defined?(MQTT) }
 
-    description <<-MD
+    description <<~MD
       The MQTT Agent allows both publication and subscription to an MQTT topic.
 
       #{'## Include `mqtt` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -59,19 +58,19 @@ module Agents
       Find out more detail on [subscription wildcards](http://www.eclipse.org/paho/files/mqttdoc/Cclient/wildcard.html)
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events are simply nested MQTT payloads. For example, an MQTT payload for Owntracks
 
-      <pre><code>{
-        "topic": "owntracks/kcqlmkgx/Dan",
-        "message": {"_type": "location", "lat": "-34.8493644", "lon": "138.5218119", "tst": "1401771049", "acc": "50.0", "batt": "31", "desc": "Home", "event": "enter"},
-        "time": 1401771051
-      }</code></pre>
+          {
+            "topic": "owntracks/kcqlmkgx/Dan",
+            "message": {"_type": "location", "lat": "-34.8493644", "lon": "138.5218119", "tst": "1401771049", "acc": "50.0", "batt": "31", "desc": "Home", "event": "enter"},
+            "time": 1401771051
+          }
     MD
 
     def validate_options
       unless options['uri'].present? &&
-             options['topic'].present?
+          options['topic'].present?
         errors.add(:base, "topic and uri are required")
       end
     end
@@ -84,7 +83,7 @@ module Agents
       {
         'uri' => 'mqtts://user:pass@localhost:8883',
         'ssl' => :TLSv1,
-        'ca_file'  => './ca.pem',
+        'ca_file' => './ca.pem',
         'cert_file' => './client.crt',
         'key_file' => './client.key',
         'topic' => 'huginn',
@@ -94,16 +93,14 @@ module Agents
     end
 
     def mqtt_client
-      @client ||= begin
-        MQTT::Client.new(interpolated['uri']).tap do |c|
-          if interpolated['ssl']
-            c.ssl = interpolated['ssl'].to_sym
-            c.ca_file = interpolated['ca_file']
-            c.cert_file = interpolated['cert_file']
-            c.key_file = interpolated['key_file']
-          end
+      @client ||= MQTT::Client.new(interpolated['uri']).tap { |c|
+        if interpolated['ssl']
+          c.ssl = interpolated['ssl'].to_sym
+          c.ca_file = interpolated['ca_file']
+          c.cert_file = interpolated['cert_file']
+          c.key_file = interpolated['key_file']
         end
-      end
+      }
     end
 
     def receive(incoming_events)
@@ -114,7 +111,6 @@ module Agents
       end
     end
 
-
     def check
       last_message = memory['last_message']
       mqtt_client.connect
@@ -131,7 +127,7 @@ module Agents
           # A lot of services generate JSON, so try that.
           begin
             payload = JSON.parse(payload)
-          rescue
+          rescue StandardError
           end
 
           create_event payload: {
@@ -151,6 +147,5 @@ module Agents
       self.memory['last_message'] = last_message
       save!
     end
-
   end
 end

+ 21 - 22
app/models/agents/pdf_info_agent.rb

@@ -3,13 +3,12 @@ require 'hypdf'
 
 module Agents
   class PdfInfoAgent < Agent
-
     gem_dependency_check { defined?(HyPDF) }
 
     cannot_be_scheduled!
     no_bulk_receive!
 
-    description <<-MD
+    description <<~MD
       The PDF Info Agent returns the metadata contained within a given PDF file, using HyPDF.
 
       #{'## Include the `hypdf` gem in your `Gemfile` to use PDFInfo Agents.' if dependencies_missing?}
@@ -19,24 +18,24 @@ module Agents
       It works by acting on events that contain a key `url` in their payload, and runs the [pdfinfo](https://devcenter.heroku.com/articles/hypdf#pdfinfo) command on them.
     MD
 
-    event_description <<-MD
-    This will change based on the metadata in the pdf.
-
-      { "Title"=>"Everyday Rails Testing with RSpec", 
-        "Author"=>"Aaron Sumner",
-        "Creator"=>"LaTeX with hyperref package",
-        "Producer"=>"xdvipdfmx (0.7.8)",
-        "CreationDate"=>"Fri Aug  2 05",
-        "32"=>"50 2013",
-        "Tagged"=>"no",
-        "Pages"=>"150",
-        "Encrypted"=>"no",
-        "Page size"=>"612 x 792 pts (letter)",
-        "Optimized"=>"no",
-        "PDF version"=>"1.5",
-        "url": "your url"
-      }
-    MD
+    event_description do
+      "This will change based on the metadata in the pdf.\n\n    " +
+        Utils.pretty_print({
+          "Title" => "Everyday Rails Testing with RSpec",
+          "Author" => "Aaron Sumner",
+          "Creator" => "LaTeX with hyperref package",
+          "Producer" => "xdvipdfmx (0.7.8)",
+          "CreationDate" => "Fri Aug  2 05",
+          "32" => "50 2013",
+          "Tagged" => "no",
+          "Pages" => "150",
+          "Encrypted" => "no",
+          "Page size" => "612 x 792 pts (letter)",
+          "Optimized" => "no",
+          "PDF version" => "1.5",
+          "url": "your url"
+        })
+    end
 
     def working?
       !recent_error_logs?
@@ -57,12 +56,12 @@ module Agents
 
     def check_url(in_url, payload)
       return unless in_url.present?
+
       Array(in_url).each do |url|
         log "Fetching #{url}"
         info = HyPDF.pdfinfo(open(url))
-        create_event :payload => info.merge(payload)
+        create_event payload: info.merge(payload)
       end
     end
-
   end
 end

+ 14 - 10
app/models/agents/peak_detector_agent.rb

@@ -1,12 +1,10 @@
-require 'pp'
-
 module Agents
   class PeakDetectorAgent < Agent
     cannot_be_scheduled!
 
     DEFAULT_SEARCH_URL = 'https://twitter.com/search?q={q}'
 
-    description <<-MD
+    description <<~MD
       The Peak Detector Agent will watch for peaks in an event stream.  When a peak is detected, the resulting Event will have a payload message of `message`.  You can include extractions in the message, for example: `I saw a bar of: {{foo.bar}}`, have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) for details.
 
       The `value_path` value is a [JSONPath](http://goessner.net/articles/JsonPath/) to the value of interest.  `group_by_path` is a JSONPath that will be used to group values, if present.
@@ -20,7 +18,7 @@ module Agents
       You may set `search_url` to point to something else than Twitter search, using the URI Template syntax defined in [RFC 6570](https://tools.ietf.org/html/rfc6570). Default value is `#{DEFAULT_SEARCH_URL}` where `{q}` will be replaced with group name.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like:
 
           {
@@ -37,7 +35,7 @@ module Agents
       end
       begin
         tmpl = search_url
-      rescue => e
+      rescue StandardError => e
         errors.add(:base, "search_url must be a valid URI template: #{e.message}")
       else
         unless tmpl.keys.include?('q')
@@ -81,13 +79,18 @@ module Agents
       return if memory['data'][group].length <= options['min_events'].to_i
 
       if 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)
+        average_value, standard_deviation = stats_for(group, skip_last: 1)
         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' => interpolated(event)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s }
+          create_event payload: {
+            'message' => interpolated(event)['message'],
+            'peak' => newest_value,
+            'peak_time' => newest_time,
+            'grouped_by' => group.to_s
+          }
         end
       end
     end
@@ -132,19 +135,20 @@ module Agents
     end
 
     def group_for(event)
-      ((interpolated['group_by_path'].present? && Utils.value_at(event.payload, interpolated['group_by_path'])) || 'no_group')
+      group_by_path = interpolated['group_by_path'].presence
+      (group_by_path && Utils.value_at(event.payload, group_by_path)) || 'no_group'
     end
 
     def remember(group, event)
       memory['data'] ||= {}
       memory['data'][group] ||= []
-      memory['data'][group] << [ Utils.value_at(event.payload, interpolated['value_path']).to_f, event.created_at.to_i ]
+      memory['data'][group] << [Utils.value_at(event.payload, interpolated['value_path']).to_f, 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 }
+      memory['data'][group].reject! { |_value, time| time <= newest_time - window_duration }
     end
   end
 end

+ 3 - 2
app/models/agents/phantom_js_cloud_agent.rb

@@ -11,7 +11,7 @@ module Agents
 
     default_schedule 'every_12h'
 
-    description <<-MD
+    description <<~MD
       This Agent generates [PhantomJs Cloud](https://phantomjscloud.com/) URLs that can be used to render JavaScript-heavy webpages for content extraction.
 
       URLs generated by this Agent are formulated in accordance with the [PhantomJs Cloud API](https://phantomjscloud.com/docs/index.html).
@@ -37,8 +37,9 @@ module Agents
       As this agent only provides a limited subset of the most commonly used options, you can follow [this guide](https://github.com/huginn/huginn/wiki/Browser-Emulation-Using-PhantomJS-Cloud) to make full use of additional options PhantomJsCloud provides.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
+
           {
             "url": "..."
           }

+ 18 - 15
app/models/agents/post_agent.rb

@@ -13,7 +13,7 @@ module Agents
     default_schedule "never"
 
     description do
-      <<-MD
+      <<~MD
         A Post Agent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url.  To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`.
 
         The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`).
@@ -56,16 +56,17 @@ module Agents
       MD
     end
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
-        {
-          "status": 200,
-          "headers": {
-            "Content-Type": "text/html",
-            ...
-          },
-          "body": "<html>Some data...</html>"
-        }
+
+          {
+            "status": 200,
+            "headers": {
+              "Content-Type": "text/html",
+              ...
+            },
+            "body": "<html>Some data...</html>"
+          }
 
       Original event contents will be merged when `output_mode` is set to `merge`.
     MD
@@ -89,7 +90,7 @@ module Agents
 
     def working?
       return false if recent_error_logs?
-      
+
       if interpolated['expected_receive_period_in_days'].present?
         return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
       end
@@ -106,7 +107,8 @@ module Agents
         errors.add(:base, "post_url is a required field")
       end
 
-      if options['payload'].present? && %w[get delete].include?(method) && !(options['payload'].is_a?(Hash) || options['payload'].is_a?(Array))
+      if options['payload'].present? && %w[get
+                                           delete].include?(method) && !(options['payload'].is_a?(Hash) || options['payload'].is_a?(Array))
         errors.add(:base, "if provided, payload must be a hash or an array")
       end
 
@@ -134,11 +136,11 @@ module Agents
         errors.add(:base, "method must be 'post', 'get', 'put', 'delete', or 'patch'")
       end
 
-      if options['no_merge'].present? && !%[true false].include?(options['no_merge'].to_s)
+      if options['no_merge'].present? && !%(true false).include?(options['no_merge'].to_s)
         errors.add(:base, "if provided, no_merge must be 'true' or 'false'")
       end
 
-      if options['output_mode'].present? && !options['output_mode'].to_s.include?('{') && !%[clean merge].include?(options['output_mode'].to_s)
+      if options['output_mode'].present? && !options['output_mode'].to_s.include?('{') && !%(clean merge).include?(options['output_mode'].to_s)
         errors.add(:base, "if provided, output_mode must be 'clean' or 'merge'")
       end
 
@@ -173,7 +175,8 @@ module Agents
 
       case method
       when 'get', 'delete'
-        params, body = data, nil
+        params = data
+        body = nil
       when 'post', 'put', 'patch'
         params = nil
 

+ 43 - 33
app/models/agents/public_transport_agent.rb

@@ -6,7 +6,7 @@ module Agents
 
     default_schedule "every_2m"
 
-    description <<-MD
+    description <<~MD
       The Public Transport Request Agent generates Events based on NextBus GPS transit predictions.
 
       Specify the following user settings:
@@ -17,7 +17,7 @@ module Agents
 
       First, select an agency by visiting [http://www.nextbus.com/predictor/adaAgency.jsp](http://www.nextbus.com/predictor/adaAgency.jsp) and finding your transit system.  Once you find it, copy the part of the URL after `?a=`.  For example, for the San Francisco MUNI system, you would end up on [http://www.nextbus.com/predictor/adaDirection.jsp?a=**sf-muni**](http://www.nextbus.com/predictor/adaDirection.jsp?a=sf-muni) and copy "sf-muni".  Put that into this Agent's agency setting.
 
-      Next, find the stop tags that you care about. 
+      Next, find the stop tags that you care about.
 
       Select your destination and lets use the n-judah route. The link should be [http://www.nextbus.com/predictor/adaStop.jsp?a=sf-muni&r=N](http://www.nextbus.com/predictor/adaStop.jsp?a=sf-muni&r=N) Once you find it, copy the part of the URL after `r=`.
 
@@ -42,18 +42,22 @@ module Agents
           alert_window_in_minutes: 5
     MD
 
-    event_description <<-MD
-    Events look like this:
-      { "routeTitle":"N-Judah",
-        "stopTag":"5215",
-        "prediction":
-           {"epochTime":"1389622846689",
-            "seconds":"3454","minutes":"57","isDeparture":"false",
-            "affectedByLayover":"true","dirTag":"N__OB4KJU","vehicle":"1489",
-            "block":"9709","tripTag":"5840086"
-            }
-      }
-    MD
+    event_description "Events look like this:\n\n    " +
+      Utils.pretty_print({
+        "routeTitle": "N-Judah",
+        "stopTag": "5215",
+        "prediction": {
+          "epochTime": "1389622846689",
+          "seconds": "3454",
+          "minutes": "57",
+          "isDeparture": "false",
+          "affectedByLayover": "true",
+          "dirTag": "N__OB4KJU",
+          "vehicle": "1489",
+          "block": "9709",
+          "tripTag": "5840086"
+        }
+      })
 
     def check_url
       query = URI.encode_www_form([
@@ -65,27 +69,27 @@ module Agents
     end
 
     def stops
-      interpolated["stops"].collect{|a| a.split("|").last}
+      interpolated["stops"].collect { |a| a.split("|").last }
     end
 
     def check
       hydra = Typhoeus::Hydra.new
-      request = Typhoeus::Request.new(check_url, :followlocation => true)
+      request = Typhoeus::Request.new(check_url, followlocation: true)
       request.on_success do |response|
         page = Nokogiri::XML response.body
         predictions = page.css("//prediction")
         predictions.each do |pr|
           parent = pr.parent.parent
-          vals = {"routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"]}
-          if pr["minutes"] && pr["minutes"].to_i < interpolated["alert_window_in_minutes"].to_i
-            vals = vals.merge Hash.from_xml(pr.to_xml)
-            if not_already_in_memory?(vals)
-              create_event(:payload => vals)
-              log "creating event..."
-              update_memory(vals)
-            else
-              log "not creating event since already in memory"
-            end
+          vals = { "routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"] }
+          next unless pr["minutes"] && pr["minutes"].to_i < interpolated["alert_window_in_minutes"].to_i
+
+          vals = vals.merge Hash.from_xml(pr.to_xml)
+          if not_already_in_memory?(vals)
+            create_event(payload: vals)
+            log "creating event..."
+            update_memory(vals)
+          else
+            log "not creating event since already in memory"
           end
         end
       end
@@ -100,20 +104,26 @@ module Agents
 
     def cleanup_old_memory
       self.memory["existing_routes"] ||= []
-      self.memory["existing_routes"].reject!{|h| h["currentTime"].to_time <= (Time.now - 2.hours)}
+      time = 2.hours.ago
+      self.memory["existing_routes"].reject! { |h| h["currentTime"].to_time <= time }
     end
 
     def add_to_memory(vals)
-      self.memory["existing_routes"] ||= []
-      self.memory["existing_routes"] << {"stopTag" => vals["stopTag"], "tripTag" => vals["prediction"]["tripTag"], "epochTime" => vals["prediction"]["epochTime"], "currentTime" => Time.now}
+      (self.memory["existing_routes"] ||= []) << {
+        "stopTag" => vals["stopTag"],
+        "tripTag" => vals["prediction"]["tripTag"],
+        "epochTime" => vals["prediction"]["epochTime"],
+        "currentTime" => Time.now
+      }
     end
 
     def not_already_in_memory?(vals)
       m = self.memory["existing_routes"] || []
-      m.select{|h| h['stopTag'] == vals["stopTag"] &&
-                h['tripTag'] == vals["prediction"]["tripTag"] &&
-                h['epochTime'] == vals["prediction"]["epochTime"]
-              }.count == 0
+      m.select { |h|
+        h['stopTag'] == vals["stopTag"] &&
+          h['tripTag'] == vals["prediction"]["tripTag"] &&
+          h['epochTime'] == vals["prediction"]["epochTime"]
+      }.count == 0
     end
 
     def default_options

+ 15 - 10
app/models/agents/pushbullet_agent.rb

@@ -10,13 +10,13 @@ module Agents
 
     API_BASE = 'https://api.pushbullet.com/v2/'
     TYPE_TO_ATTRIBUTES = {
-            'note'    => [:title, :body],
-            'link'    => [:title, :body, :url],
-            'address' => [:name, :address]
+      'note' => [:title, :body],
+      'link' => [:title, :body, :url],
+      'address' => [:name, :address]
     }
     class Unauthorized < StandardError; end
 
-    description <<-MD
+    description <<~MD
       The Pushbullet agent sends pushes to a pushbullet device
 
       To authenticate you need to either the `api_key` or create a `pushbullet_api_key` credential, you can find yours at your account page:
@@ -60,9 +60,11 @@ module Agents
     def validate_options
       errors.add(:base, "you need to specify a pushbullet api_key") if options['api_key'].blank?
       errors.add(:base, "you need to specify a device_id") if options['device_id'].blank?
-      errors.add(:base, "you need to specify a valid message type") if options['type'].blank? or not ['note', 'link', 'address'].include?(options['type'])
+      errors.add(:base, "you need to specify a valid message type") if options['type'].blank? ||
+        !['note', 'link', 'address'].include?(options['type'])
       TYPE_TO_ATTRIBUTES[options['type']].each do |attr|
-        errors.add(:base, "you need to specify '#{attr.to_s}' for the type '#{options['type']}'") if options[attr].blank?
+        errors.add(:base,
+                   "you need to specify '#{attr}' for the type '#{options['type']}'") if options[attr].blank?
       end
     end
 
@@ -75,7 +77,7 @@ module Agents
 
     def complete_device_id
       devices
-        .map { |d| {text: d['nickname'], id: d['iden']} }
+        .map { |d| { text: d['nickname'], id: d['iden'] } }
         .unshift(text: 'All Devices', id: '__ALL__')
     end
 
@@ -92,6 +94,7 @@ module Agents
     end
 
     private
+
     def safely
       yield
     rescue Unauthorized => e
@@ -101,6 +104,7 @@ module Agents
     def request(http_method, method, options)
       response = JSON.parse(HTTParty.send(http_method, API_BASE + method, options).body)
       raise Unauthorized, response['error']['message'] if response['error'].present?
+
       response
     end
 
@@ -113,20 +117,21 @@ module Agents
 
     def create_device
       return if options['device_id'].present?
+
       safely do
-        response = request(:post, 'devices', basic_auth.merge(body: {nickname: 'Huginn', type: 'stream'}))
+        response = request(:post, 'devices', basic_auth.merge(body: { nickname: 'Huginn', type: 'stream' }))
         self.options[:device_id] = response['iden']
       end
     end
 
     def basic_auth
-      {basic_auth: {username: interpolated[:api_key].presence || credential('pushbullet_api_key'), password: ''}}
+      { basic_auth: { username: interpolated[:api_key].presence || credential('pushbullet_api_key'), password: '' } }
     end
 
     def query_options(event)
       mo = interpolated(event)
       dev_ident = mo[:device_id] == "__ALL__" ? '' : mo[:device_id]
-      basic_auth.merge(body: {device_iden: dev_ident, type: mo[:type]}.merge(payload(mo)))
+      basic_auth.merge(body: { device_iden: dev_ident, type: mo[:type] }.merge(payload(mo)))
     end
 
     def payload(mo)

+ 9 - 10
app/models/agents/pushover_agent.rb

@@ -5,10 +5,9 @@ module Agents
     cannot_create_events!
     no_bulk_receive!
 
-
     API_URL = 'https://api.pushover.net/1/messages.json'
 
-    description <<-MD
+    description <<~MD
       The Pushover Agent receives and collects events and sends them via push notification to a user/group.
 
       **You need a Pushover API Token:** [https://pushover.net/apps/build](https://pushover.net/apps/build)
@@ -88,15 +87,15 @@ module Agents
             retry
             expire
           ].each do |key|
-            if value = String.try_convert(interpolated[key].presence)
-              case key
-              when 'url'
-                value.slice!(512..-1)
-              when 'url_title'
-                value.slice!(100..-1)
-              end
-              post_params[key] = value
+            value = String.try_convert(interpolated[key].presence) or next
+
+            case key
+            when 'url'
+              value.slice!(512..-1)
+            when 'url_title'
+              value.slice!(100..-1)
             end
+            post_params[key] = value
           end
           # html is special because String.try_convert(true) gives nil (not even "nil", just nil)
           if value = interpolated['html'].presence

+ 8 - 5
app/models/agents/read_file_agent.rb

@@ -13,7 +13,7 @@ module Agents
     end
 
     description do
-      <<-MD
+      <<~MD
         The ReadFileAgent takes events from `FileHandling` agents, reads the file, and emits the contents as a string.
 
         `data_key` specifies the key of the emitted event which contains the file contents.
@@ -22,10 +22,12 @@ module Agents
       MD
     end
 
-    event_description <<-MD
-      {
-        "data" => '...'
-      }
+    event_description <<~MD
+      Events look like:
+
+          {
+            "data" => '...'
+          }
     MD
 
     form_configurable :data_key, type: :string
@@ -43,6 +45,7 @@ module Agents
     def receive(incoming_events)
       incoming_events.each do |event|
         next unless io = get_io(event)
+
         create_event payload: { interpolated['data_key'] => io.read }
       end
     end

+ 26 - 25
app/models/agents/rss_agent.rb

@@ -11,7 +11,7 @@ module Agents
     DEFAULT_EVENTS_ORDER = [['{{date_published}}', 'time'], ['{{last_updated}}', 'time']]
 
     description do
-      <<-MD
+      <<~MD
         The RSS Agent consumes RSS feeds and emits events when they change.
 
         This agent, using [Feedjira](https://github.com/feedjira/feedjira) as a base, can parse various types of RSS and Atom feeds and has some special handlers for FeedBurner, iTunes RSS, and so on.  However, supported fields are limited by its general and abstract nature.  For complex feeds with additional field types, we recommend using a WebsiteAgent.  See [this example](https://github.com/huginn/huginn/wiki/Agent-configuration-examples#itunes-trailers).
@@ -49,7 +49,7 @@ module Agents
       }
     end
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like:
 
           {
@@ -131,11 +131,13 @@ module Agents
       errors.add(:base, "url is required") unless options['url'].present?
 
       unless options['expected_update_period_in_days'].present? && options['expected_update_period_in_days'].to_i > 0
-        errors.add(:base, "Please provide 'expected_update_period_in_days' to indicate how many days can pass without an update before this Agent is considered to not be working")
+        errors.add(:base,
+                   "Please provide 'expected_update_period_in_days' to indicate how many days can pass without an update before this Agent is considered to not be working")
       end
 
       if options['remembered_id_count'].present? && options['remembered_id_count'].to_i < 1
-        errors.add(:base, "Please provide 'remembered_id_count' as a number bigger than 0 indicating how many IDs should be saved to distinguish between new and old IDs in RSS feeds. Delete option to use default (500).")
+        errors.add(:base,
+                   "Please provide 'remembered_id_count' as a number bigger than 0 indicating how many IDs should be saved to distinguish between new and old IDs in RSS feeds. Delete option to use default (500).")
       end
 
       validate_web_request_options!
@@ -161,17 +163,15 @@ module Agents
       max_events = (interpolated['max_events_per_run'].presence || 0).to_i
 
       urls.each do |url|
-        begin
-          response = faraday.get(url)
-          if response.success?
-            feed = Feedjira.parse(preprocessed_body(response))
-            new_events.concat feed_to_events(feed)
-          else
-            error "Failed to fetch #{url}: #{response.inspect}"
-          end
-        rescue => e
-          error "Failed to fetch #{url} with message '#{e.message}': #{e.backtrace}"
+        response = faraday.get(url)
+        if response.success?
+          feed = Feedjira.parse(preprocessed_body(response))
+          new_events.concat feed_to_events(feed)
+        else
+          error "Failed to fetch #{url}: #{response.inspect}"
         end
+      rescue StandardError => e
+        error "Failed to fetch #{url} with message '#{e.message}': #{e.backtrace}"
       end
 
       events = sort_events(new_events).select.with_index { |event, index|
@@ -210,7 +210,8 @@ module Agents
       else
         # Encoding is already known, so do not let the parser detect
         # it from the XML declaration in the content.
-        body.sub!(/(?<noenc>\A\u{FEFF}?\s*<\?xml(?:\s+\w+(?<av>\s*=\s*(?:'[^']*'|"[^"]*")))*?)\s+encoding\g<av>/, '\\k<noenc>')
+        body.sub!(/(?<noenc>\A\u{FEFF}?\s*<\?xml(?:\s+\w+(?<av>\s*=\s*(?:'[^']*'|"[^"]*")))*?)\s+encoding\g<av>/,
+                  '\\k<noenc>')
       end
       body
     end
@@ -226,7 +227,7 @@ module Agents
 
       {
         id: feed.feed_id,
-        type: type,
+        type:,
         url: feed.url,
         links: feed.links,
         title: feed.title,
@@ -257,15 +258,15 @@ module Agents
           itunes_summary
           language
         ].each { |attr|
-          if value = feed.try(attr).presence
-            data[attr] =
-              case attr
-              when :itunes_summary
-                clean_fragment(value)
-              else
-                value
-              end
-          end
+          next unless value = feed.try(attr).presence
+
+          data[attr] =
+            case attr
+            when :itunes_summary
+              clean_fragment(value)
+            else
+              value
+            end
         }
       end
       data

+ 29 - 25
app/models/agents/s3_agent.rb

@@ -11,7 +11,7 @@ module Agents
     gem_dependency_check { defined?(Aws::S3) }
 
     description do
-      <<-MD
+      <<~MD
         The S3Agent can watch a bucket for changes or emit an event for every file in that bucket. When receiving events, it writes the data into a file on S3.
 
         #{'## Include `aws-sdk-core` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -41,22 +41,23 @@ module Agents
     end
 
     event_description do
-      "Events will looks like this:\n\n    %s" % if boolify(interpolated['watch'])
-        Utils.pretty_print({
-          "file_pointer" => {
-            "file" => "filename",
-            "agent_id" => id
-          },
-          "event_type" => "modified/added/removed"
-        })
-      else
-        Utils.pretty_print({
-          "file_pointer" => {
-            "file" => "filename",
-            "agent_id" => id
-          }
-        })
-      end
+      "Events will looks like this:\n\n    " +
+        if boolify(interpolated['watch'])
+          Utils.pretty_print({
+            "file_pointer" => {
+              "file" => "filename",
+              "agent_id" => id
+            },
+            "event_type" => "modified/added/removed"
+          })
+        else
+          Utils.pretty_print({
+            "file_pointer" => {
+              "file" => "filename",
+              "agent_id" => id
+            }
+          })
+        end
     end
 
     def default_options
@@ -70,11 +71,12 @@ module Agents
       }
     end
 
-    form_configurable :mode, type: :array, values: %w(read write)
+    form_configurable :mode, type: :array, values: %w[read write]
     form_configurable :access_key_id, roles: :validatable
     form_configurable :access_key_secret, roles: :validatable
-    form_configurable :region, type: :array, values: %w(us-east-1 us-west-1 us-west-2 eu-west-1 eu-central-1 ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2 sa-east-1)
-    form_configurable :watch, type: :array, values: %w(true false)
+    form_configurable :region, type: :array,
+                               values: %w[us-east-1 us-west-1 us-west-2 eu-west-1 eu-central-1 ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2 sa-east-1]
+    form_configurable :watch, type: :array, values: %w[true false]
     form_configurable :bucket, roles: :completable
     form_configurable :filename
     form_configurable :data
@@ -114,7 +116,7 @@ module Agents
     end
 
     def complete_bucket
-      (buckets || []).collect { |room| {text: room.name, id: room.name} }
+      (buckets || []).collect { |room| { text: room.name, id: room.name } }
     end
 
     def working?
@@ -123,9 +125,10 @@ module Agents
 
     def check
       return if interpolated['mode'] != 'read'
+
       contents = safely do
-                   get_bucket_contents
-                 end
+        get_bucket_contents
+      end
       if boolify(interpolated['watch'])
         watch(contents)
       else
@@ -141,6 +144,7 @@ module Agents
 
     def receive(incoming_events)
       return if interpolated['mode'] != 'write'
+
       incoming_events.each do |event|
         safely do
           mo = interpolated(event)
@@ -155,7 +159,7 @@ module Agents
       yield
     rescue Aws::S3::Errors::AccessDenied => e
       error("Could not access '#{interpolated['bucket']}' #{e.class} #{e.message}")
-    rescue Aws::S3::Errors::ServiceError =>e
+    rescue Aws::S3::Errors::ServiceError => e
       error("#{e.class}: #{e.message}")
     end
 
@@ -175,7 +179,7 @@ module Agents
         end
         contents.delete(key)
       end
-      contents.each do |key, etag|
+      contents.each do |key, _etag|
         create_event payload: get_file_pointer(key).merge(event_type: :added)
       end
 

+ 1 - 1
app/models/agents/scheduler_agent.rb

@@ -12,7 +12,7 @@ module Agents
 
     cattr_reader :second_precision_enabled
 
-    description <<-MD
+    description <<~MD
       The Scheduler Agent periodically takes an action on target Agents according to a user-defined schedule.
 
       # Action types

+ 21 - 16
app/models/agents/sentiment_agent.rb

@@ -6,7 +6,7 @@ module Agents
 
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Sentiment Agent generates `good-bad` (psychological valence or happiness index), `active-passive` (arousal), and  `strong-weak` (dominance) score. It will output a value between 1 and 9. It will only work on English content.
 
       Make sure the content this agent is analyzing is of sufficient length to get respectable results.
@@ -14,7 +14,7 @@ module Agents
       Provide a JSONPath in `content` field where content is residing and set `expected_receive_period_in_days` to the maximum number of days you would allow to be passed between events being received by this agent.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like:
 
           {
@@ -41,17 +41,22 @@ module Agents
       incoming_events.each do |event|
         Utils.values_at(event.payload, interpolated['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
@@ -67,18 +72,18 @@ module Agents
     def sentiment_values(anew, text)
       valence, arousal, dominance, freq = [0] * 4
       text.downcase.strip.gsub(/[^a-z ]/, "").split.each do |word|
-        if anew.has_key? word
-          valence += anew[word][0]
-          arousal += anew[word][1]
-          dominance += anew[word][2]
-          freq += 1
-        end
+        next unless anew.has_key? word
+
+        valence += anew[word][0]
+        arousal += anew[word][1]
+        dominance += anew[word][2]
+        freq += 1
       end
       if valence != 0
-        [valence/freq, arousal/freq, dominance/freq]
+        [valence / freq, arousal / freq, dominance / freq]
       else
         ["Insufficient data for meaningful answer"] * 3
       end
     end
   end
-end
+end

+ 18 - 19
app/models/agents/shell_command_agent.rb

@@ -5,12 +5,11 @@ module Agents
     can_dry_run!
     no_bulk_receive!
 
-
     def self.should_run?
       ENV['ENABLE_INSECURE_AGENTS'] == "true"
     end
 
-    description <<-MD
+    description <<~MD
       The Shell Command Agent will execute commands on your local system, returning the output.
 
       `command` specifies the command (either a shell command line string or an array of command line arguments) to be executed, and `path` will tell ShellCommandAgent in what directory to run this command.  The content of `stdin` will be fed to the command via the standard input.
@@ -33,26 +32,26 @@ module Agents
       You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
     MD
 
-    event_description <<-MD
-    Events look like this:
+    event_description <<~MD
+      Events look like this:
 
-        {
-          "command": "pwd",
-          "path": "/home/Huginn",
-          "exit_status": 0,
-          "errors": "",
-          "output": "/home/Huginn"
-        }
+          {
+            "command": "pwd",
+            "path": "/home/Huginn",
+            "exit_status": 0,
+            "errors": "",
+            "output": "/home/Huginn"
+          }
     MD
 
     def default_options
       {
-          'path' => "/",
-          'command' => "pwd",
-          'unbundle' => false,
-          'suppress_on_failure' => false,
-          'suppress_on_empty_output' => false,
-          'expected_update_period_in_days' => 1
+        'path' => "/",
+        'command' => "pwd",
+        'unbundle' => false,
+        'suppress_on_failure' => false,
+        'suppress_on_empty_output' => false,
+        'expected_update_period_in_days' => 1
       }
     end
 
@@ -109,7 +108,7 @@ module Agents
         }
 
         unless suppress_event?(payload)
-          created_event = create_event payload: payload
+          created_event = create_event(payload:)
         end
 
         log("Ran '#{command}' under '#{path}'", outbound_event: created_event, inbound_event: event)
@@ -146,7 +145,7 @@ module Agents
 
         _, status = Process.wait2(pid)
         exit_status = status.exitstatus
-      rescue => e
+      rescue StandardError => e
         errors = e.to_s
         result = ''.freeze
         exit_status = nil

+ 4 - 4
app/models/agents/slack_agent.rb

@@ -10,7 +10,7 @@ module Agents
 
     gem_dependency_check { defined?(Slack) }
 
-    description <<-MD
+    description <<~MD
       The Slack Agent lets you receive events and send notifications to [Slack](https://slack.com/).
 
       #{'## Include `slack-notifier` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -38,7 +38,7 @@ module Agents
 
     def validate_options
       unless options['webhook_url'].present? ||
-             (options['auth_token'].present? && options['team_name'].present?)  # compatibility
+          (options['auth_token'].present? && options['team_name'].present?)  # compatibility
         errors.add(:base, "webhook_url is required")
       end
 
@@ -65,11 +65,11 @@ module Agents
     end
 
     def slack_notifier
-      @slack_notifier ||= Slack::Notifier.new(webhook_url, username: username)
+      @slack_notifier ||= Slack::Notifier.new(webhook_url, username:)
     end
 
     def filter_options(opts)
-      opts.select { |key, value| ALLOWED_PARAMS.include? key }.symbolize_keys
+      opts.select { |key, _value| ALLOWED_PARAMS.include? key }.symbolize_keys
     end
 
     def receive(incoming_events)

+ 16 - 17
app/models/agents/stubhub_agent.rb

@@ -2,24 +2,25 @@ module Agents
   class StubhubAgent < Agent
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The StubHub Agent creates an event for a given StubHub Event.
 
       It can be used to track how many tickets are available for the event and the minimum and maximum price. All that is required is that you paste in the url from the actual event, e.g. https://www.stubhub.com/outside-lands-music-festival-tickets/outside-lands-music-festival-3-day-pass-san-francisco-golden-gate-park-polo-fields-8-8-2014-9020701/
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events looks like this:
-        {
-          "url": "https://stubhub.com/valid-event-url"
-          "name": "Event Name"
-          "date": "2014-08-01"
-          "max_price": "999.99"
-          "min_price": "100.99"
-          "total_postings": "50"
-          "total_tickets": "150"
-          "venue_name": "Venue Name"
-        }
+
+          {
+            "url": "https://stubhub.com/valid-event-url"
+            "name": "Event Name"
+            "date": "2014-08-01"
+            "max_price": "999.99"
+            "min_price": "100.99"
+            "total_postings": "50"
+            "total_tickets": "150"
+            "venue_name": "Venue Name"
+          }
     MD
 
     default_schedule "every_1d"
@@ -29,7 +30,7 @@ module Agents
     end
 
     def default_options
-      { 'url' =>  'https://stubhub.com/enter-your-event-here' }
+      { 'url' => 'https://stubhub.com/enter-your-event-here' }
     end
 
     def validate_options
@@ -41,7 +42,7 @@ module Agents
     end
 
     def check
-      create_event :payload => fetch_stubhub_data(url)
+      create_event payload: fetch_stubhub_data(url)
     end
 
     def fetch_stubhub_data(url)
@@ -49,7 +50,6 @@ module Agents
     end
 
     class StubhubFetcher
-
       def self.call(url)
         new(url).fields
       end
@@ -63,7 +63,7 @@ module Agents
       end
 
       def base_url
-       'https://www.stubhub.com/listingCatalog/select/?q='
+        'https://www.stubhub.com/listingCatalog/select/?q='
       end
 
       def build_url
@@ -96,7 +96,6 @@ module Agents
       private
 
       attr_reader :url
-
     end
   end
 end

+ 29 - 12
app/models/agents/telegram_agent.rb

@@ -9,14 +9,14 @@ module Agents
     no_bulk_receive!
     can_dry_run!
 
-    description <<-MD
+    description <<~MD
       The Telegram Agent receives and collects events and sends them via [Telegram](https://telegram.org/).
 
       It is assumed that events have either a `text`, `photo`, `audio`, `document`, `video` or `group` key. You can use the EventFormattingAgent if your event does not provide these keys.
 
       The value of `text` key is sent as a plain text message. You can also tell Telegram how to parse the message with `parse_mode`, set to either `html`, `markdown` or `markdownv2`.
       The value of `photo`, `audio`, `document` and `video` keys should be a url whose contents will be sent to you.
-      The value of `group` key should be a list and must consist of 2-10 objects representing an [InputMedia](https://core.telegram.org/bots/api#inputmedia) from the [Telegram Bot API](https://core.telegram.org/bots/api#inputmedia). Be careful: the `caption` field is not covered by the "long message" setting. 
+      The value of `group` key should be a list and must consist of 2-10 objects representing an [InputMedia](https://core.telegram.org/bots/api#inputmedia) from the [Telegram Bot API](https://core.telegram.org/bots/api#inputmedia). Be careful: the `caption` field is not covered by the "long message" setting.#{' '}
 
       **Setup**
 
@@ -63,17 +63,31 @@ module Agents
     def complete_chat_id
       response = HTTMultiParty.post(telegram_bot_uri('getUpdates'))
       return [] unless response['ok']
+
       response['result'].map { |update| update_to_complete(update) }.uniq
     end
 
     def validate_options
       errors.add(:base, 'auth_token is required') unless options['auth_token'].present?
       errors.add(:base, 'chat_id is required') unless options['chat_id'].present?
-      errors.add(:base, 'caption should be 1024 characters or less') if interpolated['caption'].present? && interpolated['caption'].length > 1024 && (!interpolated['long_message'].present? || interpolated['long_message'] != 'split')
-      errors.add(:base, "disable_notification has invalid value: should be 'true' or 'false'") if interpolated['disable_notification'].present? && !%w(true false).include?(interpolated['disable_notification'])
-      errors.add(:base, "disable_web_page_preview has invalid value: should be 'true' or 'false'") if interpolated['disable_web_page_preview'].present? && !%w(true false).include?(interpolated['disable_web_page_preview'])
-      errors.add(:base, "long_message has invalid value: should be 'split' or 'truncate'") if interpolated['long_message'].present? && !%w(split truncate).include?(interpolated['long_message'])
-      errors.add(:base, "parse_mode has invalid value: should be 'html', 'markdown' or 'markdownv2'") if interpolated['parse_mode'].present? && !%w(html markdown markdownv2).include?(interpolated['parse_mode'])
+      errors.add(:base,
+                 'caption should be 1024 characters or less') if interpolated['caption'].present? && interpolated['caption'].length > 1024 && (!interpolated['long_message'].present? || interpolated['long_message'] != 'split')
+      errors.add(:base,
+                 "disable_notification has invalid value: should be 'true' or 'false'") if interpolated['disable_notification'].present? && !%w[
+                   true false
+                 ].include?(interpolated['disable_notification'])
+      errors.add(:base,
+                 "disable_web_page_preview has invalid value: should be 'true' or 'false'") if interpolated['disable_web_page_preview'].present? && !%w[
+                   true false
+                 ].include?(interpolated['disable_web_page_preview'])
+      errors.add(:base,
+                 "long_message has invalid value: should be 'split' or 'truncate'") if interpolated['long_message'].present? && !%w[
+                   split truncate
+                 ].include?(interpolated['long_message'])
+      errors.add(:base,
+                 "parse_mode has invalid value: should be 'html', 'markdown' or 'markdownv2'") if interpolated['parse_mode'].present? && !%w[
+                   html markdown markdownv2
+                 ].include?(interpolated['parse_mode'])
     end
 
     def working?
@@ -99,11 +113,13 @@ module Agents
 
     def configure_params(params)
       params[:chat_id] = interpolated['chat_id']
-      params[:disable_notification] = interpolated['disable_notification'] if interpolated['disable_notification'].present?
+      params[:disable_notification] =
+        interpolated['disable_notification'] if interpolated['disable_notification'].present?
       if params.has_key?(:text)
-        params[:disable_web_page_preview] = interpolated['disable_web_page_preview'] if interpolated['disable_web_page_preview'].present?
+        params[:disable_web_page_preview] =
+          interpolated['disable_web_page_preview'] if interpolated['disable_web_page_preview'].present?
         params[:parse_mode] = interpolated['parse_mode'] if interpolated['parse_mode'].present?
-      elsif not params.has_key?(:media)
+      elsif !params.has_key?(:media)
         params[:caption] = interpolated['caption'] if interpolated['caption'].present?
       end
 
@@ -115,8 +131,9 @@ module Agents
         messages_send = TELEGRAM_ACTIONS.count do |field, _method|
           payload = event.payload[field]
           next unless payload.present?
+
           if field == :group
-            send_telegram_messages field, configure_params(:media => payload)
+            send_telegram_messages field, configure_params(media: payload)
           else
             send_telegram_messages field, configure_params(field => payload)
           end
@@ -163,7 +180,7 @@ module Agents
 
     def update_to_complete(update)
       chat = (update['message'] || update.fetch('channel_post', {})).fetch('chat', {})
-      {id: chat['id'], text: chat['title'] || "#{chat['first_name']} #{chat['last_name']}"}
+      { id: chat['id'], text: chat['title'] || "#{chat['first_name']} #{chat['last_name']}" }
     end
   end
 end

+ 36 - 22
app/models/agents/trigger_agent.rb

@@ -3,9 +3,19 @@ module Agents
     cannot_be_scheduled!
     can_dry_run!
 
-    VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value not\ in]
-
-    description <<-MD
+    VALID_COMPARISON_TYPES = %w[
+      regex
+      !regex
+      field<value
+      field<=value
+      field==value
+      field!=value
+      field>=value
+      field>value
+      not\ in
+    ]
+
+    description <<~MD
       The Trigger Agent will watch for a specific value in an Event payload.
 
       The `rules` array contains a mixture of strings and hashes.
@@ -32,7 +42,7 @@ module Agents
       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.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
 
           { "message": "Your message" }
@@ -52,14 +62,19 @@ module Agents
 
     def validate_options
       unless options['expected_receive_period_in_days'].present? &&
-             options['rules'].present? &&
-             options['rules'].all? { |rule| valid_rule?(rule) }
-        errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
+          options['rules'].present? &&
+          options['rules'].all? { |rule| valid_rule?(rule) }
+        errors.add(:base,
+                   "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
       end
 
-      errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
+      errors.add(:base,
+                 "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
 
-      errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event'])
+      errors.add(:base,
+                 "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[
+                   true false
+                 ].include?(options['keep_event'])
 
       if options['must_match'].present?
         if options['must_match'].to_i < 1
@@ -75,10 +90,10 @@ module Agents
         'expected_receive_period_in_days' => "2",
         'keep_event' => 'false',
         'rules' => [{
-                      'type' => "regex",
-                      'value' => "foo\\d+bar",
-                      'path' => "topkey.subkey.subkey.goal",
-                    }],
+          'type' => "regex",
+          'value' => "foo\\d+bar",
+          'path' => "topkey.subkey.subkey.goal",
+        }],
         'message' => "Looks like your pattern matched in '{{value}}'!"
       }
     end
@@ -89,7 +104,6 @@ module Agents
 
     def receive(incoming_events)
       incoming_events.each do |event|
-
         opts = interpolated(event)
 
         match_results = opts['rules'].map do |rule|
@@ -129,16 +143,16 @@ module Agents
           end
         end
 
-        if matches?(match_results)
-          if keep_event?
-            payload = event.payload.dup
-            payload['message'] = opts['message'] if opts['message'].present?
-          else
-            payload = { 'message' => opts['message'] }
-          end
+        next unless matches?(match_results)
 
-          create_event :payload => payload
+        if keep_event?
+          payload = event.payload.dup
+          payload['message'] = opts['message'] if opts['message'].present?
+        else
+          payload = { 'message' => opts['message'] }
         end
+
+        create_event(payload:)
       end
     end
 

+ 8 - 7
app/models/agents/tumblr_likes_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     gem_dependency_check { defined?(Tumblr::Client) }
 
-    description <<-MD
+    description <<~MD
       The Tumblr Likes Agent checks for liked Tumblr posts from a specific blog.
 
       #{'## Include `tumblr_client` and `omniauth-tumblr` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -23,7 +23,8 @@ module Agents
 
     def validate_options
       errors.add(:base, 'blog_name is required') unless options['blog_name'].present?
-      errors.add(:base, 'expected_update_period_in_days is required') unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 'expected_update_period_in_days is required') unless options['expected_update_period_in_days'].present?
     end
 
     def working?
@@ -47,11 +48,11 @@ module Agents
       if liked['liked_posts']
         # Loop over all liked posts which came back from Tumblr, add to memory, and create events.
         liked['liked_posts'].each do |post|
-          unless memory[:ids].include?(post['id'])
-            memory[:ids].push(post['id'])
-            memory[:last_liked] = post['liked_timestamp'] if post['liked_timestamp'] > memory[:last_liked]
-            create_event(payload: post)
-          end
+          next if memory[:ids].include?(post['id'])
+
+          memory[:ids].push(post['id'])
+          memory[:last_liked] = post['liked_timestamp'] if post['liked_timestamp'] > memory[:last_liked]
+          create_event(payload: post)
         end
       elsif liked['status'] && liked['msg']
         # If there was a problem fetching likes (like 403 Forbidden or 404 Not Found) create an error message.

+ 13 - 14
app/models/agents/tumblr_publish_agent.rb

@@ -6,7 +6,7 @@ module Agents
 
     gem_dependency_check { defined?(Tumblr::Client) }
 
-    description <<-MD
+    description <<~MD
       The Tumblr Publish Agent publishes Tumblr posts from the events it receives.
 
       #{'## Include `tumblr_client` and `omniauth-tumblr` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -59,7 +59,8 @@ module Agents
     MD
 
     def validate_options
-      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
     end
 
     def working?
@@ -112,9 +113,9 @@ module Agents
             return
           end
           expanded_post = get_post(blog_name, post["id"])
-          create_event :payload => {
+          create_event payload: {
             'success' => true,
-            'published_post' => "["+blog_name+"] "+post_type,
+            'published_post' => "[" + blog_name + "] " + post_type,
             'post_id' => post["id"],
             'agent_id' => event.agent_id,
             'event_id' => event.id,
@@ -126,13 +127,13 @@ module Agents
 
     def publish_post(blog_name, post_type, options)
       options_obj = {
-          :state => options['state'],
-          :tags => options['tags'],
-          :tweet => options['tweet'],
-          :date => options['date'],
-          :format => options['format'],
-          :slug => options['slug'],
-        }
+        state: options['state'],
+        tags: options['tags'],
+        tweet: options['tweet'],
+        date: options['date'],
+        format: options['format'],
+        slug: options['slug'],
+      }
 
       case post_type
       when "text"
@@ -174,9 +175,7 @@ module Agents
     end
 
     def get_post(blog_name, id)
-      obj = tumblr.posts(blog_name, {
-        :id => id
-      })
+      obj = tumblr.posts(blog_name, { id: })
       obj["posts"].first
     end
   end

+ 15 - 12
app/models/agents/twilio_agent.rb

@@ -8,7 +8,7 @@ module Agents
 
     gem_dependency_check { defined?(Twilio) }
 
-    description <<-MD
+    description <<~MD
       The Twilio Agent receives and collects events and sends them via text message (up to 160 characters) or gives you a call when scheduled.
 
       #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -29,16 +29,17 @@ module Agents
         'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
         'sender_cell' => 'xxxxxxxxxx',
         'receiver_cell' => 'xxxxxxxxxx',
-        'server_url'    => 'http://somename.com:3000',
-        'receive_text'  => 'true',
-        'receive_call'  => 'false',
+        '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?
-        errors.add(:base, 'account_sid, auth_token, sender_cell, receiver_cell, receive_text, receive_call and expected_receive_period_in_days are all required')
+        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
 
@@ -66,15 +67,15 @@ module Agents
     end
 
     def send_message(message)
-      client.messages.create :from => interpolated['sender_cell'],
-                                         :to => interpolated['receiver_cell'],
-                                         :body => message
+      client.messages.create from: interpolated['sender_cell'],
+                             to: interpolated['receiver_cell'],
+                             body: message
     end
 
     def make_call(secret)
-      client.calls.create :from => interpolated['sender_cell'],
-                                  :to => interpolated['receiver_cell'],
-                                  :url => post_url(interpolated['server_url'], secret)
+      client.calls.create from: interpolated['sender_cell'],
+                          to: interpolated['receiver_cell'],
+                          url: post_url(interpolated['server_url'], secret)
     end
 
     def post_url(server_url, secret)
@@ -83,7 +84,9 @@ module Agents
 
     def receive_web_request(params, method, format)
       if memory['pending_calls'].has_key? params['secret']
-        response = Twilio::TwiML::VoiceResponse.new {|r| r.say( message: memory['pending_calls'][params['secret']], voice: 'woman')}
+        response = Twilio::TwiML::VoiceResponse.new { |r|
+          r.say(message: memory['pending_calls'][params['secret']], voice: 'woman')
+        }
         memory['pending_calls'].delete params['secret']
         [response.to_s, 200]
       end

+ 18 - 18
app/models/agents/twilio_receive_text_agent.rb

@@ -5,20 +5,21 @@ module Agents
 
     gem_dependency_check { defined?(Twilio) }
 
-    description do <<-MD
-      The Twilio Receive Text Agent receives text messages from Twilio and emits them as events.
+    description do
+      <<~MD
+        The Twilio Receive Text Agent receives text messages from Twilio and emits them as events.
 
-      #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
+        #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
 
-      In order to create events with this agent, configure Twilio to send POST requests to:
+        In order to create events with this agent, configure Twilio to send POST requests to:
 
-      ```
-      #{post_url}
-      ```
+        ```
+        #{post_url}
+        ```
 
-      #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
+        #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
 
-      Options:
+        Options:
 
         * `server_url` must be set to the URL of your
         Huginn installation (probably "https://#{ENV['DOMAIN']}"), which must be web-accessible.  Be sure to set http/https correctly.
@@ -35,8 +36,8 @@ module Agents
       {
         'account_sid' => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
         'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
-        'server_url'    => "https://#{ENV['DOMAIN'].presence || 'example.com'}",
-        'reply_text'    => '',
+        'server_url' => "https://#{ENV['DOMAIN'].presence || 'example.com'}",
+        'reply_text' => '',
         "expected_receive_period_in_days" => 1
       }
     end
@@ -73,11 +74,10 @@ module Agents
       # validate from twilio
       @validator ||= Twilio::Security::RequestValidator.new interpolated['auth_token']
       if !@validator.validate(post_url, params, signature)
-        error("Twilio Signature Failed to Validate\n\n"+
-          "URL: #{post_url}\n\n"+
-          "POST params: #{params.inspect}\n\n"+
-          "Signature: #{signature}"
-          )
+        error("Twilio Signature Failed to Validate\n\n" +
+          "URL: #{post_url}\n\n" +
+          "POST params: #{params.inspect}\n\n" +
+          "Signature: #{signature}")
         return ["Not authorized", 401]
       end
 
@@ -87,9 +87,9 @@ module Agents
             r.message(body: interpolated['reply_text'])
           end
         end
-        return [response.to_s, 200, "text/xml"]
+        [response.to_s, 200, "text/xml"]
       else
-        return ["Bad request", 400]
+        ["Bad request", 400]
       end
     end
   end

+ 1 - 1
app/models/agents/twitter_action_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Twitter Action Agent is able to retweet or favorite tweets from the events it receives.
 
       #{twitter_dependencies_missing if dependencies_missing?}

+ 1 - 1
app/models/agents/twitter_favorites.rb

@@ -5,7 +5,7 @@ module Agents
     can_dry_run!
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Twitter Favorites List Agent follows the favorites list of a specified Twitter user.
 
       #{twitter_dependencies_missing if dependencies_missing?}

+ 22 - 20
app/models/agents/twitter_publish_agent.rb

@@ -4,7 +4,7 @@ module Agents
 
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Twitter Publish Agent publishes tweets from the events it receives.
 
       #{twitter_dependencies_missing if dependencies_missing?}
@@ -19,32 +19,34 @@ module Agents
       If `output_mode` is set to `merge`, the emitted Event will be merged into the original contents of the received Event.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
-        {
-          "success": true,
-          "published_tweet": "...",
-          "tweet_id": ...,
-          "tweet_url": "...",
-          "agent_id": ...,
-          "event_id": ...
-        }
-
-        {
-          "success": false,
-          "error": "...",
-          "failed_tweet": "...",
-          "agent_id": ...,
-          "event_id": ...
-        }
+
+          {
+            "success": true,
+            "published_tweet": "...",
+            "tweet_id": ...,
+            "tweet_url": "...",
+            "agent_id": ...,
+            "event_id": ...
+          }
+
+          {
+            "success": false,
+            "error": "...",
+            "failed_tweet": "...",
+            "agent_id": ...,
+            "event_id": ...
+          }
 
       Original event contents will be merged when `output_mode` is set to `merge`.
     MD
 
     def validate_options
-      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
 
-      if options['output_mode'].present? && !options['output_mode'].to_s.include?('{') && !%[clean merge].include?(options['output_mode'].to_s)
+      if options['output_mode'].present? && !options['output_mode'].to_s.include?('{') && !%(clean merge).include?(options['output_mode'].to_s)
         errors.add(:base, "if provided, output_mode must be 'clean' or 'merge'")
       end
     end

+ 1 - 1
app/models/agents/twitter_search_agent.rb

@@ -5,7 +5,7 @@ module Agents
     can_dry_run!
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Twitter Search Agent performs and emits the results of a specified Twitter search.
 
       #{twitter_dependencies_missing if dependencies_missing?}

+ 5 - 5
app/models/agents/twitter_stream_agent.rb

@@ -5,7 +5,7 @@ module Agents
 
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Twitter Stream Agent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide.
 
       #{twitter_dependencies_missing if dependencies_missing?}
@@ -147,7 +147,7 @@ module Agents
         config_hash.push(oauth_token)
 
         Worker.new(id: agents.first.worker_id(config_hash),
-                   config: { filter_to_agent_map: filter_to_agent_map },
+                   config: { filter_to_agent_map: },
                    agent: agents.first)
       end
     end
@@ -155,7 +155,7 @@ module Agents
     class Worker < LongRunnable::Worker
       RELOAD_TIMEOUT = 60.minutes
       DUPLICATE_DETECTION_LENGTH = 1000
-      SEPARATOR = /[^\w_-]+/
+      SEPARATOR = /[^\w-]+/
 
       def setup
         require 'twitter/json_stream'
@@ -187,13 +187,13 @@ module Agents
 
         path =
           if track.present?
-            "/1.1/statuses/filter.json?#{{ track: track }.to_param}"
+            "/1.1/statuses/filter.json?#{{ track: }.to_param}"
           else
             "/1.1/statuses/sample.json"
           end
 
         stream = Twitter::JSONStream.connect(
-          path: path,
+          path:,
           ssl: true,
           oauth: {
             consumer_key: agent.twitter_consumer_key,

+ 1 - 1
app/models/agents/twitter_user_agent.rb

@@ -5,7 +5,7 @@ module Agents
     can_dry_run!
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Twitter User Agent either follows the timeline of a specific Twitter user or follows your own home timeline including both your tweets and tweets from people whom you are following.
 
       #{twitter_dependencies_missing if dependencies_missing?}

+ 19 - 12
app/models/agents/user_location_agent.rb

@@ -6,20 +6,21 @@ module Agents
 
     gem_dependency_check { defined?(Haversine) }
 
-    description do <<-MD
-      The User Location Agent creates events based on WebHook POSTS that contain a `latitude` and `longitude`.  You can use the [POSTLocation](https://github.com/cantino/post_location) or [PostGPS](https://github.com/chriseidhof/PostGPS) iOS app to post your location to `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options.
+    description do
+      <<~MD
+        The User Location Agent creates events based on WebHook POSTS that contain a `latitude` and `longitude`.  You can use the [POSTLocation](https://github.com/cantino/post_location) or [PostGPS](https://github.com/chriseidhof/PostGPS) iOS app to post your location to `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options.
 
-      #{'## Include `haversine` in your Gemfile to use this Agent!' if dependencies_missing?}
+        #{'## Include `haversine` in your Gemfile to use this Agent!' if dependencies_missing?}
 
-      If you want to only keep more precise locations, set `max_accuracy` to the upper bound, in meters. The default name for this field is `accuracy`, but you can change this by setting a value for `accuracy_field`.
+        If you want to only keep more precise locations, set `max_accuracy` to the upper bound, in meters. The default name for this field is `accuracy`, but you can change this by setting a value for `accuracy_field`.
 
-      If you want to require a certain distance traveled, set `min_distance` to the minimum distance, in meters. Note that GPS readings and the measurement itself aren't exact, so don't rely on this for precision filtering.
+        If you want to require a certain distance traveled, set `min_distance` to the minimum distance, in meters. Note that GPS readings and the measurement itself aren't exact, so don't rely on this for precision filtering.
 
-      To view the locations on a map, set `api_key` to your [Google Maps JavaScript API key](https://developers.google.com/maps/documentation/javascript/get-api-key#key).
-    MD
+        To view the locations on a map, set `api_key` to your [Google Maps JavaScript API key](https://developers.google.com/maps/documentation/javascript/get-api-key#key).
+      MD
     end
 
-    event_description <<-MD
+    event_description <<~MD
       Assuming you're using the iOS application, events look like this:
 
           {
@@ -49,7 +50,8 @@ module Agents
     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
 
     def receive(incoming_events)
@@ -71,7 +73,7 @@ module Agents
 
       handle_payload params.except(:secret)
 
-      return ['ok', 200]
+      ['ok', 200]
     end
 
     private
@@ -87,7 +89,12 @@ module Agents
 
       def far_enough?(payload)
         if memory['last_location'].present?
-          travel = Haversine.distance(memory['last_location']['latitude'].to_i, memory['last_location']['longitude'].to_i, payload['latitude'].to_i, payload['longitude'].to_i).to_meters
+          travel = Haversine.distance(
+            memory['last_location']['latitude'].to_i,
+            memory['last_location']['longitude'].to_i,
+            payload['latitude'].to_i,
+            payload['longitude'].to_i
+          ).to_meters
           !interpolated[:min_distance].present? || travel > interpolated[:min_distance].to_i
         else # for the first run, before "last_location" exists
           true
@@ -98,7 +105,7 @@ module Agents
         if interpolated[:max_accuracy].present? && !payload[accuracy_field].present?
           log "Accuracy field missing; all locations will be kept"
         end
-        create_event payload: payload, location: location
+        create_event(payload:, location:)
         memory["last_location"] = payload
       end
     end

+ 20 - 20
app/models/agents/weather_agent.rb

@@ -7,24 +7,23 @@ module Agents
 
     gem_dependency_check { defined?(ForecastIO) }
 
-    description <<-MD
+    description <<~MD
       The Weather Agent creates an event for the day's weather at a given `location`.
 
       #{'## Include `forecast_io` in your Gemfile to use this Agent!' if dependencies_missing?}
 
       You also must select when you would like to get the weather forecast for using the `which_day` option, where the number 1 represents today, 2 represents tomorrow and so on. Weather forecast inforation is only returned for at most one week at a time.
 
-      The weather forecast information is provided by Dark Sky. 
+      The weather forecast information is provided by Dark Sky.
 
       The `location` must be a comma-separated string of map co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`.
 
       You must set up an [API key for Dark Sky](https://darksky.net/dev/) in order to use this Agent.
 
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
-
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events look like this:
 
           {
@@ -71,7 +70,7 @@ module Agents
 
     def check
       if key_setup?
-        create_event :payload => model(which_day).merge('location' => location)
+        create_event payload: model(which_day).merge('location' => location)
       end
     end
 
@@ -93,7 +92,7 @@ module Agents
       interpolated["language"].presence || "en"
     end
 
-    def wunderground? 
+    def wunderground?
       interpolated["service"].presence && interpolated["service"].presence.downcase == "wunderground"
     end
 
@@ -112,12 +111,14 @@ module Agents
           :base,
           "Location #{location} is malformed. Location for " +
           'Dark Sky must be in the format "-00.000,-00.00000". The ' +
-          "number of decimal places does not matter.")
+          "number of decimal places does not matter."
+        )
       end
     end
 
     def validate_options
-      errors.add(:base, "The Weather Underground API has been disabled since Jan 1st 2018, please switch to DarkSky") if wunderground?
+      errors.add(:base,
+                 "The Weather Underground API has been disabled since Jan 1st 2018, please switch to DarkSky") if wunderground?
       validate_location
       errors.add(:base, "api_key is required") unless interpolated['api_key'].present?
       errors.add(:base, "which_day selection is required") unless which_day.present?
@@ -127,7 +128,7 @@ module Agents
       if key_setup?
         ForecastIO.api_key = interpolated['api_key']
         lat, lng = coordinates
-        ForecastIO.forecast(lat, lng, params: {lang: language.downcase})['daily']['data']
+        ForecastIO.forecast(lat, lng, params: { lang: language.downcase })['daily']['data']
       end
     end
 
@@ -135,7 +136,7 @@ module Agents
       value = dark_sky[which_day - 1]
       if value
         timestamp = Time.at(value.time)
-        day = {
+        {
           'date' => {
             'epoch' => value.time.to_s,
             'pretty' => timestamp.strftime("%l:%M %p %Z on %B %d, %Y"),
@@ -146,7 +147,7 @@ module Agents
             'hour' => timestamp.hour,
             'min' => timestamp.strftime("%M"),
             'sec' => timestamp.sec,
-            'isdst' => timestamp.isdst ? 1 : 0 ,
+            'isdst' => timestamp.isdst ? 1 : 0,
             'monthname' => timestamp.strftime("%B"),
             'monthname_short' => timestamp.strftime("%b"),
             'weekday_short' => timestamp.strftime("%a"),
@@ -156,18 +157,18 @@ module Agents
           },
           'period' => which_day.to_i,
           'high' => {
-            'fahrenheit' => value.temperatureMax.round().to_s,
+            'fahrenheit' => value.temperatureMax.round.to_s,
             'epoch' => value.temperatureMaxTime.to_s,
-            'fahrenheit_apparent' => value.apparentTemperatureMax.round().to_s,
+            'fahrenheit_apparent' => value.apparentTemperatureMax.round.to_s,
             'epoch_apparent' => value.apparentTemperatureMaxTime.to_s,
-            'celsius' => ((5*(Float(value.temperatureMax) - 32))/9).round().to_s
+            'celsius' => ((5 * (Float(value.temperatureMax) - 32)) / 9).round.to_s
           },
           'low' => {
-            'fahrenheit' => value.temperatureMin.round().to_s,
+            'fahrenheit' => value.temperatureMin.round.to_s,
             'epoch' => value.temperatureMinTime.to_s,
-            'fahrenheit_apparent' => value.apparentTemperatureMin.round().to_s,
+            'fahrenheit_apparent' => value.apparentTemperatureMin.round.to_s,
             'epoch_apparent' => value.apparentTemperatureMinTime.to_s,
-            'celsius' => ((5*(Float(value.temperatureMin) - 32))/9).round().to_s
+            'celsius' => ((5 * (Float(value.temperatureMin) - 32)) / 9).round.to_s
           },
           'conditions' => value.summary,
           'icon' => value.icon,
@@ -184,8 +185,8 @@ module Agents
           },
           'dewPoint' => value.dewPoint.to_s,
           'avewind' => {
-            'mph' => value.windSpeed.round().to_s,
-            'kph' =>  (Float(value.windSpeed) * 1.609344).round().to_s,
+            'mph' => value.windSpeed.round.to_s,
+            'kph' => (Float(value.windSpeed) * 1.609344).round.to_s,
             'degrees' => value.windBearing.to_s
           },
           'visibility' => value.visibility.to_s,
@@ -193,7 +194,6 @@ module Agents
           'pressure' => value.pressure.to_s,
           'ozone' => value.ozone.to_s
         }
-        return day
       end
     end
   end

+ 22 - 12
app/models/agents/webhook_agent.rb

@@ -6,16 +6,17 @@ module Agents
     cannot_be_scheduled!
     cannot_receive_events!
 
-    description do <<-MD
-      The Webhook Agent will create events by receiving webhooks from any source. In order to create events with this agent, make a POST request to:
+    description do
+      <<~MD
+        The Webhook Agent will create events by receiving webhooks from any source. In order to create events with this agent, make a POST request to:
 
-      ```
-         https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || ':id'}/#{options['secret'] || ':secret'}
-      ```
+        ```
+        https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || ':id'}/#{options['secret'] || ':secret'}
+        ```
 
-      #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
+        #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
 
-      Options:
+        Options:
 
         * `secret` - A token that the host will provide for authentication.
         * `expected_receive_period_in_days` - How often you expect to receive
@@ -38,14 +39,15 @@ module Agents
     end
 
     event_description do
-      <<-MD
+      <<~MD
         The event payload is based on the value of the `payload_path` option,
         which is set to `#{interpolated['payload_path']}`.
       MD
     end
 
     def default_options
-      { "secret" => "supersecretstring",
+      {
+        "secret" => "supersecretstring",
         "expected_receive_period_in_days" => 1,
         "payload_path" => "some_key",
         "event_headers" => "",
@@ -97,7 +99,7 @@ module Agents
         begin
           response = faraday.post('https://www.google.com/recaptcha/api/siteverify',
                                   parameters)
-        rescue => e
+        rescue StandardError => e
           error "Verification failed: #{e.message}"
           return ["Not Authorized", 401]
         end
@@ -117,9 +119,17 @@ module Agents
       end
 
       if interpolated['response_headers'].presence
-        [interpolated(params)['response'] || 'Event Created', code, "text/plain", interpolated['response_headers'].presence]
+        [
+          interpolated(params)['response'] || 'Event Created',
+          code,
+          "text/plain",
+          interpolated['response_headers'].presence
+        ]
       else
-        [interpolated(params)['response'] || 'Event Created', code]
+        [
+          interpolated(params)['response'] || 'Event Created',
+          code
+        ]
       end
     end
 

+ 62 - 45
app/models/agents/website_agent.rb

@@ -14,7 +14,7 @@ module Agents
     UNIQUENESS_LOOK_BACK = 200
     UNIQUENESS_FACTOR = 3
 
-    description <<-MD
+    description <<~MD
       The Website Agent scrapes a website, XML document, or JSON feed and creates Events based on the results.
 
       Specify a `url` and select a `mode` for when to create Events based on the scraped data, either `all`, `on_change`, or `merge` (if fetching based on an Event, see below).
@@ -224,37 +224,42 @@ module Agents
 
     def default_options
       {
-          'expected_update_period_in_days' => "2",
-          'url' => "https://xkcd.com",
-          'type' => "html",
-          'mode' => "on_change",
-          'extract' => {
-            'url' => { 'css' => "#comic img", 'value' => "@src" },
-            'title' => { 'css' => "#comic img", 'value' => "@alt" },
-            'hovertext' => { 'css' => "#comic img", 'value' => "@title" }
-          }
+        'expected_update_period_in_days' => "2",
+        'url' => "https://xkcd.com",
+        'type' => "html",
+        'mode' => "on_change",
+        'extract' => {
+          'url' => { 'css' => "#comic img", 'value' => "@src" },
+          'title' => { 'css' => "#comic img", 'value' => "@alt" },
+          'hovertext' => { 'css' => "#comic img", 'value' => "@title" }
+        }
       }
     end
 
     def validate_options
       # Check for required fields
-      errors.add(:base, "either url, url_from_event, or data_from_event are required") unless options['url'].present? || options['url_from_event'].present? || options['data_from_event'].present?
-      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
+      errors.add(:base,
+                 "either url, url_from_event, or data_from_event are required") unless options['url'].present? || options['url_from_event'].present? || options['data_from_event'].present?
+      errors.add(:base,
+                 "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
       validate_extract_options!
       validate_template_options!
       validate_http_success_codes!
 
       # Check for optional fields
       if options['mode'].present?
-        errors.add(:base, "mode must be set to on_change, all or merge") unless %w[on_change all merge].include?(options['mode'])
+        errors.add(:base, "mode must be set to on_change, all or merge") unless %w[on_change all
+                                                                                   merge].include?(options['mode'])
       end
 
       if options['expected_update_period_in_days'].present?
-        errors.add(:base, "Invalid expected_update_period_in_days format") unless is_positive_integer?(options['expected_update_period_in_days'])
+        errors.add(:base,
+                   "Invalid expected_update_period_in_days format") unless is_positive_integer?(options['expected_update_period_in_days'])
       end
 
       if options['uniqueness_look_back'].present?
-        errors.add(:base, "Invalid uniqueness_look_back format") unless is_positive_integer?(options['uniqueness_look_back'])
+        errors.add(:base,
+                   "Invalid uniqueness_look_back format") unless is_positive_integer?(options['uniqueness_look_back'])
       end
 
       validate_web_request_options!
@@ -264,23 +269,24 @@ module Agents
       consider_success = options["http_success_codes"]
       if consider_success.present?
 
-        if (consider_success.class != Array)
+        if consider_success.class != Array
           errors.add(:http_success_codes, "must be an array and specify at least one status code")
-        else
-          if consider_success.uniq.count != consider_success.count
-            errors.add(:http_success_codes, "duplicate http code found")
-          else
-            if consider_success.any?{|e| e.to_s !~ /^\d+$/ }
-              errors.add(:http_success_codes, "please make sure to use only numeric values for code, ex 404, or \"404\"")
-            end
-          end
+        elsif consider_success.uniq.count != consider_success.count
+          errors.add(:http_success_codes, "duplicate http code found")
+        elsif consider_success.any? { |e| e.to_s !~ /^\d+$/ }
+          errors.add(:http_success_codes,
+                     "please make sure to use only numeric values for code, ex 404, or \"404\"")
         end
 
       end
     end
 
     def validate_extract_options!
-      extraction_type = (extraction_type() rescue extraction_type(options))
+      extraction_type = begin
+        extraction_type()
+      rescue StandardError
+        extraction_type(options)
+      end
       case extract = options['extract']
       when Hash
         if extract.each_value.any? { |value| !value.is_a?(Hash) }
@@ -297,7 +303,8 @@ module Agents
                 when String
                   # ok
                 when nil
-                  errors.add(:base, "When type is html or xml, all extractions must have a css or xpath attribute (bad extraction details for #{name.inspect})")
+                  errors.add(:base,
+                             "When type is html or xml, all extractions must have a css or xpath attribute (bad extraction details for #{name.inspect})")
                 else
                   errors.add(:base, "Wrong type of \"xpath\" value in extraction details for #{name.inspect}")
                 end
@@ -318,7 +325,8 @@ module Agents
               when String
                 # ok
               when nil
-                errors.add(:base, "When type is json, all extractions must have a path attribute (bad extraction details for #{name.inspect})")
+                errors.add(:base,
+                           "When type is json, all extractions must have a path attribute (bad extraction details for #{name.inspect})")
               else
                 errors.add(:base, "Wrong type of \"path\" value in extraction details for #{name.inspect}")
               end
@@ -329,11 +337,12 @@ module Agents
               when String
                 begin
                   re = Regexp.new(regexp)
-                rescue => e
+                rescue StandardError => e
                   errors.add(:base, "invalid regexp for #{name.inspect}: #{e.message}")
                 end
               when nil
-                errors.add(:base, "When type is text, all extractions must have a regexp attribute (bad extraction details for #{name.inspect})")
+                errors.add(:base,
+                           "When type is text, all extractions must have a regexp attribute (bad extraction details for #{name.inspect})")
               else
                 errors.add(:base, "Wrong type of \"regexp\" value in extraction details for #{name.inspect}")
               end
@@ -346,7 +355,8 @@ module Agents
                   errors.add(:base, "no named capture #{index.inspect} found in regexp for #{name.inspect})")
                 end
               when nil
-                errors.add(:base, "When type is text, all extractions must have an index attribute (bad extraction details for #{name.inspect})")
+                errors.add(:base,
+                           "When type is text, all extractions must have an index attribute (bad extraction details for #{name.inspect})")
               else
                 errors.add(:base, "Wrong type of \"index\" value in extraction details for #{name.inspect}")
               end
@@ -369,8 +379,7 @@ module Agents
     def validate_template_options!
       template = options['template'].presence or return
 
-      unless Hash === template &&
-             template.each_pair.all? { |key, value| String === value }
+      unless Hash === template && template.each_key.all?(String)
         errors.add(:base, 'template must be a hash of strings.')
       end
     end
@@ -403,7 +412,7 @@ module Agents
         interpolation_context['_response_'] = ResponseDrop.new(response)
         handle_data(response.body, response.env[:url], existing_payload)
       }
-    rescue => e
+    rescue StandardError => e
       error "Error when fetching url: #{e.message}\n#{e.backtrace.join("\n")}"
     end
 
@@ -432,12 +441,12 @@ module Agents
 
       output =
         case extraction_type
-          when 'json'
-            extract_json(doc)
-          when 'text'
-            extract_text(doc)
-          else
-            extract_xml(doc)
+        when 'json'
+          extract_json(doc)
+        when 'text'
+          extract_text(doc)
+        else
+          extract_xml(doc)
         end
 
       num_tuples = output.size or
@@ -485,6 +494,7 @@ module Agents
     end
 
     private
+
     def consider_response_successful?(response)
       response.success? || begin
         consider_success = options["http_success_codes"]
@@ -497,7 +507,7 @@ module Agents
         interpolation_context['_response_'] = ResponseFromEventDrop.new(event)
         handle_data(data, event.payload['url'].presence, existing_payload)
       }
-    rescue => e
+    rescue StandardError => e
       error "Error when handling event data: #{e.message}\n#{e.backtrace.join("\n")}"
     end
 
@@ -557,7 +567,7 @@ module Agents
       if interpolated.key?('use_namespaces')
         boolify(interpolated['use_namespaces'])
       else
-        interpolated['extract'].none? { |name, extraction_details|
+        interpolated['extract'].none? { |_name, extraction_details|
           extraction_details.key?('xpath')
         }
       end
@@ -620,7 +630,7 @@ module Agents
         log "Extracting #{extraction_type} at #{xpath || css}"
         case nodes
         when Nokogiri::XML::NodeSet
-          stringified_nodes  = nodes.map do |node|
+          stringified_nodes = nodes.map do |node|
             case value = node.xpath(extraction_details['value'] || '.')
             when Float
               # Node#xpath() returns any numeric value as float;
@@ -677,6 +687,7 @@ module Agents
           if @size && @size != size
             raise UnevenSizeError, 'got an uneven size'
           end
+
           @size = size
         end
 
@@ -684,7 +695,7 @@ module Agents
       end
 
       def each
-        @size.times.zip(*@hash.values) do |index, *values|
+        @size.times.zip(*@hash.values) do |_index, *values|
           yield @hash.each_key.lazy.zip(values).to_h
         end
       end
@@ -734,14 +745,20 @@ module Agents
 
     class ResponseFromEventDrop < LiquidDroppable::Drop
       def headers
-        headers = Faraday::Utils::Headers.from(@object.payload[:headers]) rescue {}
+        headers = begin
+          Faraday::Utils::Headers.from(@object.payload[:headers])
+        rescue StandardError
+          {}
+        end
 
         HeaderDrop.new(headers)
       end
 
       # Integer value of HTTP status
       def status
-        Integer(@object.payload[:status]) rescue nil
+        Integer(@object.payload[:status])
+      rescue StandardError
+        nil
       end
 
       # The URL

+ 15 - 19
app/models/agents/weibo_publish_agent.rb

@@ -1,12 +1,10 @@
-# encoding: utf-8
-
 module Agents
   class WeiboPublishAgent < Agent
     include WeiboConcern
 
     cannot_be_scheduled!
 
-    description <<-MD
+    description <<~MD
       The Weibo Publish Agent publishes tweets from the events it receives.
 
       #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -24,7 +22,7 @@ module Agents
 
     def validate_options
       unless options['uid'].present? &&
-             options['expected_update_period_in_days'].present?
+          options['expected_update_period_in_days'].present?
         errors.add(:base, "expected_update_period_in_days and uid are required")
       end
     end
@@ -62,7 +60,7 @@ module Agents
           else
             publish_tweet tweet_text
           end
-          create_event :payload => {
+          create_event payload: {
             'success' => true,
             'published_tweet' => tweet_text,
             'published_pic' => pic_url,
@@ -70,7 +68,7 @@ module Agents
             'event_id' => event.id
           }
         rescue OAuth2::Error => e
-          create_event :payload => {
+          create_event payload: {
             'success' => false,
             'error' => e.message,
             'failed_tweet' => tweet_text,
@@ -84,29 +82,27 @@ module Agents
       end
     end
 
-    def publish_tweet text
+    def publish_tweet(text)
       weibo_client.statuses.update text
     end
 
-    def publish_tweet_with_pic text, pic
+    def publish_tweet_with_pic(text, pic)
       weibo_client.statuses.upload text, open(pic)
     end
 
     def valid_image?(url)
-      begin
-        url = URI.parse(url)
-        http = Net::HTTP.new(url.host, url.port)
-        http.use_ssl = (url.scheme == "https")
-        http.start do |http|
-          # images supported #http://open.weibo.com/wiki/2/statuses/upload
-          return ['image/gif', 'image/jpeg', 'image/png'].include? http.head(url.request_uri)['Content-Type']
-        end
-      rescue => e
-        return false
+      url = URI.parse(url)
+      http = Net::HTTP.new(url.host, url.port)
+      http.use_ssl = (url.scheme == "https")
+      http.start do |http|
+        # images supported #http://open.weibo.com/wiki/2/statuses/upload
+        return ['image/gif', 'image/jpeg', 'image/png'].include? http.head(url.request_uri)['Content-Type']
       end
+    rescue StandardError => e
+      false
     end
 
-    def unwrap_tco_urls text, tweet_json
+    def unwrap_tco_urls(text, tweet_json)
       tweet_json[:entities][:urls].each do |url|
         text.gsub! url[:url], url[:expanded_url]
       end

+ 7 - 10
app/models/agents/weibo_user_agent.rb

@@ -1,12 +1,10 @@
-# encoding: utf-8 
-
 module Agents
   class WeiboUserAgent < Agent
     include WeiboConcern
 
     cannot_receive_events!
 
-    description <<-MD
+    description <<~MD
       The Weibo User Agent follows the timeline of a specified Weibo user. It uses this endpoint: http://open.weibo.com/wiki/2/statuses/user_timeline/en
 
       #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?}
@@ -18,7 +16,7 @@ module Agents
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
     MD
 
-    event_description <<-MD
+    event_description <<~MD
       Events are the raw JSON provided by the Weibo API. Should look something like:
 
           {
@@ -72,7 +70,7 @@ module Agents
 
     def validate_options
       unless options['uid'].present? &&
-             options['expected_update_period_in_days'].present?
+          options['expected_update_period_in_days'].present?
         errors.add(:base, "expected_update_period_in_days and uid are required")
       end
     end
@@ -93,22 +91,21 @@ module Agents
 
     def check
       since_id = memory['since_id'] || nil
-      opts = {:uid => interpolated['uid'].to_i}
-      opts.merge! :since_id => since_id unless since_id.nil?
+      opts = { uid: interpolated['uid'].to_i }
+      opts.merge! since_id: since_id unless since_id.nil?
 
       # http://open.weibo.com/wiki/2/statuses/user_timeline/en
       resp = weibo_client.statuses.user_timeline opts
       if resp[:statuses]
 
-
         resp[:statuses].each do |status|
           memory['since_id'] = status.id if !memory['since_id'] || (status.id > memory['since_id'])
 
-          create_event :payload => status.as_json
+          create_event payload: status.as_json
         end
       end
 
       save!
     end
   end
-end
+end

+ 36 - 36
app/models/agents/witai_agent.rb

@@ -3,43 +3,42 @@ module Agents
     cannot_be_scheduled!
     no_bulk_receive!
 
-    description <<-MD
+    description <<~MD
       The `wit.ai` agent receives events, sends a text query to your `wit.ai` instance and generates outcome events.
 
       Fill in `Server Access Token` of your `wit.ai` instance. Use [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) to fill query field.
-      
+
       `expected_receive_period_in_days` is the expected number of days by which agent should receive events. It helps in determining if the agent is working.
     MD
 
-    event_description <<-MD
-
-    Every event have `outcomes` key with your payload as value. Sample event:
+    event_description <<~MD
+      Every event have `outcomes` key with your payload as value. Sample event:
 
-        {"outcome" : [
-          {"_text" : "set temperature to 34 degrees at 11 PM",
-           "intent" : "get_temperature",
-           "entities" : {
-             "temperature" : [
-             {
-               "type" : "value",
-               "value" : 34,
-               "unit" : "degree"
-             }],
-             "datetime" : [
-             {
-               "grain" : "hour",
-               "type" : "value",
-               "value" : "2015-03-26T21:00:00.000-07:00"
-             }]},
-             "confidence" : 0.556
-           }]}
+          {"outcome" : [
+            {"_text" : "set temperature to 34 degrees at 11 PM",
+             "intent" : "get_temperature",
+             "entities" : {
+               "temperature" : [
+               {
+                 "type" : "value",
+                 "value" : 34,
+                 "unit" : "degree"
+               }],
+               "datetime" : [
+               {
+                 "grain" : "hour",
+                 "type" : "value",
+                 "value" : "2015-03-26T21:00:00.000-07:00"
+               }]},
+               "confidence" : 0.556
+             }]}
     MD
 
     def default_options
       {
-       'server_access_token' => 'xxxxx',
-       'expected_receive_period_in_days' => 2,
-       'query' => '{{xxxx}}'
+        'server_access_token' => 'xxxxx',
+        'expected_receive_period_in_days' => 2,
+        'query' => '{{xxxx}}'
       }
     end
 
@@ -64,17 +63,18 @@ module Agents
     end
 
     private
-      def api_endpoint
-        'https://api.wit.ai/message?v=20141022'
-      end
 
-      def query_url(query)
-        api_endpoint + { q: query }.to_query
-      end
+    def api_endpoint
+      'https://api.wit.ai/message?v=20141022'
+    end
 
-      def headers
-        #oauth
-        {:headers => {'Authorization' => 'Bearer ' + interpolated[:server_access_token]}}
-      end
+    def query_url(query)
+      api_endpoint + { q: query }.to_query
+    end
+
+    def headers
+      # oauth
+      { headers: { 'Authorization' => 'Bearer ' + interpolated[:server_access_token] } }
+    end
   end
 end

+ 26 - 20
app/models/event.rb

@@ -12,10 +12,12 @@ class Event < ActiveRecord::Base
   json_serialize :payload
 
   belongs_to :user, optional: true
-  belongs_to :agent, :counter_cache => true
+  belongs_to :agent, counter_cache: true
 
-  has_many :agent_logs_as_inbound_event, :class_name => "AgentLog", :foreign_key => :inbound_event_id, :dependent => :nullify
-  has_many :agent_logs_as_outbound_event, :class_name => "AgentLog", :foreign_key => :outbound_event_id, :dependent => :nullify
+  has_many :agent_logs_as_inbound_event, class_name: "AgentLog", foreign_key: :inbound_event_id,
+                                         dependent: :nullify
+  has_many :agent_logs_as_outbound_event, class_name: "AgentLog", foreign_key: :outbound_event_id,
+                                          dependent: :nullify
 
   scope :recent, lambda { |timespan = 12.hours.ago|
     where("events.created_at > ?", timespan)
@@ -44,20 +46,18 @@ class Event < ActiveRecord::Base
   def location
     @location ||= Location.new(
       # lat and lng are BigDecimal, but converted to Float by the Location class
-      lat: lat,
-      lng: lng,
+      lat:,
+      lng:,
       radius:
-        begin
-          h = payload[:horizontal_accuracy].presence
-          v = payload[:vertical_accuracy].presence
-          if h && v
-            (h.to_f + v.to_f) / 2
-          else
-            (h || v || payload[:accuracy]).to_f
-          end
+        if (h = payload[:horizontal_accuracy].presence) &&
+            (v = payload[:vertical_accuracy].presence)
+          (h.to_f + v.to_f) / 2
+        else
+          (h || v || payload[:accuracy]).to_f
         end,
       course: payload[:course],
-      speed: payload[:speed].presence)
+      speed: payload[:speed].presence
+    )
   end
 
   def location=(location)
@@ -69,13 +69,14 @@ class Event < ActiveRecord::Base
     else
       location = Location.new(location)
     end
-    self.lat, self.lng = location.lat, location.lng
+    self.lat = location.lat
+    self.lng = location.lng
     location
   end
 
   # Emit this event again, as a new Event.
   def reemit!
-    agent.create_event :payload => payload, :lat => lat, :lng => lng
+    agent.create_event(payload:, lat:, lng:)
   end
 
   # Look for Events whose `expires_at` is present and in the past.  Remove those events and then update affected Agents'
@@ -95,9 +96,9 @@ class Event < ActiveRecord::Base
   end
 
   def possibly_propagate
-    #immediately schedule agents that want immediate updates
-    propagate_ids = agent.receivers.where(:propagate_immediately => true).pluck(:id)
-    Agent.receive!(:only_receivers => propagate_ids) unless propagate_ids.empty?
+    # immediately schedule agents that want immediate updates
+    propagate_ids = agent.receivers.where(propagate_immediately: true).pluck(:id)
+    Agent.receive!(only_receivers: propagate_ids) unless propagate_ids.empty?
   end
 end
 
@@ -132,6 +133,11 @@ class EventDrop
   end
 
   def as_json
-    {location: _location_.as_json, agent: @object.agent.to_liquid.as_json, payload: @payload.as_json, created_at: created_at.as_json}
+    {
+      location: _location_.as_json,
+      agent: @object.agent.to_liquid.as_json,
+      payload: @payload.as_json,
+      created_at: created_at.as_json
+    }
   end
 end

+ 2 - 2
app/models/link.rb

@@ -1,7 +1,7 @@
 # A Link connects Agents in a directed Event flow from the `source` to the `receiver`.
 class Link < ActiveRecord::Base
-  belongs_to :source, :class_name => "Agent", :inverse_of => :links_as_source
-  belongs_to :receiver, :class_name => "Agent", :inverse_of => :links_as_receiver
+  belongs_to :source, class_name: "Agent", inverse_of: :links_as_source
+  belongs_to :receiver, class_name: "Agent", inverse_of: :links_as_receiver
 
   before_create :store_event_id_at_creation
 

+ 10 - 12
app/models/scenario.rb

@@ -1,16 +1,16 @@
 class Scenario < ActiveRecord::Base
   include HasGuid
 
-  belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios
-  has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario
-  has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios
+  belongs_to :user, counter_cache: :scenario_count, inverse_of: :scenarios
+  has_many :scenario_memberships, dependent: :destroy, inverse_of: :scenario
+  has_many :agents, through: :scenario_memberships, inverse_of: :scenarios
 
   validates_presence_of :name, :user
 
   validates_format_of :tag_fg_color, :tag_bg_color,
-    # Regex adapted from: http://stackoverflow.com/a/1636354/3130625
-    :with => /\A#(?:[0-9a-fA-F]{3}){1,2}\z/, :allow_nil => true,
-    :message => "must be a valid hex color."
+                      # Regex adapted from: http://stackoverflow.com/a/1636354/3130625
+                      with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/, allow_nil: true,
+                      message: "must be a valid hex color."
 
   validate :agents_are_owned
 
@@ -26,18 +26,16 @@ class Scenario < ActiveRecord::Base
   end
 
   def self.icons
-    @icons ||= begin
-      YAML.load_file(Rails.root.join('config/icons.yml'))
-    end
+    @icons ||= YAML.load_file(Rails.root.join('config/icons.yml'))
   end
 
   private
 
   def unique_agent_ids
     agents.joins(:scenario_memberships)
-          .group('scenario_memberships.agent_id')
-          .having('count(scenario_memberships.agent_id) = 1')
-          .pluck('scenario_memberships.agent_id')
+      .group('scenario_memberships.agent_id')
+      .having('count(scenario_memberships.agent_id) = 1')
+      .pluck('scenario_memberships.agent_id')
   end
 
   def agents_are_owned

+ 2 - 2
app/models/scenario_membership.rb

@@ -1,4 +1,4 @@
 class ScenarioMembership < ActiveRecord::Base
-  belongs_to :agent, :inverse_of => :scenario_memberships
-  belongs_to :scenario, :inverse_of => :scenario_memberships
+  belongs_to :agent, inverse_of: :scenario_memberships
+  belongs_to :scenario, inverse_of: :scenario_memberships
 end

+ 17 - 15
app/models/service.rb

@@ -1,8 +1,8 @@
 class Service < ActiveRecord::Base
   serialize :options, Hash
 
-  belongs_to :user, :inverse_of => :services
-  has_many :agents, :inverse_of => :service
+  belongs_to :user, inverse_of: :services
+  has_many :agents, inverse_of: :service
 
   validates_presence_of :user_id, :provider, :name, :token
 
@@ -20,7 +20,7 @@ class Service < ActiveRecord::Base
   end
 
   def toggle_availability!
-    disable_agents(where_not: {user_id: self.user_id}) if global
+    disable_agents(where_not: { user_id: self.user_id }) if global
     self.global = !self.global
     self.save!
   end
@@ -34,20 +34,21 @@ class Service < ActiveRecord::Base
   def refresh_token_parameters
     {
       grant_type: 'refresh_token',
-      client_id:     oauth_key,
+      client_id: oauth_key,
       client_secret: oauth_secret,
-      refresh_token: refresh_token
+      refresh_token:
     }
   end
 
   def refresh_token!
     response = HTTParty.post(endpoint, query: refresh_token_parameters)
     data = JSON.parse(response.body)
-    update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || refresh_token)
+    update(expires_at: Time.now + data['expires_in'], token: data['access_token'],
+           refresh_token: data['refresh_token'].presence || refresh_token)
   end
 
   def endpoint
-    client_options =  Devise.omniauth_configs[provider.to_sym].strategy_class.default_options['client_options']
+    client_options = Devise.omniauth_configs[provider.to_sym].strategy_class.default_options['client_options']
     URI.join(client_options['site'], client_options['token_url'])
   end
 
@@ -63,12 +64,14 @@ class Service < ActiveRecord::Base
     options = get_options(omniauth)
 
     find_or_initialize_by(provider: omniauth['provider'], uid: omniauth['uid'].to_s).tap do |service|
-      service.assign_attributes token: omniauth['credentials']['token'],
-                                secret: omniauth['credentials']['secret'],
-                                name: options[:name],
-                                refresh_token: omniauth['credentials']['refresh_token'],
-                                expires_at: omniauth['credentials']['expires_at'] && Time.at(omniauth['credentials']['expires_at']),
-                                options: options
+      service.attributes = {
+        token: omniauth['credentials']['token'],
+        secret: omniauth['credentials']['secret'],
+        name: options[:name],
+        refresh_token: omniauth['credentials']['refresh_token'],
+        expires_at: omniauth['credentials']['expires_at'] && Time.at(omniauth['credentials']['expires_at']),
+        options:
+      }
     end
   end
 
@@ -80,12 +83,11 @@ class Service < ActiveRecord::Base
     option_providers.fetch(omniauth['provider'], option_providers['default']).call(omniauth)
   end
 
-  private
   @@option_providers = HashWithIndifferentAccess.new
   cattr_reader :option_providers
 
   register_options_provider('default') do |omniauth|
-    {name: omniauth['info']['nickname'] || omniauth['info']['name']}
+    { name: omniauth['info']['nickname'] || omniauth['info']['name'] }
   end
 
   register_options_provider('google') do |omniauth|

+ 32 - 23
app/models/user.rb

@@ -1,10 +1,9 @@
 # Huginn is designed to be a multi-User system.  Users have many Agents (and Events created by those Agents).
 class User < ActiveRecord::Base
-  DEVISE_MODULES = [:database_authenticatable, :registerable,
-                    :recoverable, :rememberable, :trackable,
-                    :validatable, :lockable, :omniauthable,
-                    (ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true' ? :confirmable : nil)].compact
-  devise *DEVISE_MODULES
+  devise :database_authenticatable, :registerable,
+         :recoverable, :rememberable, :trackable,
+         :validatable, :lockable, :omniauthable,
+         *(:confirmable if ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true')
 
   INVITATION_CODES = [ENV['INVITATION_CODE'] || 'try-huginn']
 
@@ -12,17 +11,29 @@ class User < ActiveRecord::Base
   # This is in addition to a real persisted field like 'username'
   attr_accessor :login
 
-  validates_presence_of :username
-  validates :username, uniqueness: { case_sensitive: false }
-  validates_format_of :username, :with => /\A[a-zA-Z0-9_-]{3,190}\Z/, :message => "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 190 characters in length."
-  validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid", if: -> { !requires_no_invitation_code? && User.using_invitation_code? }
-
-  has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
-  has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
-  has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user
-  has_many :logs, :through => :agents, :class_name => "AgentLog"
-  has_many :scenarios, :inverse_of => :user, :dependent => :destroy
-  has_many :services, -> { by_name('asc') }, :dependent => :destroy
+  validates :username,
+            presence: true,
+            uniqueness: { case_sensitive: false },
+            format: {
+              with: /\A[a-zA-Z0-9_-]{3,190}\Z/,
+              message: "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 190 characters in length."
+            }
+  validates :invitation_code,
+            inclusion: {
+              in: INVITATION_CODES,
+              message: "is not valid",
+            },
+            if: -> {
+              !requires_no_invitation_code? && User.using_invitation_code?
+            },
+            on: :create
+
+  has_many :user_credentials, dependent: :destroy, inverse_of: :user
+  has_many :events, -> { order("events.created_at desc") }, dependent: :delete_all, inverse_of: :user
+  has_many :agents, -> { order("agents.created_at desc") }, dependent: :destroy, inverse_of: :user
+  has_many :logs, through: :agents, class_name: "AgentLog"
+  has_many :scenarios, inverse_of: :user, dependent: :destroy
+  has_many :services, -> { by_name('asc') }, dependent: :destroy
 
   def available_services
     Service.available_to_user(self).by_name
@@ -32,7 +43,7 @@ class User < ActiveRecord::Base
   def self.find_first_by_auth_conditions(warden_conditions)
     conditions = warden_conditions.dup
     if login = conditions.delete(:login)
-      where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
+      where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first
     else
       where(conditions).first
     end
@@ -78,12 +89,10 @@ class User < ActiveRecord::Base
 
   def undefined_agent_types
     agents.reorder('').group(:type).pluck(:type).select do |type|
-      begin
-        type.constantize
-        false
-      rescue NameError
-        true
-      end
+      type.constantize
+      false
+    rescue NameError
+      true
     end
   end
 

+ 1 - 1
app/models/user_credential.rb

@@ -3,7 +3,7 @@ class UserCredential < ActiveRecord::Base
 
   belongs_to :user
 
-  validates :credential_name, presence: true, uniqueness: { case_sensitive: true, scope: :user_id}
+  validates :credential_name, presence: true, uniqueness: { case_sensitive: true, scope: :user_id }
   validates :credential_value, presence: true
   validates :mode, inclusion: { in: MODES }
   validates :user_id, presence: true

+ 14 - 8
app/presenters/form_configurable_agent_presenter.rb

@@ -16,14 +16,15 @@ class FormConfigurableAgentPresenter < Decorator
   def option_field_for(attribute)
     data = @agent.form_configurable_fields[attribute]
     value = @agent.options[attribute.to_s] || @agent.default_options[attribute.to_s]
-    html_options = {role: (data[:roles] + ['form-configurable']).join(' '), data: {attribute: attribute}}
+    html_options = { role: (data[:roles] + ['form-configurable']).join(' '), data: { attribute: } }
 
     case data[:type]
     when :text
       @view.content_tag 'div' do
-        @view.concat @view.text_area_tag("agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3))
+        @view.concat @view.text_area_tag("agent[options][#{attribute}]", value,
+                                         html_options.merge(class: 'form-control', rows: 3))
         if data[:ace].present?
-          ace_options = { source: "[name='agent[options][#{attribute}]']", mode: '', theme: ''}.deep_symbolize_keys!
+          ace_options = { source: "[name='agent[options][#{attribute}]']", mode: '', theme: '' }.deep_symbolize_keys!
           ace_options.deep_merge!(data[:ace].deep_symbolize_keys) if data[:ace].is_a?(Hash)
           @view.concat @view.content_tag('div', '', class: 'ace-editor', data: ace_options)
         end
@@ -31,21 +32,26 @@ class FormConfigurableAgentPresenter < Decorator
     when :boolean
       @view.content_tag 'div' do
         @view.concat(@view.content_tag('label', class: 'radio-inline') do
-          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'true', @agent.send(:boolify, value) == true, html_options
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'true',
+                                              @agent.send(:boolify, value) == true, html_options
           @view.concat "True"
         end)
         @view.concat(@view.content_tag('label', class: 'radio-inline') do
-          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'false', @agent.send(:boolify, value) == false, html_options
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'false',
+                                              @agent.send(:boolify, value) == false, html_options
           @view.concat "False"
         end)
         @view.concat(@view.content_tag('label', class: 'radio-inline') do
-          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'manual', @agent.send(:boolify, value) == nil, html_options
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'manual',
+                                              @agent.send(:boolify, value).nil?, html_options
           @view.concat "Manual Input"
         end)
-        @view.concat(@view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}"))
+        @view.concat(@view.text_field_tag("agent[options][#{attribute}]", value,
+                                          html_options.merge(class: "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}")))
       end
     when :array, :string
-      @view.text_field_tag "agent[options][#{attribute}]", value, html_options.deep_merge(:class => 'form-control', data: {cache_response: data[:cache_response] != false})
+      @view.text_field_tag "agent[options][#{attribute}]", value,
+                           html_options.deep_merge(class: 'form-control', data: { cache_response: data[:cache_response] != false })
     end
   end
 end

+ 2 - 1
lib/location.rb

@@ -13,6 +13,7 @@ class Location
     case data
     when Array
       raise ArgumentError, 'unsupported location data' unless data.size == 2
+
       self.lat, self.lng = data
     when Hash, Location
       data.each { |key, value|
@@ -91,7 +92,7 @@ class Location
   def floatify(value)
     case value
     when nil, ''
-      return nil
+      nil
     else
       float = Float(value)
       if block_given?

+ 18 - 17
spec/controllers/agents/dry_runs_controller_spec.rb

@@ -16,7 +16,7 @@ describe Agents::DryRunsController do
 
   describe "GET index" do
     it "does not load any events without specifing sources" do
-      get :index, params: {type: 'Agents::WebsiteAgent', source_ids: []}
+      get :index, params: { type: 'Agents::WebsiteAgent', source_ids: [] }
       expect(assigns(:events)).to eq([])
     end
 
@@ -29,13 +29,13 @@ describe Agents::DryRunsController do
       end
 
       it "for new agents" do
-        get :index, params: {type: 'Agents::WebsiteAgent', source_ids: [@agent.id]}
+        get :index, params: { type: 'Agents::WebsiteAgent', source_ids: [@agent.id] }
         expect(assigns(:events)).to eq([])
       end
 
       it "for existing agents" do
         expect(@agent.events.count).not_to be(0)
-        expect { get :index, params: {agent_id: @agent} }.to raise_error(NoMethodError)
+        expect { get :index, params: { agent_id: @agent } }.to raise_error(NoMethodError)
       end
     end
 
@@ -47,12 +47,12 @@ describe Agents::DryRunsController do
       end
 
       it "load the most recent events when providing source ids" do
-        get :index, params: {type: 'Agents::WebsiteAgent', source_ids: [@agent.id]}
+        get :index, params: { type: 'Agents::WebsiteAgent', source_ids: [@agent.id] }
         expect(assigns(:events)).to eq([@agent.events.first])
       end
 
       it "loads the most recent events for a saved agent" do
-        get :index, params: {agent_id: @agent}
+        get :index, params: { agent_id: @agent }
         expect(assigns(:events)).to eq([@agent.events.first])
       end
     end
@@ -60,12 +60,13 @@ describe Agents::DryRunsController do
 
   describe "POST create" do
     before do
-      stub_request(:any, /xkcd/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), status: 200)
+      stub_request(:any, /xkcd/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/xkcd.html")),
+                                           status: 200)
     end
 
     it "does not actually create any agent, event or log" do
       expect {
-        post :create, params: {agent: valid_attributes}
+        post :create, params: { agent: valid_attributes }
       }.not_to change {
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count]
       }
@@ -81,7 +82,7 @@ describe Agents::DryRunsController do
     it "does not actually update an agent" do
       agent = agents(:bob_weather_agent)
       expect {
-        post :create, params: {agent_id: agent, agent: valid_attributes(name: 'New Name')}
+        post :create, params: { agent_id: agent, agent: valid_attributes(name: 'New Name') }
       }.not_to change {
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at]
       }
@@ -93,7 +94,7 @@ describe Agents::DryRunsController do
       agent.save!
       url_from_event = "http://xkcd.com/?from_event=1".freeze
       expect {
-        post :create, params: {agent_id: agent, event: { url: url_from_event }.to_json}
+        post :create, params: { agent_id: agent.id, event: { url: url_from_event }.to_json }
       }.not_to change {
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at]
       }
@@ -103,25 +104,25 @@ describe Agents::DryRunsController do
 
     it "uses the memory of an existing Agent" do
       valid_params = {
-        :name => "somename",
-        :options => {
-          :code => "Agent.check = function() { this.createEvent({ 'message': this.memory('fu') }); };",
+        name: "somename",
+        options: {
+          code: "Agent.check = function() { this.createEvent({ 'message': this.memory('fu') }); };",
         }
       }
       agent = Agents::JavaScriptAgent.new(valid_params)
-      agent.memory = {fu: "bar"}
+      agent.memory = { fu: "bar" }
       agent.user = users(:bob)
       agent.save!
-      post :create, params: {agent_id: agent, agent: valid_params}
+      post :create, params: { agent_id: agent, agent: valid_params }
       results = assigns(:results)
-      expect(results[:events][0]).to eql({"message" => "bar"})
+      expect(results[:events][0]).to eql({ "message" => "bar" })
     end
 
     it 'sets created_at of the dry-runned event' do
       agent = agents(:bob_formatting_agent)
-      agent.options['instructions'] = {'created_at' => '{{created_at | date: "%a, %b %d, %y"}}'}
+      agent.options['instructions'] = { 'created_at' => '{{created_at | date: "%a, %b %d, %y"}}' }
       agent.save
-      post :create, params: {agent_id: agent, event: {test: 1}.to_json}
+      post :create, params: { agent_id: agent, event: { test: 1 }.to_json }
       results = assigns(:results)
       expect(results[:events]).to be_a(Array)
       expect(results[:events].length).to eq(1)

+ 3 - 1
spec/features/create_an_agent_spec.rb

@@ -114,7 +114,9 @@ describe "Creating a new agent", js: true do
       "expected_receive_period_in_days": "2"
       "keep_event": "false"
     }')
-    expect(get_alert_text_from { click_on "Save" }).to have_text("Sorry, there appears to be an error in your JSON input. Please fix it before continuing.")
+    expect(get_alert_text_from {
+             click_on "Save"
+           }).to have_text("Sorry, there appears to be an error in your JSON input. Please fix it before continuing.")
   end
 
   context "displaying the correct information" do

+ 82 - 72
spec/models/agent_spec.rb

@@ -33,7 +33,7 @@ describe Agent do
 
   describe ".bulk_check" do
     before do
-      @weather_agent_count = Agents::WeatherAgent.where(:schedule => "midnight", :disabled => false).count
+      @weather_agent_count = Agents::WeatherAgent.where(schedule: "midnight", disabled: false).count
     end
 
     it "should run all Agents with the given schedule" do
@@ -142,7 +142,7 @@ describe Agent do
       default_schedule "2pm"
 
       def check
-        create_event :payload => {}
+        create_event payload: {}
       end
 
       def validate_options
@@ -154,8 +154,8 @@ describe Agent do
       cannot_be_scheduled!
 
       def receive(events)
-        events.each do |event|
-          create_event :payload => { :events_received => 1 }
+        events.each do |_event|
+          create_event payload: { events_received: 1 }
         end
       end
     end
@@ -167,7 +167,7 @@ describe Agent do
 
     describe Agents::SomethingSource do
       let(:new_instance) do
-        agent = Agents::SomethingSource.new(:name => "some agent")
+        agent = Agents::SomethingSource.new(name: "some agent")
         agent.user = users(:bob)
         agent
       end
@@ -190,7 +190,7 @@ describe Agent do
       end
 
       it "sets the default on new instances, allows setting new schedules, and prevents invalid schedules" do
-        @checker = Agents::SomethingSource.new(:name => "something")
+        @checker = Agents::SomethingSource.new(name: "something")
         @checker.user = users(:bob)
         expect(@checker.schedule).to eq("2pm")
         @checker.save!
@@ -205,7 +205,7 @@ describe Agent do
       end
 
       it "should have an empty schedule if it cannot_be_scheduled" do
-        @checker = Agents::CannotBeScheduled.new(:name => "something")
+        @checker = Agents::CannotBeScheduled.new(name: "something")
         @checker.user = users(:bob)
         expect(@checker.schedule).to be_nil
         expect(@checker).to be_valid
@@ -221,7 +221,7 @@ describe Agent do
 
     describe "#create_event" do
       before do
-        @checker = Agents::SomethingSource.new(:name => "something")
+        @checker = Agents::SomethingSource.new(name: "something")
         @checker.user = users(:bob)
         @checker.save!
       end
@@ -242,7 +242,7 @@ describe Agent do
 
     describe ".async_check" do
       before do
-        @checker = Agents::SomethingSource.new(:name => "something")
+        @checker = Agents::SomethingSource.new(name: "something")
         @checker.user = users(:bob)
         @checker.save!
       end
@@ -283,7 +283,8 @@ describe Agent do
 
     describe ".receive!" do
       before do
-        stub_request(:any, /darksky/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
+        stub_request(:any, /darksky/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")),
+                                                status: 200)
       end
 
       it "should use available events" do
@@ -360,7 +361,7 @@ describe Agent do
 
       it "should group events" do
         count = 0
-        allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) { |agent, events|
+        allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) { |_agent, events|
           count += 1
           expect(events.map(&:user).map(&:username).uniq.length).to eq(1)
         }
@@ -381,7 +382,7 @@ describe Agent do
       end
 
       it "should ignore events that were created before a particular Link" do
-        agent2 = Agents::SomethingSource.new(:name => "something")
+        agent2 = Agents::SomethingSource.new(name: "something")
         agent2.user = users(:bob)
         agent2.save!
         agent2.check
@@ -436,14 +437,14 @@ describe Agent do
     describe "creating a new agent and then calling .receive!" do
       it "should not backfill events for a newly created agent" do
         Event.delete_all
-        sender = Agents::SomethingSource.new(:name => "Sending Agent")
+        sender = Agents::SomethingSource.new(name: "Sending Agent")
         sender.user = users(:bob)
         sender.save!
-        sender.create_event :payload => {}
-        sender.create_event :payload => {}
+        sender.create_event payload: {}
+        sender.create_event payload: {}
         expect(sender.events.count).to eq(2)
 
-        receiver = Agents::CannotBeScheduled.new(:name => "Receiving Agent")
+        receiver = Agents::CannotBeScheduled.new(name: "Receiving Agent")
         receiver.user = users(:bob)
         receiver.sources << sender
         receiver.save!
@@ -451,7 +452,7 @@ describe Agent do
         expect(receiver.events.count).to eq(0)
         Agent.receive!
         expect(receiver.events.count).to eq(0)
-        sender.create_event :payload => {}
+        sender.create_event payload: {}
         Agent.receive!
         expect(receiver.events.count).to eq(1)
       end
@@ -460,32 +461,32 @@ describe Agent do
     describe "creating agents with propagate_immediately = true" do
       it "should schedule subagent events immediately" do
         Event.delete_all
-        sender = Agents::SomethingSource.new(:name => "Sending Agent")
+        sender = Agents::SomethingSource.new(name: "Sending Agent")
         sender.user = users(:bob)
         sender.save!
 
         receiver = Agents::CannotBeScheduled.new(
-           :name => "Receiving Agent",
+          name: "Receiving Agent",
         )
         receiver.propagate_immediately = true
         receiver.user = users(:bob)
         receiver.sources << sender
         receiver.save!
 
-        sender.create_event :payload => {"message" => "new payload"}
+        sender.create_event payload: { "message" => "new payload" }
         expect(sender.events.count).to eq(1)
         expect(receiver.events.count).to eq(1)
-        #should be true without calling Agent.receive!
+        # should be true without calling Agent.receive!
       end
 
       it "should only schedule receiving agents that are set to propagate_immediately" do
         Event.delete_all
-        sender = Agents::SomethingSource.new(:name => "Sending Agent")
+        sender = Agents::SomethingSource.new(name: "Sending Agent")
         sender.user = users(:bob)
         sender.save!
 
         im_receiver = Agents::CannotBeScheduled.new(
-           :name => "Immediate Receiving Agent",
+          name: "Immediate Receiving Agent",
         )
         im_receiver.propagate_immediately = true
         im_receiver.user = users(:bob)
@@ -493,20 +494,20 @@ describe Agent do
 
         im_receiver.save!
         slow_receiver = Agents::CannotBeScheduled.new(
-           :name => "Slow Receiving Agent",
+          name: "Slow Receiving Agent",
         )
         slow_receiver.user = users(:bob)
         slow_receiver.sources << sender
         slow_receiver.save!
 
-        sender.create_event :payload => {"message" => "new payload"}
+        sender.create_event payload: { "message" => "new payload" }
         expect(sender.events.count).to eq(1)
         expect(im_receiver.events.count).to eq(1)
-        #we should get the quick one
-        #but not the slow one
+        # we should get the quick one
+        # but not the slow one
         expect(slow_receiver.events.count).to eq(0)
         Agent.receive!
-        #now we should have one in both
+        # now we should have one in both
         expect(im_receiver.events.count).to eq(1)
         expect(slow_receiver.events.count).to eq(1)
       end
@@ -514,7 +515,7 @@ describe Agent do
 
     describe "validations" do
       it "calls validate_options" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.options[:bad] = true
         expect(agent).to have(1).error_on(:base)
@@ -523,7 +524,7 @@ describe Agent do
       end
 
       it "makes options symbol-indifferent before validating" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.options["bad"] = true
         expect(agent).to have(1).error_on(:base)
@@ -532,7 +533,7 @@ describe Agent do
       end
 
       it "makes memory symbol-indifferent before validating" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.memory["bad"] = 2
         agent.save
@@ -540,7 +541,7 @@ describe Agent do
       end
 
       it "should work when assigned a hash or JSON string" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.memory = {}
         expect(agent.memory).to eq({})
         expect(agent.memory["foo"]).to be_nil
@@ -578,11 +579,11 @@ describe Agent do
         agent.options = 5
         expect(agent.options["hi"]).to eq(2)
         expect(agent).to have(1).errors_on(:options)
-        expect(agent.errors_on(:options)).to include("cannot be set to an instance of #{2.class}")  # Integer (ruby >=2.4) or Fixnum (ruby <2.4)
+        expect(agent.errors_on(:options)).to include("cannot be set to an instance of #{2.class}") # Integer (ruby >=2.4) or Fixnum (ruby <2.4)
       end
 
       it "should not allow source agents owned by other people" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.source_ids = [agents(:bob_weather_agent).id]
         expect(agent).to have(0).errors_on(:sources)
@@ -593,7 +594,7 @@ describe Agent do
       end
 
       it "should not allow target agents owned by other people" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.receiver_ids = [agents(:bob_weather_agent).id]
         expect(agent).to have(0).errors_on(:receivers)
@@ -604,7 +605,7 @@ describe Agent do
       end
 
       it "should not allow controller agents owned by other people" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         agent.controller_ids = [agents(:bob_weather_agent).id]
         expect(agent).to have(0).errors_on(:controllers)
@@ -615,7 +616,7 @@ describe Agent do
       end
 
       it "should not allow control target agents owned by other people" do
-        agent = Agents::CannotBeScheduled.new(:name => "something")
+        agent = Agents::CannotBeScheduled.new(name: "something")
         agent.user = users(:bob)
         agent.control_target_ids = [agents(:bob_weather_agent).id]
         expect(agent).to have(0).errors_on(:control_targets)
@@ -626,7 +627,7 @@ describe Agent do
       end
 
       it "should not allow scenarios owned by other people" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
 
         agent.scenario_ids = [scenarios(:bob_weather).id]
@@ -643,7 +644,7 @@ describe Agent do
       end
 
       it "validates keep_events_for" do
-        agent = Agents::SomethingSource.new(:name => "something")
+        agent = Agents::SomethingSource.new(name: "something")
         agent.user = users(:bob)
         expect(agent).to be_valid
         agent.keep_events_for = nil
@@ -669,11 +670,11 @@ describe Agent do
       before do
         @time = "2014-01-01 01:00:00 +00:00"
         travel_to @time do
-          @agent = Agents::SomethingSource.new(:name => "something")
+          @agent = Agents::SomethingSource.new(name: "something")
           @agent.keep_events_for = 5.days
           @agent.user = users(:bob)
           @agent.save!
-          @event = @agent.create_event :payload => { "hello" => "world" }
+          @event = @agent.create_event payload: { "hello" => "world" }
           expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i)
         end
       end
@@ -695,9 +696,9 @@ describe Agent do
         it "updates events' expires_at" do
           travel_to @time do
             expect {
-                @agent.options[:foo] = "bar1"
-                @agent.keep_events_for = 3.days
-                @agent.save!
+              @agent.options[:foo] = "bar1"
+              @agent.keep_events_for = 3.days
+              @agent.save!
             }.to change { @event.reload.expires_at }
             expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i)
           end
@@ -731,18 +732,20 @@ describe Agent do
         @sender = Agents::SomethingSource.new(
           name: 'Agent (2)',
           options: { foo: 'bar2' },
-          schedule: '5pm')
+          schedule: '5pm'
+        )
         @sender.user = users(:bob)
         @sender.save!
-        @sender.create_event :payload => {}
-        @sender.create_event :payload => {}
+        @sender.create_event payload: {}
+        @sender.create_event payload: {}
         expect(@sender.events.count).to eq(2)
 
         @receiver = Agents::CannotBeScheduled.new(
           name: 'Agent',
           options: { foo: 'bar3' },
           keep_events_for: 3.days,
-          propagate_immediately: true)
+          propagate_immediately: true
+        )
         @receiver.user = users(:bob)
         @receiver.sources << @sender
         @receiver.memory[:test] = 1
@@ -752,19 +755,19 @@ describe Agent do
       it "should create a clone of a given agent for editing" do
         sender_clone = users(:bob).agents.build_clone(@sender)
 
-        expect(sender_clone.attributes).to eq(Agent.new.attributes.
-          update(@sender.slice(:user_id, :type,
-            :options, :schedule, :keep_events_for, :propagate_immediately)).
-          update('name' => 'Agent (2) (2)', 'options' => { 'foo' => 'bar2' }))
+        expect(sender_clone.attributes).to eq(Agent.new.attributes
+          .update(@sender.slice(:user_id, :type,
+                                :options, :schedule, :keep_events_for, :propagate_immediately))
+          .update('name' => 'Agent (2) (2)', 'options' => { 'foo' => 'bar2' }))
 
         expect(sender_clone.source_ids).to eq([])
 
         receiver_clone = users(:bob).agents.build_clone(@receiver)
 
-        expect(receiver_clone.attributes).to eq(Agent.new.attributes.
-          update(@receiver.slice(:user_id, :type,
-            :options, :schedule, :keep_events_for, :propagate_immediately)).
-          update('name' => 'Agent (3)', 'options' => { 'foo' => 'bar3' }))
+        expect(receiver_clone.attributes).to eq(Agent.new.attributes
+          .update(@receiver.slice(:user_id, :type,
+                                  :options, :schedule, :keep_events_for, :propagate_immediately))
+          .update('name' => 'Agent (3)', 'options' => { 'foo' => 'bar3' }))
 
         expect(receiver_clone.source_ids).to eq([@sender.id])
       end
@@ -782,7 +785,7 @@ describe Agent do
 
     context "when .receive_web_request is defined" do
       before do
-        @agent = Agents::WebRequestReceiver.new(:name => "something")
+        @agent = Agents::WebRequestReceiver.new(name: "something")
         @agent.user = users(:bob)
         @agent.save!
 
@@ -794,46 +797,49 @@ describe Agent do
 
       it "calls the .receive_web_request hook, updates last_web_request_at, and saves" do
         request = ActionDispatch::Request.new({
-          'action_dispatch.request.request_parameters' => { :some_param => "some_value" },
+          'action_dispatch.request.request_parameters' => { some_param: "some_value" },
           'REQUEST_METHOD' => "POST",
           'HTTP_ACCEPT' => 'text/html'
         })
 
         @agent.trigger_web_request(request)
-        expect(@agent.reload.memory['last_request']).to eq([ { "some_param" => "some_value" }, "post", "text/html" ])
+        expect(@agent.reload.memory['last_request']).to eq([{ "some_param" => "some_value" }, "post", "text/html"])
         expect(@agent.last_web_request_at.to_i).to be_within(1).of(Time.now.to_i)
       end
     end
 
     context "when .receive_web_request is defined with just request" do
       before do
-        @agent = Agents::WebRequestReceiver.new(:name => "something")
+        @agent = Agents::WebRequestReceiver.new(name: "something")
         @agent.user = users(:bob)
         @agent.save!
 
         def @agent.receive_web_request(request)
-          memory['last_request'] = [request.params, request.method_symbol.to_s, request.format, {'HTTP_X_CUSTOM_HEADER' => request.headers['HTTP_X_CUSTOM_HEADER']}]
+          memory['last_request'] =
+            [request.params, request.method_symbol.to_s, request.format,
+             { 'HTTP_X_CUSTOM_HEADER' => request.headers['HTTP_X_CUSTOM_HEADER'] }]
           ['Ok!', 200]
         end
       end
 
       it "calls the .trigger_web_request with headers, and they get passed to .receive_web_request" do
         request = ActionDispatch::Request.new({
-          'action_dispatch.request.request_parameters' => { :some_param => "some_value" },
+          'action_dispatch.request.request_parameters' => { some_param: "some_value" },
           'REQUEST_METHOD' => "POST",
           'HTTP_ACCEPT' => 'text/html',
           'HTTP_X_CUSTOM_HEADER' => "foo"
         })
 
         @agent.trigger_web_request(request)
-        expect(@agent.reload.memory['last_request']).to eq([ { "some_param" => "some_value" }, "post", "text/html", {'HTTP_X_CUSTOM_HEADER' => "foo"} ])
+        expect(@agent.reload.memory['last_request']).to eq([{ "some_param" => "some_value" }, "post", "text/html",
+                                                            { 'HTTP_X_CUSTOM_HEADER' => "foo" }])
         expect(@agent.last_web_request_at.to_i).to be_within(1).of(Time.now.to_i)
       end
     end
 
     context "when .receive_webhook is defined" do
       before do
-        @agent = Agents::WebRequestReceiver.new(:name => "something")
+        @agent = Agents::WebRequestReceiver.new(name: "something")
         @agent.user = users(:bob)
         @agent.save!
 
@@ -845,7 +851,7 @@ describe Agent do
 
       it "outputs a deprecation warning and calls .receive_webhook with the params" do
         request = ActionDispatch::Request.new({
-          'action_dispatch.request.request_parameters' => { :some_param => "some_value" },
+          'action_dispatch.request.request_parameters' => { some_param: "some_value" },
           'REQUEST_METHOD' => "POST",
           'HTTP_ACCEPT' => 'text/html'
         })
@@ -890,7 +896,7 @@ describe Agent do
       end
 
       it "sets expires_at on created events" do
-        event = agents(:jane_weather_agent).create_event :payload => { 'hi' => 'there' }
+        event = agents(:jane_weather_agent).create_event payload: { 'hi' => 'there' }
         expect(event.expires_at.to_i).to be_within(5).of(agents(:jane_weather_agent).keep_events_for.seconds.from_now.to_i)
       end
     end
@@ -901,7 +907,7 @@ describe Agent do
       end
 
       it "does not set expires_at on created events" do
-        event = agents(:jane_website_agent).create_event :payload => { 'hi' => 'there' }
+        event = agents(:jane_website_agent).create_event payload: { 'hi' => 'there' }
         expect(event.expires_at).to be_nil
       end
     end
@@ -925,7 +931,8 @@ describe Agent do
 
   describe ".drop_pending_events" do
     before do
-      stub_request(:any, /darksky/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")), status: 200)
+      stub_request(:any, /darksky/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")),
+                                              status: 200)
     end
 
     it "should drop pending events while the agent was disabled when set to true" do
@@ -979,7 +986,8 @@ describe AgentDrop do
         },
       },
       schedule: 'every_1h',
-      keep_events_for: 2.days)
+      keep_events_for: 2.days
+    )
     @wsa1.user = users(:bob)
     @wsa1.save!
 
@@ -996,7 +1004,8 @@ describe AgentDrop do
         },
       },
       schedule: 'every_12h',
-      keep_events_for: 2.days)
+      keep_events_for: 2.days
+    )
     @wsa2.user = users(:bob)
     @wsa2.save!
 
@@ -1012,7 +1021,8 @@ describe AgentDrop do
         skip_created_at: 'false',
       },
       keep_events_for: 2.days,
-      propagate_immediately: true)
+      propagate_immediately: true
+    )
     @efa.user = users(:bob)
     @efa.sources << @wsa1 << @wsa2
     @efa.memory[:test] = 1
@@ -1039,7 +1049,7 @@ describe AgentDrop do
     expect(interpolate(t, @wsa1)).to eq('http://xkcd.com/')
     expect(interpolate(t, @wsa2)).to eq('http://dilbert.com/')
     expect(interpolate('{{agent.options.instructions.message}}',
-                @efa)).to eq('{{agent.name}}: {{title}} {{url}}')
+                       @efa)).to eq('{{agent.name}}: {{title}} {{url}}')
   end
 
   it 'should have .sources' do

+ 15 - 5
spec/models/agents/shell_command_agent_spec.rb

@@ -12,7 +12,8 @@ describe Agents::ShellCommandAgent do
 
     @valid_params2 = {
       path: @valid_path,
-      command: [RbConfig.ruby, '-e', 'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"'],
+      command: [RbConfig.ruby, '-e',
+                'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"'],
       stdin: "{{name}}",
       expected_update_period_in_days: '1',
     }
@@ -77,9 +78,13 @@ describe Agents::ShellCommandAgent do
       allow(@checker).to receive(:run_command).with(@valid_path, 'pwd', nil, {}) { ["fake pwd output", "", 0] }
       allow(@checker).to receive(:run_command).with(@valid_path, 'empty_output', nil, {}) { ["", "", 0] }
       allow(@checker).to receive(:run_command).with(@valid_path, 'failure', nil, {}) { ["failed", "error message", 1] }
-      allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: true) { orig_run_command.(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: true) }
+      allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: true) {
+                           orig_run_command.call(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: true)
+                         }
       [[], [{}], [{ unbundle: false }]].each do |rest|
-        allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil, *rest) { [ENV['BUNDLE_GEMFILE'].to_s, "", 0] }
+        allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil, *rest) {
+                             [ENV['BUNDLE_GEMFILE'].to_s, "", 0]
+                           }
       end
     end
 
@@ -93,7 +98,10 @@ describe Agents::ShellCommandAgent do
     it "should create an event when checking (unstubbed)" do
       expect { @checker2.check }.to change { Event.count }.by(1)
       expect(Event.last.payload[:path]).to eq(@valid_path)
-      expect(Event.last.payload[:command]).to eq([RbConfig.ruby, '-e', 'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"'])
+      expect(Event.last.payload[:command]).to eq [
+        RbConfig.ruby, '-e',
+        'puts "hello, #{STDIN.eof? ? "world" : STDIN.read.strip}."; STDERR.puts "warning!"'
+      ]
       expect(Event.last.payload[:output]).to eq('hello, world.')
       expect(Event.last.payload[:errors]).to eq('warning!')
     end
@@ -169,7 +177,9 @@ describe Agents::ShellCommandAgent do
 
   describe "#receive" do
     before do
-      allow(@checker).to receive(:run_command).with(@valid_path, @event.payload[:cmd], nil, {}) { ["fake ls output", "", 0] }
+      allow(@checker).to receive(:run_command).with(@valid_path, @event.payload[:cmd], nil, {}) {
+                           ["fake ls output", "", 0]
+                         }
     end
 
     it "creates events" do