From c5f4435e081747847b7e04b4cff1b6ac6453cdf6 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Sun, 16 Apr 2023 23:04:04 +0900 Subject: [PATCH 01/32] Add rubocop and the friends --- Gemfile | 4 ++++ Gemfile.lock | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e057571b3e..edfceba9e8 100644 --- a/Gemfile +++ b/Gemfile @@ -145,6 +145,10 @@ group :development do gem 'capistrano-rails' gem 'capistrano-bundler' + gem 'rubocop', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rspec', require: false + if_true(ENV['SPRING']) do gem 'spring-commands-rspec' gem 'spring' diff --git a/Gemfile.lock b/Gemfile.lock index ca81053d41..7cb1fd85ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -155,6 +155,7 @@ GEM public_suffix (>= 2.0.2, < 6.0) airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) + ast (2.4.2) aws-eventstream (1.2.0) aws-partitions (1.547.0) aws-sdk-core (3.125.2) @@ -417,7 +418,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.6.1) + json (2.6.3) jsonpath (1.1.0) multi_json jwt (2.3.0) @@ -551,6 +552,9 @@ GEM rack orm_adapter (0.5.0) os (1.1.4) + parallel (1.22.1) + parser (3.2.2.0) + ast (~> 2.4.1) pg (1.4.4) poltergeist (1.8.1) capybara (~> 2.1) @@ -604,6 +608,7 @@ GEM method_source rake (>= 12.2) thor (~> 1.0) + rainbow (3.1.1) raindrops (0.20.0) rake (13.0.6) rb-fsevent (0.11.2) @@ -611,6 +616,7 @@ GEM ffi (~> 1.0) rb-kqueue (0.2.4) ffi (>= 0.5.0) + regexp_parser (2.7.0) representable (3.1.1) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -656,8 +662,29 @@ GEM erector nokogiri rest-client + rubocop (1.50.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.17.1) + rubocop (~> 1.41) + rubocop-performance (1.17.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rspec (2.18.1) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) ruby-growl (4.1) uuid (~> 2.3, >= 2.3.5) + ruby-progressbar (1.13.0) rufus-scheduler (3.8.1) fugit (~> 1.1, >= 1.1.6) sass (3.7.4) @@ -738,6 +765,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.4) + unicode-display_width (2.4.2) unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -858,6 +886,9 @@ DEPENDENCIES rspec-mocks rspec-rails rturk (~> 2.12.1) + rubocop + rubocop-performance + rubocop-rspec ruby-growl (~> 4.1.0) rufus-scheduler (~> 3.4) sass-rails (>= 6.0) From 930da10ceae1a13a0f263336987e1d71caf6628c Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 01:50:24 +0900 Subject: [PATCH 02/32] Reformat code with Rubocop and manually adjust styles --- app/concerns/email_concern.rb | 15 +- app/concerns/event_headers_concern.rb | 6 +- app/concerns/web_request_concern.rb | 32 +- app/controllers/agents/dry_runs_controller.rb | 15 +- app/helpers/scenario_helper.rb | 4 +- app/models/agent.rb | 4 +- app/models/agent_log.rb | 10 +- app/models/agents/adioso_agent.rb | 64 ++-- app/models/agents/aftership_agent.rb | 30 +- .../agents/attribute_difference_agent.rb | 9 +- app/models/agents/change_detector_agent.rb | 31 +- app/models/agents/commander_agent.rb | 2 +- app/models/agents/csv_agent.rb | 78 +++-- app/models/agents/data_output_agent.rb | 126 ++++---- app/models/agents/de_duplication_agent.rb | 12 +- app/models/agents/delay_agent.rb | 5 +- app/models/agents/digest_agent.rb | 15 +- app/models/agents/dropbox_file_url_agent.rb | 75 ++--- app/models/agents/dropbox_watch_agent.rb | 18 +- app/models/agents/email_agent.rb | 36 ++- app/models/agents/email_digest_agent.rb | 34 +-- app/models/agents/event_formatting_agent.rb | 11 +- app/models/agents/evernote_agent.rb | 58 ++-- app/models/agents/ftpsite_agent.rb | 21 +- app/models/agents/gap_detector_agent.rb | 10 +- .../agents/google_calendar_publish_agent.rb | 35 +-- app/models/agents/google_translation_agent.rb | 4 +- app/models/agents/growl_agent.rb | 24 +- app/models/agents/hipchat_agent.rb | 25 +- app/models/agents/http_status_agent.rb | 21 +- app/models/agents/human_task_agent.rb | 277 ++++++++++-------- app/models/agents/imap_folder_agent.rb | 69 +++-- app/models/agents/jabber_agent.rb | 21 +- app/models/agents/java_script_agent.rb | 48 +-- app/models/agents/jira_agent.rb | 85 +++--- app/models/agents/jq_agent.rb | 7 +- app/models/agents/json_parse_agent.rb | 18 +- app/models/agents/key_value_store_agent.rb | 2 +- app/models/agents/liquid_output_agent.rb | 160 +++++----- app/models/agents/local_file_agent.rb | 48 +-- app/models/agents/manual_event_agent.rb | 11 +- app/models/agents/mqtt_agent.rb | 39 ++- app/models/agents/pdf_info_agent.rb | 43 ++- app/models/agents/peak_detector_agent.rb | 24 +- app/models/agents/phantom_js_cloud_agent.rb | 5 +- app/models/agents/post_agent.rb | 33 ++- app/models/agents/public_transport_agent.rb | 76 ++--- app/models/agents/pushbullet_agent.rb | 25 +- app/models/agents/pushover_agent.rb | 19 +- app/models/agents/read_file_agent.rb | 13 +- app/models/agents/rss_agent.rb | 51 ++-- app/models/agents/s3_agent.rb | 54 ++-- app/models/agents/scheduler_agent.rb | 2 +- app/models/agents/sentiment_agent.rb | 37 ++- app/models/agents/shell_command_agent.rb | 37 ++- app/models/agents/slack_agent.rb | 8 +- app/models/agents/stubhub_agent.rb | 33 +-- app/models/agents/telegram_agent.rb | 41 ++- app/models/agents/trigger_agent.rb | 58 ++-- app/models/agents/tumblr_likes_agent.rb | 15 +- app/models/agents/tumblr_publish_agent.rb | 27 +- app/models/agents/twilio_agent.rb | 27 +- .../agents/twilio_receive_text_agent.rb | 36 +-- app/models/agents/twitter_action_agent.rb | 2 +- app/models/agents/twitter_favorites.rb | 2 +- app/models/agents/twitter_publish_agent.rb | 42 +-- app/models/agents/twitter_search_agent.rb | 2 +- app/models/agents/twitter_stream_agent.rb | 10 +- app/models/agents/twitter_user_agent.rb | 2 +- app/models/agents/user_location_agent.rb | 31 +- app/models/agents/weather_agent.rb | 40 +-- app/models/agents/webhook_agent.rb | 34 ++- app/models/agents/website_agent.rb | 107 ++++--- app/models/agents/weibo_publish_agent.rb | 34 +-- app/models/agents/weibo_user_agent.rb | 17 +- app/models/agents/witai_agent.rb | 72 ++--- app/models/event.rb | 46 +-- app/models/link.rb | 4 +- app/models/scenario.rb | 22 +- app/models/scenario_membership.rb | 4 +- app/models/service.rb | 32 +- app/models/user.rb | 55 ++-- app/models/user_credential.rb | 2 +- .../form_configurable_agent_presenter.rb | 22 +- lib/location.rb | 3 +- .../agents/dry_runs_controller_spec.rb | 35 +-- spec/features/create_an_agent_spec.rb | 4 +- spec/models/agent_spec.rb | 154 +++++----- .../models/agents/shell_command_agent_spec.rb | 20 +- 89 files changed, 1685 insertions(+), 1392 deletions(-) mode change 100644 => 100755 app/models/agents/jira_agent.rb diff --git a/app/concerns/email_concern.rb b/app/concerns/email_concern.rb index cc43348a7b..b579925d6c 100644 --- a/app/concerns/email_concern.rb +++ b/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 @@ def present(payload) 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 diff --git a/app/concerns/event_headers_concern.rb b/app/concerns/event_headers_concern.rb index 78e61ec26f..6bdfc77bac 100644 --- a/app/concerns/event_headers_concern.rb +++ b/app/concerns/event_headers_concern.rb @@ -14,11 +14,11 @@ def validate_event_headers_options! 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 diff --git a/app/concerns/web_request_concern.rb b/app/concerns/web_request_concern.rb index ead57151a2..690939a845 100644 --- a/app/concerns/web_request_concern.rb +++ b/app/concerns/web_request_concern.rb @@ -38,8 +38,12 @@ def call(env) # 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 @@ def validate_web_request_options! 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 @@ def faraday @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 @@ def faraday 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 @@ def headers(value = interpolated['headers']) 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 diff --git a/app/controllers/agents/dry_runs_controller.rb b/app/controllers/agents/dry_runs_controller.rb index 7dfde8aa7c..8905f07aed 100644 --- a/app/controllers/agents/dry_runs_controller.rb +++ b/app/controllers/agents/dry_runs_controller.rb @@ -7,7 +7,7 @@ def index 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 @@ def create @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 diff --git a/app/helpers/scenario_helper.rb b/app/helpers/scenario_helper.rb index e71fba96d0..393c524ffd 100644 --- a/app/helpers/scenario_helper.rb +++ b/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 @@ def default_scenario_bg_color def default_scenario_fg_color '#FFFFFF' end - end diff --git a/app/models/agent.rb b/app/models/agent.rb index fb83581d4b..9c17eb954d 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -325,7 +325,7 @@ def build_clone(original) # 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 @@ def async_receive(agent_id, event_ids) 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) diff --git a/app/models/agent_log.rb b/app/models/agent_log.rb index 026327d714..ffb2e3a2a6 100644 --- a/app/models/agent_log.rb +++ b/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 @@ def self.log_length 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 diff --git a/app/models/agents/adioso_agent.rb b/app/models/agents/adioso_agent.rb index 3c4d47eda4..4459fe1478 100644 --- a/app/models/agents/adioso_agent.rb +++ b/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 @@ class AdiosoAgent < Agent 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 @@ def working? 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 @@ def check 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 - diff --git a/app/models/agents/aftership_agent.rb b/app/models/agents/aftership_agent.rb index 14e08a00a7..62185acbef 100644 --- a/app/models/agents/aftership_agent.rb +++ b/app/models/agents/aftership_agent.rb @@ -2,21 +2,20 @@ 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 @@ class AftershipAgent < Agent * `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 @@ class AftershipAgent < Agent MD def default_options - { 'api_key' => 'YOUR_API_KEY', - 'path' => 'trackings' + { + 'api_key' => 'YOUR_API_KEY', + 'path' => 'trackings', } end @@ -104,10 +104,11 @@ def validate_options 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 @@ def event_url 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 diff --git a/app/models/agents/attribute_difference_agent.rb b/app/models/agents/attribute_difference_agent.rb index c75b927796..ab69e8d115 100644 --- a/app/models/agents/attribute_difference_agent.rb +++ b/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 @@ class AttributeDifferenceAgent < Agent 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 @@ def handle(opts, event) 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 diff --git a/app/models/agents/change_detector_agent.rb b/app/models/agents/change_detector_agent.rb index 0c8c789ea4..a07c2fceb5 100644 --- a/app/models/agents/change_detector_agent.rb +++ b/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 @@ class ChangeDetectorAgent < Agent 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 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/commander_agent.rb b/app/models/agents/commander_agent.rb index ac2eca932b..f49f77f10a 100644 --- a/app/models/agents/commander_agent.rb +++ b/app/models/agents/commander_agent.rb @@ -4,7 +4,7 @@ class CommanderAgent < Agent 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 diff --git a/app/models/agents/csv_agent.rb b/app/models/agents/csv_agent.rb index 50e1e9f829..051d3b9c0e 100644 --- a/app/models/agents/csv_agent.rb +++ b/app/models/agents/csv_agent.rb @@ -19,7 +19,7 @@ def default_options 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 @@ def default_options 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 @@ def receive(incoming_events) 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 @@ def parse(incoming_events) 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 } diff --git a/app/models/agents/data_output_agent.rb b/app/models/agents/data_output_agent.rb index f027432002..405e28a34b 100644 --- a/app/models/agents/data_output_agent.rb +++ b/app/models/agents/data_output_agent.rb @@ -5,13 +5,13 @@ class DataOutputAgent < Agent 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 @@ def validate_options 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 @@ def feed_link 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 @@ def feed_icon end def itunes_icon - if(boolify(interpolated['ns_itunes'])) + if boolify(interpolated['ns_itunes']) "" - end + end end def feed_description @@ -181,13 +183,13 @@ def rss_content_type 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 @@ def latest_events(reload = false) 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 @@ def latest_events(reload = false) 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 @@ def receive_web_request(params, method, format) 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 @@ def receive_web_request(params, method, format) items = items_to_xml(items) - return [<<-XML, 200, rss_content_type, interpolated['response_headers'].presence] - - - - - #{feed_icon.encode(xml: :text)} - #{itunes_icon} -#{hub_links} - #{feed_title.encode(xml: :text)} - #{feed_description.encode(xml: :text)} - #{feed_link.encode(xml: :text)} - #{now.rfc2822.to_s.encode(xml: :text)} - #{now.rfc2822.to_s.encode(xml: :text)} - #{feed_ttl} -#{items} - - + return [<<~XML, 200, rss_content_type, interpolated['response_headers'].presence] + + + + + #{feed_icon.encode(xml: :text)} + #{itunes_icon} + #{hub_links} + #{feed_title.encode(xml: :text)} + #{feed_description.encode(xml: :text)} + #{feed_link.encode(xml: :text)} + #{now.rfc2822.to_s.encode(xml: :text)} + #{now.rfc2822.to_s.encode(xml: :text)} + #{feed_ttl} + #{items} + + XML end end @@ -336,13 +340,17 @@ def receive(incoming_events) 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 @@ def to_xml(options) 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 @@ def simplify_item_for_json(item) 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 @@ def push_to_hub(hub, url) 'hub.mode' => 'publish', 'hub.url' => url } - rescue => e - error "Push failed: #{e.message}" + rescue StandardError => e + error "Push failed: #{e.message}" end end end diff --git a/app/models/agents/de_duplication_agent.rb b/app/models/agents/de_duplication_agent.rb index 463bd54853..dd956b5398 100644 --- a/app/models/agents/de_duplication_agent.rb +++ b/app/models/agents/de_duplication_agent.rb @@ -3,7 +3,7 @@ class DeDuplicationAgent < Agent 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 @@ class DeDuplicationAgent < Agent `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 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/delay_agent.rb b/app/models/agents/delay_agent.rb index 9d01cd3033..ffd9543dae 100644 --- a/app/models/agents/delay_agent.rb +++ b/app/models/agents/delay_agent.rb @@ -4,7 +4,7 @@ class DelayAgent < Agent 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 @@ def default_options 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]) diff --git a/app/models/agents/digest_agent.rb b/app/models/agents/digest_agent.rb index f49e6f19f9..2f255b129e 100644 --- a/app/models/agents/digest_agent.rb +++ b/app/models/agents/digest_agent.rb @@ -4,7 +4,7 @@ class DigestAgent < Agent 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 @@ class DigestAgent < Agent 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 @@ class DigestAgent < Agent 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 @@ def default_options 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 @@ def check 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 diff --git a/app/models/agents/dropbox_file_url_agent.rb b/app/models/agents/dropbox_file_url_agent.rb index 7cc7af76b8..2756d49b62 100644 --- a/app/models/agents/dropbox_file_url_agent.rb +++ b/app/models/agents/dropbox_file_url_agent.rb @@ -6,7 +6,7 @@ class DropboxFileUrlAgent < Agent 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 @@ class DropboxFileUrlAgent < Agent 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 @@ def temporary_url_for(path) 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 diff --git a/app/models/agents/dropbox_watch_agent.rb b/app/models/agents/dropbox_watch_agent.rb index d2313eb8ba..fe189deb6e 100644 --- a/app/models/agents/dropbox_watch_agent.rb +++ b/app/models/agents/dropbox_watch_agent.rb @@ -5,13 +5,13 @@ class DropboxWatchAgent < Agent 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 @@ def default_options 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 @@ def check 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 @@ def remember(contents) class DropboxDirDiff def initialize(previous, current) - @previous, @current = [previous || [], current || []] + @previous = previous || [] + @current = current || [] end def empty? @@ -99,7 +103,5 @@ def find_by_path(array, path) array.find { |entry| entry['path'] == path } end end - end - end diff --git a/app/models/agents/email_agent.rb b/app/models/agents/email_agent.rb index 765699879b..efb6b9cd74 100644 --- a/app/models/agents/email_agent.rb +++ b/app/models/agents/email_agent.rb @@ -9,7 +9,7 @@ class EmailAgent < Agent 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 @@ class EmailAgent < Agent 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 @@ def working? 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 diff --git a/app/models/agents/email_digest_agent.rb b/app/models/agents/email_digest_agent.rb index 0cfa3ba96e..630d8119a9 100644 --- a/app/models/agents/email_digest_agent.rb +++ b/app/models/agents/email_digest_agent.rb @@ -8,7 +8,7 @@ class EmailDigestAgent < Agent 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 @@ class EmailDigestAgent < Agent 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 @@ def check 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 diff --git a/app/models/agents/event_formatting_agent.rb b/app/models/agents/event_formatting_agent.rb index 4286923f6c..898c0ef6c7 100644 --- a/app/models/agents/event_formatting_agent.rb +++ b/app/models/agents/event_formatting_agent.rb @@ -3,7 +3,7 @@ class EventFormattingAgent < Agent 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 @@ class EventFormattingAgent < Agent 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 @@ def validate_options 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 @@ def validate_matchers if regexp.present? begin Regexp.new(regexp) - rescue + rescue StandardError errors.add(:base, "bad regexp found in matchers: #{regexp}") end else diff --git a/app/models/agents/evernote_agent.rb b/app/models/agents/evernote_agent.rb index 5eb6260a6e..3356f5a08b 100644 --- a/app/models/agents/evernote_agent.rb +++ b/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 @@ class EvernoteAgent < Agent } MD - event_description <<-MD + event_description <<~MD When `mode` is `update`, events look like: { @@ -106,7 +106,7 @@ def default_options 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 @@ def validate_options 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 @@ def receive(incoming_events) 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 @@ def check 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 @@ def note_store # 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 @@ def update_note(params) # 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 @@ def build_note(en_note) 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 @@ def with_wrapped_content(params) params[:content] = "" \ "" \ - "#{params[:content].encode(:xml => :text)}" + "#{params[:content].encode(xml: :text)}" end params @@ -278,6 +283,7 @@ def with_wrapped_content(params) class Search attr_reader :note_store, :opts + def initialize(note_store, opts) @note_store = note_store @opts = opts @@ -297,7 +303,7 @@ def notes # 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 @@ def create_filter 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 @@ def create_spec 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 @@ def initialize(en_note, notebook, tags) 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] diff --git a/app/models/agents/ftpsite_agent.rb b/app/models/agents/ftpsite_agent.rb index 3d803f055f..b6ba78216d 100644 --- a/app/models/agents/ftpsite_agent.rb +++ b/app/models/agents/ftpsite_agent.rb @@ -11,7 +11,7 @@ class FtpsiteAgent < Agent 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 @@ class FtpsiteAgent < Agent MD end - event_description <<-MD + event_description <<~MD Events look like this: { @@ -83,7 +83,7 @@ def validate_options 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 @@ def validate_options 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 @@ def check 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 @@ def each_entry 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 diff --git a/app/models/agents/gap_detector_agent.rb b/app/models/agents/gap_detector_agent.rb index c371fd93c7..effbc0ef80 100644 --- a/app/models/agents/gap_detector_agent.rb +++ b/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 @@ class GapDetectorAgent < Agent a payload of `message`. MD - event_description <<-MD + event_description <<~MD Events look like: { @@ -58,8 +58,10 @@ def check 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 diff --git a/app/models/agents/google_calendar_publish_agent.rb b/app/models/agents/google_calendar_publish_agent.rb index 319f3a69df..679083668f 100644 --- a/app/models/agents/google_calendar_publish_agent.rb +++ b/app/models/agents/google_calendar_publish_agent.rb @@ -8,7 +8,7 @@ class GoogleCalendarPublishAgent < Agent 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 @@ class GoogleCalendarPublishAgent < Agent } 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 @@ def receive(incoming_events) 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 @@ def receive(incoming_events) 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 @@ def receive(incoming_events) end end end - diff --git a/app/models/agents/google_translation_agent.rb b/app/models/agents/google_translation_agent.rb index c4f70422ba..01b18e6bec 100644 --- a/app/models/agents/google_translation_agent.rb +++ b/app/models/agents/google_translation_agent.rb @@ -4,7 +4,7 @@ class GoogleTranslationAgent < Agent 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 @@ def google_client 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 diff --git a/app/models/agents/growl_agent.rb b/app/models/agents/growl_agent.rb index 66cd9bd20f..cad3e79d58 100644 --- a/app/models/agents/growl_agent.rb +++ b/app/models/agents/growl_agent.rb @@ -9,7 +9,7 @@ class GrowlAgent < Agent 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 @@ class GrowlAgent < Agent 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 @@ def receive(incoming_events) 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) diff --git a/app/models/agents/hipchat_agent.rb b/app/models/agents/hipchat_agent.rb index f89a0c95fd..7f1c41b45f 100644 --- a/app/models/agents/hipchat_agent.rb +++ b/app/models/agents/hipchat_agent.rb @@ -8,7 +8,7 @@ class HipchatAgent < Agent 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 @@ def validate_auth_token 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 @@ def working? 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 diff --git a/app/models/agents/http_status_agent.rb b/app/models/agents/http_status_agent.rb index 313fc2aa17..5dca39b015 100644 --- a/app/models/agents/http_status_agent.rb +++ b/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 @@ class HttpStatusAgent < Agent 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 @@ class HttpStatusAgent < Agent 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 @@ def check_this_url(url, local_headers) # 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 diff --git a/app/models/agents/human_task_agent.rb b/app/models/agents/human_task_agent.rb index 90d0784656..2100956376 100644 --- a/app/models/agents/human_task_agent.rb +++ b/app/models/agents/human_task_agent.rb @@ -4,7 +4,7 @@ class HumanTaskAgent < Agent 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 @@ class HumanTaskAgent < Agent 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 @@ def validate_options 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 @@ def validate_options 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 @@ def receive(incoming_events) protected if defined?(RTurk) - def take_majority? interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" end @@ -266,139 +293,151 @@ def review_hits 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 @@ def create_hit(opts = {}) 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'] || {} diff --git a/app/models/agents/imap_folder_agent.rb b/app/models/agents/imap_folder_agent.rb index 70d3c8d7f6..101aa9291b 100644 --- a/app/models/agents/imap_folder_agent.rb +++ b/app/models/agents/imap_folder_agent.rb @@ -14,7 +14,7 @@ class ImapFolderAgent < Agent 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 @@ class ImapFolderAgent < Agent 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 @@ class ImapFolderAgent < Agent 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 @@ def validate_options 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 @@ def validate_options 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 @@ def validate_options 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 @@ def validate_options 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 @@ def check 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 @@ def check 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 @@ def check '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 @@ def check 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 @@ def each_unread_mail 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 @@ def each_unread_mail 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 @@ def each_unread_mail 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 @@ def lastseen 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 @@ def notified Notified.new(memory['notified']) end - def notified= value + def notified=(value) memory['notified'] = value end @@ -540,7 +547,7 @@ class Message < SimpleDelegator 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 @@ def body_parts(mime_types = DEFAULT_BODY_MIME_TYPES) [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) diff --git a/app/models/agents/jabber_agent.rb b/app/models/agents/jabber_agent.rb index 52e04e509c..634c9f19b4 100644 --- a/app/models/agents/jabber_agent.rb +++ b/app/models/agents/jabber_agent.rb @@ -7,7 +7,7 @@ class JabberAgent < Agent 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 @@ class JabberAgent < Agent 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 @@ class JabberAgent < Agent 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 @@ def validate_options 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 @@ def start_worker? 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 @@ def body(event) end class Worker < LongRunnable::Worker - IGNORE_MESSAGES_FOR=5 + IGNORE_MESSAGES_FOR = 5 def setup require 'xmpp4r/muc/helper/simplemucclient' @@ -124,7 +124,7 @@ def message_handler(event, args) 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 @@ def client end private + def normalize_args(event, args) case event when :on_join, :on_leave diff --git a/app/models/agents/java_script_agent.rb b/app/models/agents/java_script_agent.rb index 9a5022af96..4be444f6cc 100644 --- a/app/models/agents/java_script_agent.rb +++ b/app/models/agents/java_script_agent.rb @@ -11,7 +11,7 @@ class JavaScriptAgent < Agent 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 @@ class JavaScriptAgent < Agent 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 @@ def execute_js(js_function, incoming_events = []) 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 @@ def setup_javascript 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 diff --git a/app/models/agents/jira_agent.rb b/app/models/agents/jira_agent.rb old mode 100644 new mode 100755 index 45280f7013..b00dbd6115 --- a/app/models/agents/jira_agent.rb +++ b/app/models/agents/jira_agent.rb @@ -10,11 +10,11 @@ class JiraAgent < Agent 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 @@ class JiraAgent < Agent 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 @@ class JiraAgent < Agent def default_options { - 'username' => '', + 'username' => '', 'password' => '', 'jira_url' => 'https://jira.atlassian.com', 'jql' => '', @@ -52,9 +52,11 @@ def default_options 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 @@ def check # 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 @@ def get_issues(since) # 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 @@ def get_issues(since) 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 - diff --git a/app/models/agents/jq_agent.rb b/app/models/agents/jq_agent.rb index 5f41e52285..72f62058d3 100644 --- a/app/models/agents/jq_agent.rb +++ b/app/models/agents/jq_agent.rb @@ -29,7 +29,7 @@ def self.jq_info 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 @@ def self.jq_info 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 @@ def process_event(event) log "Creating #{results.size} events" results.each do |payload| - create_event payload: payload + create_event(payload:) end end end diff --git a/app/models/agents/json_parse_agent.rb b/app/models/agents/json_parse_agent.rb index e5d1ae9f36..7fbd13d7da 100644 --- a/app/models/agents/json_parse_agent.rb +++ b/app/models/agents/json_parse_agent.rb @@ -5,7 +5,7 @@ class JsonParseAgent < Agent 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 @@ def default_options 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 @@ def default_options 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 @@ def working? 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 diff --git a/app/models/agents/key_value_store_agent.rb b/app/models/agents/key_value_store_agent.rb index ec6f689d12..20a99bf7dd 100644 --- a/app/models/agents/key_value_store_agent.rb +++ b/app/models/agents/key_value_store_agent.rb @@ -6,7 +6,7 @@ class KeyValueStoreAgent < Agent 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. diff --git a/app/models/agents/liquid_output_agent.rb b/app/models/agents/liquid_output_agent.rb index c3f01c5d05..3f699fba27 100644 --- a/app/models/agents/liquid_output_agent.rb +++ b/app/models/agents/liquid_output_agent.rb @@ -8,24 +8,24 @@ class LiquidOutputAgent < Agent 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 @@ class LiquidOutputAgent < Agent ### 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 = < - {% for event in events %} - - {{ event.title }} - Click here to see - - {% endfor %} - -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: + + + {% for event in events %} + + + + + {% endfor %} +
{{ event.title }}Click here to see
+ EOF { "secret" => "a-secret-key", "expected_receive_period_in_days" => 2, @@ -104,7 +99,7 @@ def default_options 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 @@ def validate_options 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 @@ def mode 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 @@ def data_for_liquid_template 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 diff --git a/app/models/agents/local_file_agent.rb b/app/models/agents/local_file_agent.rb index 728f29a0e2..b51fe6fca4 100644 --- a/app/models/agents/local_file_agent.rb +++ b/app/models/agents/local_file_agent.rb @@ -13,7 +13,7 @@ def self.should_run? 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 @@ def self.should_run? 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 @@ def default_options } 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 @@ def working? 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 @@ def check 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 @@ def callback(*changes) 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 @@ def callback(*changes) 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 diff --git a/app/models/agents/manual_event_agent.rb b/app/models/agents/manual_event_agent.rb index 0d7a67629a..6704ffb6cf 100644 --- a/app/models/agents/manual_event_agent.rb +++ b/app/models/agents/manual_event_agent.rb @@ -3,7 +3,7 @@ class ManualEventAgent < Agent 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 @@ def handle_details_post(params) 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 diff --git a/app/models/agents/mqtt_agent.rb b/app/models/agents/mqtt_agent.rb index 67aa5f1666..cc74b2e341 100644 --- a/app/models/agents/mqtt_agent.rb +++ b/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 @@ class MqttAgent < Agent 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 -
{
-        "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
-      }
+ { + "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 @@ def default_options { '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 @@ def default_options 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 @@ def receive(incoming_events) end end - def check last_message = memory['last_message'] mqtt_client.connect @@ -131,7 +127,7 @@ def check # 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 @@ def check self.memory['last_message'] = last_message save! end - end end diff --git a/app/models/agents/pdf_info_agent.rb b/app/models/agents/pdf_info_agent.rb index 6738bf4fa7..2129133bf9 100644 --- a/app/models/agents/pdf_info_agent.rb +++ b/app/models/agents/pdf_info_agent.rb @@ -3,13 +3,12 @@ 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 @@ class PdfInfoAgent < Agent 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 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/peak_detector_agent.rb b/app/models/agents/peak_detector_agent.rb index fff1ca8af7..ba8bbadfe0 100644 --- a/app/models/agents/peak_detector_agent.rb +++ b/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 @@ class PeakDetectorAgent < Agent 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 @@ def validate_options 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 @@ def check_for_peak(group, event) 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 @@ def peak_spacing 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 diff --git a/app/models/agents/phantom_js_cloud_agent.rb b/app/models/agents/phantom_js_cloud_agent.rb index 1f1903bebc..10005feece 100644 --- a/app/models/agents/phantom_js_cloud_agent.rb +++ b/app/models/agents/phantom_js_cloud_agent.rb @@ -11,7 +11,7 @@ class PhantomJsCloudAgent < Agent 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 @@ class PhantomJsCloudAgent < Agent 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": "..." } diff --git a/app/models/agents/post_agent.rb b/app/models/agents/post_agent.rb index 662b24a915..06b4d646d7 100644 --- a/app/models/agents/post_agent.rb +++ b/app/models/agents/post_agent.rb @@ -13,7 +13,7 @@ class PostAgent < Agent 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 @@ class PostAgent < Agent MD end - event_description <<-MD + event_description <<~MD Events look like this: - { - "status": 200, - "headers": { - "Content-Type": "text/html", - ... - }, - "body": "Some data..." - } + + { + "status": 200, + "headers": { + "Content-Type": "text/html", + ... + }, + "body": "Some data..." + } Original event contents will be merged when `output_mode` is set to `merge`. MD @@ -89,7 +90,7 @@ def default_options 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 @@ def validate_options 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 @@ def validate_options 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 @@ def handle(data, event = Event.new, headers) case method when 'get', 'delete' - params, body = data, nil + params = data + body = nil when 'post', 'put', 'patch' params = nil diff --git a/app/models/agents/public_transport_agent.rb b/app/models/agents/public_transport_agent.rb index b4f69a7238..4ebbb85da9 100644 --- a/app/models/agents/public_transport_agent.rb +++ b/app/models/agents/public_transport_agent.rb @@ -6,7 +6,7 @@ class PublicTransportAgent < Agent 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 @@ class PublicTransportAgent < Agent 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 @@ class PublicTransportAgent < Agent 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 @@ def check_url 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 @@ def update_memory(vals) 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 diff --git a/app/models/agents/pushbullet_agent.rb b/app/models/agents/pushbullet_agent.rb index 07b2460615..f30f5e239e 100644 --- a/app/models/agents/pushbullet_agent.rb +++ b/app/models/agents/pushbullet_agent.rb @@ -10,13 +10,13 @@ class PushbulletAgent < Agent 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 @@ def default_options 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 @@ def validate_api_key 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 @@ def receive(incoming_events) end private + def safely yield rescue Unauthorized => e @@ -101,6 +104,7 @@ def safely 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 @@ def devices 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) diff --git a/app/models/agents/pushover_agent.rb b/app/models/agents/pushover_agent.rb index acffcb5ff6..6d85f7f600 100644 --- a/app/models/agents/pushover_agent.rb +++ b/app/models/agents/pushover_agent.rb @@ -5,10 +5,9 @@ class PushoverAgent < Agent 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 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/read_file_agent.rb b/app/models/agents/read_file_agent.rb index a7903408c7..ec87c50f83 100644 --- a/app/models/agents/read_file_agent.rb +++ b/app/models/agents/read_file_agent.rb @@ -13,7 +13,7 @@ def default_options 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 @@ def default_options MD end - event_description <<-MD - { - "data" => '...' - } + event_description <<~MD + Events look like: + + { + "data" => '...' + } MD form_configurable :data_key, type: :string @@ -43,6 +45,7 @@ def working? 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 diff --git a/app/models/agents/rss_agent.rb b/app/models/agents/rss_agent.rb index 9a3ac794ee..1bb7174b14 100644 --- a/app/models/agents/rss_agent.rb +++ b/app/models/agents/rss_agent.rb @@ -11,7 +11,7 @@ class RssAgent < Agent 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 @@ def default_options } end - event_description <<-MD + event_description <<~MD Events look like: { @@ -131,11 +131,13 @@ def validate_options 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 @@ def check_urls(urls) 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 @@ def preprocessed_body(response) else # Encoding is already known, so do not let the parser detect # it from the XML declaration in the content. - body.sub!(/(?\A\u{FEFF}?\s*<\?xml(?:\s+\w+(?\s*=\s*(?:'[^']*'|"[^"]*")))*?)\s+encoding\g/, '\\k') + body.sub!(/(?\A\u{FEFF}?\s*<\?xml(?:\s+\w+(?\s*=\s*(?:'[^']*'|"[^"]*")))*?)\s+encoding\g/, + '\\k') end body end @@ -226,7 +227,7 @@ def feed_data(feed) { id: feed.feed_id, - type: type, + type:, url: feed.url, links: feed.links, title: feed.title, @@ -257,15 +258,15 @@ def itunes_feed_data(feed) 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 diff --git a/app/models/agents/s3_agent.rb b/app/models/agents/s3_agent.rb index bf3291ba61..34db21cd06 100644 --- a/app/models/agents/s3_agent.rb +++ b/app/models/agents/s3_agent.rb @@ -11,7 +11,7 @@ class S3Agent < Agent 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 @@ class S3Agent < Agent 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 @@ def default_options } 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 @@ def validate_access_key_secret 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 @@ def working? 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 @@ def get_io(file) def receive(incoming_events) return if interpolated['mode'] != 'write' + incoming_events.each do |event| safely do mo = interpolated(event) @@ -155,7 +159,7 @@ def safely 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 @@ def watch(contents) 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 diff --git a/app/models/agents/scheduler_agent.rb b/app/models/agents/scheduler_agent.rb index 3d06ae1bea..3d1e824d4a 100644 --- a/app/models/agents/scheduler_agent.rb +++ b/app/models/agents/scheduler_agent.rb @@ -12,7 +12,7 @@ class SchedulerAgent < Agent 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 diff --git a/app/models/agents/sentiment_agent.rb b/app/models/agents/sentiment_agent.rb index ad48dd0c17..78fe12e897 100644 --- a/app/models/agents/sentiment_agent.rb +++ b/app/models/agents/sentiment_agent.rb @@ -6,7 +6,7 @@ class SentimentAgent < Agent 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 @@ class SentimentAgent < Agent 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 @@ def receive(incoming_events) 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 @@ def self.sentiment_hash 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 \ No newline at end of file +end diff --git a/app/models/agents/shell_command_agent.rb b/app/models/agents/shell_command_agent.rb index 672dd9b48a..6a3d965415 100644 --- a/app/models/agents/shell_command_agent.rb +++ b/app/models/agents/shell_command_agent.rb @@ -5,12 +5,11 @@ class ShellCommandAgent < Agent 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 @@ def self.should_run? 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 @@ def handle(opts, event = nil) } 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 @@ def run_command(path, command, stdin, unbundle: false) _, status = Process.wait2(pid) exit_status = status.exitstatus - rescue => e + rescue StandardError => e errors = e.to_s result = ''.freeze exit_status = nil diff --git a/app/models/agents/slack_agent.rb b/app/models/agents/slack_agent.rb index 7c6b18505d..04ad3b573d 100644 --- a/app/models/agents/slack_agent.rb +++ b/app/models/agents/slack_agent.rb @@ -10,7 +10,7 @@ class SlackAgent < Agent 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 @@ def default_options 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 @@ def username 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) diff --git a/app/models/agents/stubhub_agent.rb b/app/models/agents/stubhub_agent.rb index f0ba562be3..c4693541a5 100644 --- a/app/models/agents/stubhub_agent.rb +++ b/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 @@ def working? 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 @@ def url 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 @@ def fetch_stubhub_data(url) end class StubhubFetcher - def self.call(url) new(url).fields end @@ -63,7 +63,7 @@ def event_id 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 @@ def fields private attr_reader :url - end end end diff --git a/app/models/agents/telegram_agent.rb b/app/models/agents/telegram_agent.rb index d1a12ecfa8..da65d7d3d4 100644 --- a/app/models/agents/telegram_agent.rb +++ b/app/models/agents/telegram_agent.rb @@ -9,14 +9,14 @@ class TelegramAgent < Agent 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 @@ def validate_auth_token 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 @@ def receive(incoming_events) 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 @@ def receive_event(event) 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 @@ def telegram_bot_uri(method) 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 diff --git a/app/models/agents/trigger_agent.rb b/app/models/agents/trigger_agent.rb index 9757beceb0..bf6ee7796d 100644 --- a/app/models/agents/trigger_agent.rb +++ b/app/models/agents/trigger_agent.rb @@ -3,9 +3,19 @@ class TriggerAgent < Agent cannot_be_scheduled! can_dry_run! - VALID_COMPARISON_TYPES = %w[regex !regex field=value field>value not\ in] - - description <<-MD + VALID_COMPARISON_TYPES = %w[ + regex + !regex + 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 @@ class TriggerAgent < Agent 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 @@ class TriggerAgent < Agent 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 @@ def default_options '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 @@ def working? def receive(incoming_events) incoming_events.each do |event| - opts = interpolated(event) match_results = opts['rules'].map do |rule| @@ -129,16 +143,16 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/tumblr_likes_agent.rb b/app/models/agents/tumblr_likes_agent.rb index 317d31e8d0..e3b96cd650 100644 --- a/app/models/agents/tumblr_likes_agent.rb +++ b/app/models/agents/tumblr_likes_agent.rb @@ -4,7 +4,7 @@ class TumblrLikesAgent < Agent 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 @@ class TumblrLikesAgent < Agent 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 @@ def check 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. diff --git a/app/models/agents/tumblr_publish_agent.rb b/app/models/agents/tumblr_publish_agent.rb index c72f879aa4..1dfdb21438 100644 --- a/app/models/agents/tumblr_publish_agent.rb +++ b/app/models/agents/tumblr_publish_agent.rb @@ -6,7 +6,7 @@ class TumblrPublishAgent < Agent 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 @@ class TumblrPublishAgent < Agent 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 @@ def receive(incoming_events) 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 @@ def receive(incoming_events) 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 @@ def publish_post(blog_name, post_type, options) 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 diff --git a/app/models/agents/twilio_agent.rb b/app/models/agents/twilio_agent.rb index ad444228a0..f55d9c41c9 100644 --- a/app/models/agents/twilio_agent.rb +++ b/app/models/agents/twilio_agent.rb @@ -8,7 +8,7 @@ class TwilioAgent < Agent 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 @@ def default_options '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 @@ def working? 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 @@ def post_url(server_url, secret) 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 diff --git a/app/models/agents/twilio_receive_text_agent.rb b/app/models/agents/twilio_receive_text_agent.rb index f22e980a10..5eb16b7694 100644 --- a/app/models/agents/twilio_receive_text_agent.rb +++ b/app/models/agents/twilio_receive_text_agent.rb @@ -5,20 +5,21 @@ class TwilioReceiveTextAgent < Agent 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 @@ def default_options { '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 @@ def receive_web_request(request) # 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 @@ def receive_web_request(request) 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 diff --git a/app/models/agents/twitter_action_agent.rb b/app/models/agents/twitter_action_agent.rb index 176a80563e..a5f958cd59 100644 --- a/app/models/agents/twitter_action_agent.rb +++ b/app/models/agents/twitter_action_agent.rb @@ -4,7 +4,7 @@ class TwitterActionAgent < Agent 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?} diff --git a/app/models/agents/twitter_favorites.rb b/app/models/agents/twitter_favorites.rb index 1dae8a8dbb..ac7f797445 100644 --- a/app/models/agents/twitter_favorites.rb +++ b/app/models/agents/twitter_favorites.rb @@ -5,7 +5,7 @@ class TwitterFavorites < Agent 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?} diff --git a/app/models/agents/twitter_publish_agent.rb b/app/models/agents/twitter_publish_agent.rb index 566f2c3575..2f6ddfe6c9 100644 --- a/app/models/agents/twitter_publish_agent.rb +++ b/app/models/agents/twitter_publish_agent.rb @@ -4,7 +4,7 @@ class TwitterPublishAgent < Agent 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 @@ class TwitterPublishAgent < Agent 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 diff --git a/app/models/agents/twitter_search_agent.rb b/app/models/agents/twitter_search_agent.rb index 4c95629eed..60ed9b9581 100644 --- a/app/models/agents/twitter_search_agent.rb +++ b/app/models/agents/twitter_search_agent.rb @@ -5,7 +5,7 @@ class TwitterSearchAgent < Agent 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?} diff --git a/app/models/agents/twitter_stream_agent.rb b/app/models/agents/twitter_stream_agent.rb index b496adad80..3722accdca 100644 --- a/app/models/agents/twitter_stream_agent.rb +++ b/app/models/agents/twitter_stream_agent.rb @@ -5,7 +5,7 @@ class TwitterStreamAgent < Agent 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 @@ def self.setup_worker 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 @@ def self.setup_worker 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 @@ def stream!(filters, agent, &block) 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, diff --git a/app/models/agents/twitter_user_agent.rb b/app/models/agents/twitter_user_agent.rb index bd2cb74689..b350bbdc9a 100644 --- a/app/models/agents/twitter_user_agent.rb +++ b/app/models/agents/twitter_user_agent.rb @@ -5,7 +5,7 @@ class TwitterUserAgent < Agent 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?} diff --git a/app/models/agents/user_location_agent.rb b/app/models/agents/user_location_agent.rb index 753c3a9b7b..5152fba76b 100644 --- a/app/models/agents/user_location_agent.rb +++ b/app/models/agents/user_location_agent.rb @@ -6,20 +6,21 @@ class UserLocationAgent < Agent 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 @@ def default_options 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 @@ def receive_web_request(params, method, format) handle_payload params.except(:secret) - return ['ok', 200] + ['ok', 200] end private @@ -87,7 +89,12 @@ def accurate_enough?(payload, accuracy_field) 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 @@ def far_enough?(payload) 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 diff --git a/app/models/agents/weather_agent.rb b/app/models/agents/weather_agent.rb index 177f5174ea..e68fd62b26 100644 --- a/app/models/agents/weather_agent.rb +++ b/app/models/agents/weather_agent.rb @@ -7,24 +7,23 @@ class WeatherAgent < Agent 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 @@ def default_options 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 @@ def language interpolated["language"].presence || "en" end - def wunderground? + def wunderground? interpolated["service"].presence && interpolated["service"].presence.downcase == "wunderground" end @@ -112,12 +111,14 @@ def validate_location :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 @@ def dark_sky 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 @@ def model(which_day) 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 @@ def model(which_day) '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 @@ def model(which_day) }, '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 @@ def model(which_day) }, '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 @@ def model(which_day) 'pressure' => value.pressure.to_s, 'ozone' => value.ozone.to_s } - return day end end end diff --git a/app/models/agents/webhook_agent.rb b/app/models/agents/webhook_agent.rb index 325ecb86cb..e4ad8b0ccc 100644 --- a/app/models/agents/webhook_agent.rb +++ b/app/models/agents/webhook_agent.rb @@ -6,16 +6,17 @@ class WebhookAgent < Agent 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 @@ class WebhookAgent < Agent 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 @@ def receive_web_request(request) 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 @@ def receive_web_request(request) 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 diff --git a/app/models/agents/website_agent.rb b/app/models/agents/website_agent.rb index fe057b47de..3784975456 100644 --- a/app/models/agents/website_agent.rb +++ b/app/models/agents/website_agent.rb @@ -14,7 +14,7 @@ class WebsiteAgent < Agent 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 @@ def working? 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 @@ def validate_http_success_codes! 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 @@ def validate_extract_options! 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 @@ def validate_extract_options! 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 @@ def validate_extract_options! 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 @@ def validate_extract_options! 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 @@ def validate_extract_options! 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 @@ def check_url(url, existing_payload = {}) 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 @@ def handle_data(body, url, existing_payload) 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 @@ def receive(incoming_events) end private + def consider_response_successful?(response) response.success? || begin consider_success = options["http_success_codes"] @@ -497,7 +507,7 @@ def handle_event_data(data, event, existing_payload) 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 @@ def use_namespaces? 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 @@ def extract_xml(doc) 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 @@ def []=(key, value) if @size && @size != size raise UnevenSizeError, 'got an uneven size' end + @size = size end @@ -684,7 +695,7 @@ def []=(key, value) 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 @@ def url 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 diff --git a/app/models/agents/weibo_publish_agent.rb b/app/models/agents/weibo_publish_agent.rb index 073a511fdc..f0320d0006 100644 --- a/app/models/agents/weibo_publish_agent.rb +++ b/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 @@ class WeiboPublishAgent < Agent 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 @@ def receive(incoming_events) 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 @@ def receive(incoming_events) '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 @@ def receive(incoming_events) 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 diff --git a/app/models/agents/weibo_user_agent.rb b/app/models/agents/weibo_user_agent.rb index 295dd14a71..0b1756b4bd 100644 --- a/app/models/agents/weibo_user_agent.rb +++ b/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 @@ class WeiboUserAgent < 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 are the raw JSON provided by the Weibo API. Should look something like: { @@ -72,7 +70,7 @@ class WeiboUserAgent < Agent 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 @@ def default_options 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 \ No newline at end of file +end diff --git a/app/models/agents/witai_agent.rb b/app/models/agents/witai_agent.rb index bff5b8b0db..b5da15c350 100644 --- a/app/models/agents/witai_agent.rb +++ b/app/models/agents/witai_agent.rb @@ -3,43 +3,42 @@ class WitaiAgent < Agent 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 @@ def receive(incoming_events) 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 diff --git a/app/models/event.rb b/app/models/event.rb index 1b9735ef12..ada52f9bad 100644 --- a/app/models/event.rb +++ b/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 @@ def location=(location) 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 @@ def update_agent_last_event_at 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 @@ def _location_ 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 diff --git a/app/models/link.rb b/app/models/link.rb index 0cda8cb1c4..d202b0c403 100644 --- a/app/models/link.rb +++ b/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 diff --git a/app/models/scenario.rb b/app/models/scenario.rb index c6a053870d..2da90f2003 100644 --- a/app/models/scenario.rb +++ b/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 @@ def destroy_with_mode(mode) 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 diff --git a/app/models/scenario_membership.rb b/app/models/scenario_membership.rb index a9ea3048fb..03019caa12 100644 --- a/app/models/scenario_membership.rb +++ b/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 diff --git a/app/models/service.rb b/app/models/service.rb index 84df77ffa0..48bfb8b83a 100644 --- a/app/models/service.rb +++ b/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 @@ def disable_agents(conditions = {}) 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 @@ def prepare_request 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 @@ def self.initialize_or_update_via_omniauth(omniauth) 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 @@ def self.get_options(omniauth) 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| diff --git a/app/models/user.rb b/app/models/user.rb index 256b689736..f83924ee93 100644 --- a/app/models/user.rb +++ b/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 @@ def available_services 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 @@ def requires_no_invitation_code? 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 diff --git a/app/models/user_credential.rb b/app/models/user_credential.rb index ffa3c38729..a507b992cf 100644 --- a/app/models/user_credential.rb +++ b/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 diff --git a/app/presenters/form_configurable_agent_presenter.rb b/app/presenters/form_configurable_agent_presenter.rb index 4e8f983004..1dd8137e93 100644 --- a/app/presenters/form_configurable_agent_presenter.rb +++ b/app/presenters/form_configurable_agent_presenter.rb @@ -16,14 +16,15 @@ def initialize(agent, view) 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 @@ def option_field_for(attribute) 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 diff --git a/lib/location.rb b/lib/location.rb index e6db847659..7cdb928e59 100644 --- a/lib/location.rb +++ b/lib/location.rb @@ -13,6 +13,7 @@ def initialize(data = {}) 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 @@ def latlng def floatify(value) case value when nil, '' - return nil + nil else float = Float(value) if block_given? diff --git a/spec/controllers/agents/dry_runs_controller_spec.rb b/spec/controllers/agents/dry_runs_controller_spec.rb index 77550b775b..6e6aa28cc4 100644 --- a/spec/controllers/agents/dry_runs_controller_spec.rb +++ b/spec/controllers/agents/dry_runs_controller_spec.rb @@ -16,7 +16,7 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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 @@ def valid_attributes(options = {}) 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) diff --git a/spec/features/create_an_agent_spec.rb b/spec/features/create_an_agent_spec.rb index 04eb297877..f5103a3379 100644 --- a/spec/features/create_an_agent_spec.rb +++ b/spec/features/create_an_agent_spec.rb @@ -114,7 +114,9 @@ "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 diff --git a/spec/models/agent_spec.rb b/spec/models/agent_spec.rb index 2737f0df34..a0622e63ae 100644 --- a/spec/models/agent_spec.rb +++ b/spec/models/agent_spec.rb @@ -33,7 +33,7 @@ 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 @@ class Agents::SomethingSource < Agent default_schedule "2pm" def check - create_event :payload => {} + create_event payload: {} end def validate_options @@ -154,8 +154,8 @@ class Agents::CannotBeScheduled < Agent 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) 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 @@ def receive(events) @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 @@ def receive(events) 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 @@ class Agents::WebRequestReceiver < Agent 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 @@ def @agent.receive_web_request(params, method, format) 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 @@ def @agent.receive_webhook(params) 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 @@ def @agent.receive_webhook(params) 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 @@ def @agent.receive_webhook(params) 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 @@ def @agent.receive_webhook(params) 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 @@ def interpolate(string, agent) }, }, schedule: 'every_1h', - keep_events_for: 2.days) + keep_events_for: 2.days + ) @wsa1.user = users(:bob) @wsa1.save! @@ -996,7 +1004,8 @@ def interpolate(string, agent) }, }, schedule: 'every_12h', - keep_events_for: 2.days) + keep_events_for: 2.days + ) @wsa2.user = users(:bob) @wsa2.save! @@ -1012,7 +1021,8 @@ def interpolate(string, agent) 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 @@ def interpolate(string, agent) 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 diff --git a/spec/models/agents/shell_command_agent_spec.rb b/spec/models/agents/shell_command_agent_spec.rb index 0b82a44fb8..92f44e5770 100644 --- a/spec/models/agents/shell_command_agent_spec.rb +++ b/spec/models/agents/shell_command_agent_spec.rb @@ -12,7 +12,8 @@ @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 @@ 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 @@ 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 "#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 From bb15eea85c1eb11c8f90b0dab963deb073decaad Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:02:26 +0900 Subject: [PATCH 03/32] Update for Ruby 3.2 --- .github/workflows/ci.yml | 2 +- Gemfile | 65 ++++++++++++++++++++-------------------- Gemfile.lock | 2 +- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1307da6f..2ad7783fd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - mysql2 - postgresql ruby: - - "2.7" + - "3.2" env: DATABASE_ADAPTER: ${{ matrix.database_adapter }} DATABASE_HOST: "127.0.0.1" diff --git a/Gemfile b/Gemfile index edfceba9e8..0190dd76fa 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '>=2.7.0' +ruby '>=3.2.2' # Ensure github repositories are fetched using HTTPS git_source(:github) do |repo_name| @@ -29,18 +29,18 @@ end # Optional libraries. To conserve RAM, comment out any that you don't need, # then run `bundle` and commit the updated Gemfile and Gemfile.lock. -gem 'twilio-ruby', '~> 5.62.0' # TwilioAgent -gem 'ruby-growl', '~> 4.1.0' # GrowlAgent -gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent -gem 'forecast_io', '~> 2.0.0' # WeatherAgent -gem 'rturk', '~> 2.12.1' # HumanTaskAgent gem 'erector', github: 'dsander/erector', branch: 'rails6' +gem 'forecast_io', '~> 2.0.0' # WeatherAgent gem 'hipchat', '~> 1.2.0' # HipchatAgent +gem 'hypdf', '~> 1.0.10' # PDFInfoAgent gem 'mini_racer' # JavaScriptAgent -gem 'xmpp4r', '~> 0.5.6' # JabberAgent gem 'mqtt' # MQTTAgent +gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent +gem 'rturk', '~> 2.12.1' # HumanTaskAgent +gem 'ruby-growl', '~> 4.1.0' # GrowlAgent gem 'slack-notifier', '~> 1.0.0' # SlackAgent -gem 'hypdf', '~> 1.0.10' # PDFInfoAgent +gem 'twilio-ruby', '~> 5.62.0' # TwilioAgent +gem 'xmpp4r', '~> 0.5.6' # JabberAgent # Weibo Agents # FIXME needs to loosen omniauth dependency, add rest-client @@ -51,14 +51,15 @@ gem 'google-api-client', '~> 0.13' gem 'google-cloud-translate', '~> 2.0', require: 'google/cloud/translate' # Twitter Agents +gem 'omniauth-twitter' gem 'twitter', github: 'sferik/twitter' # Must to be loaded before cantino-twitter-stream. gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'huginn' -gem 'omniauth-twitter' # Tumblr Agents # until merge of https://github.com/tumblr/tumblr_client/pull/61 -gem 'tumblr_client', github: 'albertsun/tumblr_client', branch: 'master', ref: 'e046fe6e39291c173add0a49081630c7b60a36c7' gem 'omniauth-tumblr' +gem 'tumblr_client', github: 'albertsun/tumblr_client', branch: 'master', + ref: 'e046fe6e39291c173add0a49081630c7b60a36c7' # Dropbox Agents gem 'dropbox-api', github: 'dsander/dropbox-api', ref: '86cb7b5a1254dc5b054de7263835713c4c1018c7' @@ -68,8 +69,8 @@ gem 'omniauth-dropbox-oauth2', github: 'huginn/omniauth-dropbox-oauth2' gem 'haversine' # EvernoteAgent -gem 'omniauth-evernote' gem 'evernote_oauth' +gem 'omniauth-evernote' # LocalFileAgent (watch functionality) gem 'listen', '~> 3.0.5', require: false @@ -78,17 +79,18 @@ gem 'listen', '~> 3.0.5', require: false gem 'aws-sdk-s3', '~> 1' # ImapFolderAgent -gem 'omniauth-google-oauth2', '>= 0.8.0' gem 'gmail_xoauth' # support for Gmail using OAuth +gem 'omniauth-google-oauth2', '>= 0.8.0' # Bundler <1.5 does not recognize :x64_mingw as a valid platform name. # Unfortunately, it can't self-update because it errors when encountering :x64_mingw. unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0') - STDERR.puts "Bundler >=1.5.0 is required. Please upgrade bundler with 'gem install bundler'" + warn "Bundler >=1.5.0 is required. Please upgrade bundler with 'gem install bundler'" exit 1 end gem 'ace-rails-ap', '~> 2.0.1' +gem 'bootsnap', require: false gem 'bootstrap-kaminari-views', '~> 0.0.3' gem 'bundler', '>= 1.5.0' gem 'coffee-rails', '~> 5' @@ -97,6 +99,7 @@ gem 'delayed_job' gem 'delayed_job_active_record' gem 'devise', '~> 4.8' gem 'em-http-request', '~> 1.1.2' +gem 'execjs' gem 'faraday', '~> 0.9' gem 'faraday_middleware', '~> 0.12.2' gem 'feedjira', '~> 3.1' @@ -104,10 +107,10 @@ gem 'font-awesome-sass', '~> 4.7.0' gem 'foreman', '~> 0.63.0' gem 'geokit', '~> 1.13' gem 'geokit-rails', '~> 2.3' -gem 'httparty', '~> 0.13' gem 'httmultiparty', '~> 0.3.16' -gem 'jquery-rails', '~> 4.2.1' +gem 'httparty', '~> 0.13' gem 'huginn_agent' +gem 'jquery-rails', '~> 4.2.1' gem 'json', '~> 2.3' gem 'jsonpath', '~> 1.1' gem 'kaminari', '~> 1.2' @@ -120,16 +123,14 @@ gem 'multi_xml' gem "nokogiri", ">= 1.10.8" gem 'omniauth' gem 'rails', '~> 6.1.7' -gem 'sprockets', '~> 3.7.2' gem 'rails-html-sanitizer', '~> 1.2' gem 'rufus-scheduler', '~> 3.4', require: false gem 'sass-rails', '>= 6.0' gem 'select2-rails', '~> 3.5.4' gem 'spectrum-rails' -gem 'execjs' +gem 'sprockets', '~> 3.7.2' gem 'typhoeus', '~> 1.3.1' gem 'uglifier', '~> 2.7.2' -gem 'bootsnap', require: false group :development do gem 'better_errors' @@ -137,39 +138,40 @@ group :development do gem 'guard' gem 'guard-livereload' gem 'guard-rspec' - gem 'rack-livereload' gem 'letter_opener_web', '~> 1.4' # 2.0+ requires Ruby 2.7 + gem 'rack-livereload' gem 'web-console', '>= 3.3.0' gem 'capistrano' - gem 'capistrano-rails' gem 'capistrano-bundler' + gem 'capistrano-rails' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false if_true(ENV['SPRING']) do - gem 'spring-commands-rspec' gem 'spring' + gem 'spring-commands-rspec' gem 'spring-watcher-listen' end group :test do - gem 'coveralls', require: false gem 'capybara', '~> 2.18' gem 'capybara-screenshot' - gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', require: false + gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', + require: false + gem 'coveralls', require: false gem 'poltergeist' - gem 'pry-rails' gem 'pry-byebug' + gem 'pry-rails' + gem 'rails-controller-testing' gem 'rr', require: false gem 'rspec' - gem 'rspec-mocks' - gem 'rspec-rails' gem 'rspec-collection_matchers' gem 'rspec-html-matchers' - gem 'rails-controller-testing' + gem 'rspec-mocks' + gem 'rspec-rails' gem 'shoulda-matchers' gem 'vcr' gem 'webmock', '~> 3.5.1' @@ -182,18 +184,17 @@ end # Platform requirements. require 'rbconfig' -gem 'ffi', '>= 1.9.4' # required by typhoeus; 1.9.4 has fixes for *BSD. +gem 'ffi', '>= 1.9.4' # required by typhoeus; 1.9.4 has fixes for *BSD. gem 'tzinfo', '>= 1.2.0' # required by rails; 1.2.0 has support for *BSD and Solaris. # Windows does not have zoneinfo files, so bundle the tzinfo-data gem. gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] # BSD systems require rb-kqueue for "listen" to avoid polling for changes. gem 'rb-kqueue', '>= 0.2', require: /bsd|dragonfly/i === RbConfig::CONFIG['target_os'] - on_heroku = ENV['ON_HEROKU'] || - ENV['HEROKU_POSTGRESQL_ROSE_URL'] || - ENV['HEROKU_POSTGRESQL_GOLD_URL'] || - File.read(File.join(File.dirname(__FILE__), 'Procfile')) =~ /intended for Heroku/ + ENV['HEROKU_POSTGRESQL_ROSE_URL'] || + ENV['HEROKU_POSTGRESQL_GOLD_URL'] || + File.read(File.join(File.dirname(__FILE__), 'Procfile')) =~ /intended for Heroku/ ENV['DATABASE_ADAPTER'] ||= if on_heroku diff --git a/Gemfile.lock b/Gemfile.lock index 7cb1fd85ba..56a4307cff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -916,7 +916,7 @@ DEPENDENCIES xmpp4r (~> 0.5.6) RUBY VERSION - ruby 2.7.6p219 + ruby 3.2.2p53 BUNDLED WITH 2.4.12 From 0f1cd41d054e8c75b3c6e9bd632dd737cbbd3b3d Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:04:04 +0900 Subject: [PATCH 04/32] Use the official ruby image based on Ubuntu 22.04 --- docker/multi-process/Dockerfile | 2 +- docker/scripts/prepare | 4 +--- docker/single-process/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docker/multi-process/Dockerfile b/docker/multi-process/Dockerfile index baa71c80e6..55993b734d 100644 --- a/docker/multi-process/Dockerfile +++ b/docker/multi-process/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:18.04 +FROM rubylang/ruby:3.2-jammy COPY docker/scripts/prepare /scripts/ RUN /scripts/prepare diff --git a/docker/scripts/prepare b/docker/scripts/prepare index 23257202f3..ce1712085e 100755 --- a/docker/scripts/prepare +++ b/docker/scripts/prepare @@ -23,13 +23,11 @@ minimal_apt_get_install='apt-get install -y --no-install-recommends' apt-get update apt-get dist-upgrade -y --no-install-recommends $minimal_apt_get_install software-properties-common -add-apt-repository -y ppa:brightbox/ruby-ng-experimental -apt-get update $minimal_apt_get_install build-essential checkinstall git-core \ zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev \ libncurses5-dev libffi-dev libxml2-dev libxslt-dev curl libcurl4-openssl-dev libicu-dev \ graphviz libmysqlclient-dev libpq-dev libsqlite3-dev \ - ruby2.7 ruby2.7-dev locales tzdata shared-mime-info iputils-ping + locales tzdata shared-mime-info iputils-ping locale-gen en_US.UTF-8 update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 # Specific version 3.3.20: 3.3.21 changes platform support, breaks our build diff --git a/docker/single-process/Dockerfile b/docker/single-process/Dockerfile index d040de283f..efe4cf2037 100644 --- a/docker/single-process/Dockerfile +++ b/docker/single-process/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:18.04 +FROM rubylang/ruby:3.2-jammy COPY docker/scripts/prepare /scripts/ RUN /scripts/prepare From 056a2f5e07433d958f9281b4af05527e8bdea195 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:35:15 +0900 Subject: [PATCH 05/32] List the net-ftp gem --- Gemfile | 3 ++- Gemfile.lock | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 0190dd76fa..71a1eb6e2f 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,8 @@ gem 'hipchat', '~> 1.2.0' # HipchatAgent gem 'hypdf', '~> 1.0.10' # PDFInfoAgent gem 'mini_racer' # JavaScriptAgent gem 'mqtt' # MQTTAgent -gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent +gem 'net-ftp' +gem 'net-ftp-list' # FtpsiteAgent gem 'rturk', '~> 2.12.1' # HumanTaskAgent gem 'ruby-growl', '~> 4.1.0' # GrowlAgent gem 'slack-notifier', '~> 1.0.0' # SlackAgent diff --git a/Gemfile.lock b/Gemfile.lock index 56a4307cff..7ec764e5e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -490,6 +490,9 @@ GEM mysql2 (0.5.4) naught (1.1.0) nenv (0.3.0) + net-ftp (0.2.0) + net-protocol + time net-ftp-list (3.2.8) net-imap (0.3.4) date @@ -744,6 +747,8 @@ GEM thor (1.2.1) thread_safe (0.3.6) tilt (2.0.10) + time (0.2.2) + date timeout (0.3.1) tins (1.31.0) sync @@ -862,7 +867,8 @@ DEPENDENCIES mqtt multi_xml mysql2 (~> 0.5) - net-ftp-list (~> 3.2.8) + net-ftp + net-ftp-list nokogiri (>= 1.10.8) omniauth omniauth-dropbox-oauth2! From bf4dac959157655e19c9ea4db0368e295efa4494 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:38:41 +0900 Subject: [PATCH 06/32] Delete GrowlAgent The ruby-growl gem no longer works and Growl is obsolete. --- Gemfile | 1 - Gemfile.lock | 8 -- app/models/agents/growl_agent.rb | 93 -------------- ...731191002_migrate_growl_agent_to_liquid.rb | 17 +-- spec/models/agents/growl_agent_spec.rb | 119 ------------------ 5 files changed, 2 insertions(+), 236 deletions(-) delete mode 100644 app/models/agents/growl_agent.rb delete mode 100644 spec/models/agents/growl_agent_spec.rb diff --git a/Gemfile b/Gemfile index 71a1eb6e2f..e6089d2624 100644 --- a/Gemfile +++ b/Gemfile @@ -38,7 +38,6 @@ gem 'mqtt' # MQTTAgent gem 'net-ftp' gem 'net-ftp-list' # FtpsiteAgent gem 'rturk', '~> 2.12.1' # HumanTaskAgent -gem 'ruby-growl', '~> 4.1.0' # GrowlAgent gem 'slack-notifier', '~> 1.0.0' # SlackAgent gem 'twilio-ruby', '~> 5.62.0' # TwilioAgent gem 'xmpp4r', '~> 0.5.6' # JabberAgent diff --git a/Gemfile.lock b/Gemfile.lock index 7ec764e5e3..9116c15a0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -458,8 +458,6 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.2.8) - macaddr (1.7.1) - systemu (~> 2.6.2) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -685,8 +683,6 @@ GEM rubocop-rspec (2.18.1) rubocop (~> 1.33) rubocop-capybara (~> 2.17) - ruby-growl (4.1) - uuid (~> 2.3, >= 2.3.5) ruby-progressbar (1.13.0) rufus-scheduler (3.8.1) fugit (~> 1.1, >= 1.1.6) @@ -741,7 +737,6 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) sync (0.5.0) - systemu (2.6.4) term-ansicolor (1.7.1) tins (~> 1.0) thor (1.2.1) @@ -774,8 +769,6 @@ GEM unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) - uuid (2.3.7) - macaddr (~> 1.0) vcr (3.0.3) warden (1.2.9) rack (>= 2.0.9) @@ -895,7 +888,6 @@ DEPENDENCIES rubocop rubocop-performance rubocop-rspec - ruby-growl (~> 4.1.0) rufus-scheduler (~> 3.4) sass-rails (>= 6.0) select2-rails (~> 3.5.4) diff --git a/app/models/agents/growl_agent.rb b/app/models/agents/growl_agent.rb deleted file mode 100644 index cad3e79d58..0000000000 --- a/app/models/agents/growl_agent.rb +++ /dev/null @@ -1,93 +0,0 @@ -module Agents - class GrowlAgent < Agent - include FormConfigurable - attr_reader :growler - - cannot_be_scheduled! - cannot_create_events! - can_dry_run! - - gem_dependency_check { defined?(Growl) } - - 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?} - - The option `message`, which will hold the body of the growl notification, and the `subject` option, - which will have the headline of the Growl notification are required. All other options are optional. - When `callback_url` is set to a URL clicking on the notification will open the link in your default browser. - - 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. - - Have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) to learn - more about liquid templating. - MD - - 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' - } - end - - form_configurable :growl_server - form_configurable :growl_password - form_configurable :growl_app_name - form_configurable :growl_notification_name - form_configurable :expected_receive_period_in_days - form_configurable :subject - form_configurable :message, type: :text - form_configurable :sticky, type: :boolean - form_configurable :priority - form_configurable :callback_url - - def working? - last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs? - end - - def validate_options - unless options['growl_server'].present? && options['expected_receive_period_in_days'].present? - errors.add(:base, "growl_server and expected_receive_period_in_days are required fields") - end - end - - def register_growl - @growler = Growl::GNTP.new(interpolated['growl_server'], interpolated['growl_app_name']) - @growler.password = interpolated['growl_password'] - @growler.add_notification(interpolated['growl_notification_name']) - end - - def notify_growl(subject:, message:, priority:, sticky:, callback_url:) - @growler.notify(interpolated['growl_notification_name'], subject, message, priority, sticky, nil, callback_url) - end - - def receive(incoming_events) - incoming_events.each do |event| - interpolate_with(event) do - register_growl - message = interpolated[:message] - 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:, - message:, - priority: interpolated[:priority].to_i, - sticky: boolify(interpolated[:sticky]) || false, - callback_url: interpolated[:callback_url].presence) - else - log "Event #{event.id} not sent, message and subject expected" - end - end - end - end - end -end diff --git a/db/migrate/20170731191002_migrate_growl_agent_to_liquid.rb b/db/migrate/20170731191002_migrate_growl_agent_to_liquid.rb index 2a75e0ba16..87df9c91cf 100644 --- a/db/migrate/20170731191002_migrate_growl_agent_to_liquid.rb +++ b/db/migrate/20170731191002_migrate_growl_agent_to_liquid.rb @@ -1,18 +1,5 @@ class MigrateGrowlAgentToLiquid < ActiveRecord::Migration[5.1] - def up - Agents::GrowlAgent.find_each do |agent| - agent.options['subject'] = '{{subject}}' if agent.options['subject'].blank? - agent.options['message'] = '{{ message | default: text }}' if agent.options['message'].blank? - agent.save(validate: false) - end - end - - def down - Agents::GrowlAgent.find_each do |agent| - %w(subject message sticky priority).each do |key| - agent.options.delete(key) - end - agent.save(validate: false) - end + def change + # Agents::GrowlAgent is no more end end diff --git a/spec/models/agents/growl_agent_spec.rb b/spec/models/agents/growl_agent_spec.rb deleted file mode 100644 index c8487243a5..0000000000 --- a/spec/models/agents/growl_agent_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'rails_helper' - -describe Agents::GrowlAgent do - before do - @checker = Agents::GrowlAgent.new(:name => 'a growl agent', - :options => { :growl_server => 'localhost', - :growl_app_name => 'HuginnGrowlApp', - :growl_password => 'mypassword', - :growl_notification_name => 'Notification', - expected_receive_period_in_days: '1' , - message: '{{message}}', - subject: '{{subject}}'}) - @checker.user = users(:bob) - @checker.save! - - allow_any_instance_of(Growl::GNTP).to receive(:notify) - - @event = Event.new - @event.agent = agents(:bob_weather_agent) - @event.payload = { :subject => 'Weather Alert!', :message => 'Looks like its going to rain' } - @event.save! - end - - describe "#working?" do - it "checks if events have been received within the expected receive period" do - expect(@checker).not_to be_working # No events received - Agents::GrowlAgent.async_receive @checker.id, [@event.id] - expect(@checker.reload).to be_working # Just received events - two_days_from_now = 2.days.from_now - allow(Time).to receive(:now) { two_days_from_now } - expect(@checker.reload).not_to be_working # More time has passed than the expected receive period without any new events - end - end - - describe "validation" do - before do - expect(@checker).to be_valid - end - - it "should validate presence of of growl_server" do - @checker.options[:growl_server] = "" - expect(@checker).not_to be_valid - end - - it "should validate presence of expected_receive_period_in_days" do - @checker.options[:expected_receive_period_in_days] = "" - expect(@checker).not_to be_valid - end - end - - describe "register_growl" do - it "should set the password for the Growl connection from the agent options" do - @checker.register_growl - expect(@checker.growler.password).to eql(@checker.options[:growl_password]) - end - - it "should add a notification to the Growl connection" do - expect(Growl::GNTP).to receive(:new).and_wrap_original do |orig, *args| - orig.call(*args).tap { |obj| - expect(obj).to receive(:add_notification).with(@checker.options[:growl_notification_name]) - } - end - @checker.register_growl - end - end - - describe "notify_growl" do - it "should call Growl.notify with the correct notification name, subject, and message" do - message = "message" - subject = "subject" - expect(Growl::GNTP).to receive(:new).and_wrap_original do |orig, *args| - orig.call(*args).tap { |obj| - expect(obj).to receive(:notify).with(@checker.options[:growl_notification_name], subject, message, 0, false, nil, '') - } - end - @checker.register_growl - @checker.notify_growl(subject: subject, message: message, sticky: false, priority: 0, callback_url: '') - end - end - - describe "receive" do - def generate_events_array - events = [] - (2..rand(7)).each do - events << @event - end - return events - end - - it "should call register_growl once per received event" do - events = generate_events_array - expect(@checker).to receive(:register_growl).exactly(events.length).times.and_call_original - @checker.receive(events) - end - - it "should call notify_growl one time for each event received" do - events = generate_events_array - events.each do |event| - expect(@checker).to receive(:notify_growl).with(subject: event.payload['subject'], message: event.payload['message'], priority: 0, sticky: false, callback_url: nil) - end - @checker.receive(events) - end - - it "should not call notify_growl if message or subject are missing" do - event_without_a_subject = Event.new - event_without_a_subject.agent = agents(:bob_weather_agent) - event_without_a_subject.payload = { :message => 'Looks like its going to rain' } - event_without_a_subject.save! - - event_without_a_message = Event.new - event_without_a_message.agent = agents(:bob_weather_agent) - event_without_a_message.payload = { :subject => 'Weather Alert YO!' } - event_without_a_message.save! - - expect(@checker).not_to receive(:notify_growl) - @checker.receive([event_without_a_subject,event_without_a_message]) - end - end -end From ab23a64938fdea04e192e71678d337a0a7795a9a Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:41:33 +0900 Subject: [PATCH 07/32] Delete pry --- Gemfile | 2 -- Gemfile.lock | 8 -------- 2 files changed, 10 deletions(-) diff --git a/Gemfile b/Gemfile index e6089d2624..4c4b817d54 100644 --- a/Gemfile +++ b/Gemfile @@ -163,8 +163,6 @@ group :development do require: false gem 'coveralls', require: false gem 'poltergeist' - gem 'pry-byebug' - gem 'pry-rails' gem 'rails-controller-testing' gem 'rr', require: false gem 'rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 9116c15a0f..1c3fccedfe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -187,7 +187,6 @@ GEM rails (>= 3.1) buftok (0.2.0) builder (3.2.4) - byebug (11.1.3) capistrano (3.16.0) airbrussh (>= 1.0.0) i18n @@ -566,11 +565,6 @@ GEM pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.0) raabro (1.4.0) racc (1.6.2) @@ -871,8 +865,6 @@ DEPENDENCIES omniauth-twitter pg (~> 1.1) poltergeist - pry-byebug - pry-rails rack-livereload rails (~> 6.1.7) rails-controller-testing From 8ad1af42dd25cbd40aca0c59a8a13f20a2786118 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 02:50:24 +0900 Subject: [PATCH 08/32] Use the created_event in log --- app/models/agents/de_duplication_agent.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/agents/de_duplication_agent.rb b/app/models/agents/de_duplication_agent.rb index dd956b5398..fdda960f84 100644 --- a/app/models/agents/de_duplication_agent.rb +++ b/app/models/agents/de_duplication_agent.rb @@ -56,9 +56,13 @@ def receive(incoming_events) 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 + outbound_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, + outbound_event: + ) update_memory(property, opts['lookback'].to_i) else log("Not propagating as incoming event is a duplicate.", inbound_event: event) From 709c11ce7e24e428af755c0efd938fa963a22292 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 03:00:16 +0900 Subject: [PATCH 09/32] Use the keyword parameter syntax --- app/models/agents/local_file_agent.rb | 14 ++++++--- app/models/agents/shell_command_agent.rb | 2 +- .../20140813110107_set_charset_for_mysql.rb | 4 +-- .../models/agents/shell_command_agent_spec.rb | 29 ++++++++++--------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/models/agents/local_file_agent.rb b/app/models/agents/local_file_agent.rb index b51fe6fca4..c42e0cf054 100644 --- a/app/models/agents/local_file_agent.rb +++ b/app/models/agents/local_file_agent.rb @@ -154,7 +154,8 @@ def should_run?(log = true) class Worker < LongRunnable::Worker def setup require 'listen' - @listener = Listen.to(*listen_options, &method(:callback)) + path, options = listen_options + @listener = Listen.to(path, **options, &method(:callback)) end def run @@ -183,10 +184,15 @@ def callback(*changes) 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 diff --git a/app/models/agents/shell_command_agent.rb b/app/models/agents/shell_command_agent.rb index 6a3d965415..7e827cb3d4 100644 --- a/app/models/agents/shell_command_agent.rb +++ b/app/models/agents/shell_command_agent.rb @@ -97,7 +97,7 @@ def handle(opts, event = nil) path = opts['path'] stdin = opts['stdin'] - result, errors, exit_status = run_command(path, command, stdin, interpolated.slice(:unbundle).symbolize_keys) + result, errors, exit_status = run_command(path, command, stdin, **interpolated.slice(:unbundle).symbolize_keys) payload = { 'command' => command, diff --git a/db/migrate/20140813110107_set_charset_for_mysql.rb b/db/migrate/20140813110107_set_charset_for_mysql.rb index 35feda3ea6..6c1fb33568 100644 --- a/db/migrate/20140813110107_set_charset_for_mysql.rb +++ b/db/migrate/20140813110107_set_charset_for_mysql.rb @@ -29,7 +29,7 @@ def change type = column.type limit = column.limit options = { - limit: limit, + limit:, null: column.null, default: column.default, } @@ -53,7 +53,7 @@ def change next end - change_column table_name, name, type, options + change_column table_name, name, type, **options } execute 'ALTER TABLE %s CHARACTER SET utf8 COLLATE utf8_unicode_ci' % table_name diff --git a/spec/models/agents/shell_command_agent_spec.rb b/spec/models/agents/shell_command_agent_spec.rb index 92f44e5770..800f2ad314 100644 --- a/spec/models/agents/shell_command_agent_spec.rb +++ b/spec/models/agents/shell_command_agent_spec.rb @@ -61,7 +61,7 @@ describe "#working?" do it "generating events as scheduled" 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, 'pwd', nil) { ["fake pwd output", "", 0] } expect(@checker).not_to be_working @checker.check @@ -75,17 +75,18 @@ describe "#check" do before do orig_run_command = @checker.method(:run_command) - 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, '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.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] - } - end + orig_run_command.call(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: true) + } + allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil) { + [ENV['BUNDLE_GEMFILE'].to_s, "", 0] + } + allow(@checker).to receive(:run_command).with(@valid_path, 'echo $BUNDLE_GEMFILE', nil, unbundle: false) { + [ENV['BUNDLE_GEMFILE'].to_s, "", 0] + } end it "should create an event when checking" do @@ -177,9 +178,9 @@ 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 From bd9e11427d7c9c5cd0982afe3efe9568b373f9bd Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 03:13:28 +0900 Subject: [PATCH 10/32] Replace the obsolete coveralls gem with coverallsapp/github-action@v1 --- .github/workflows/ci.yml | 3 +++ Gemfile | 6 ++++-- Gemfile.lock | 24 ++++++++---------------- spec/rails_helper.rb | 14 +++++++++++--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ad7783fd5..4a55f7a504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,9 @@ jobs: - name: Run tests run: bundle exec rake + - name: Coveralls + uses: coverallsapp/github-action@v1 + ghcr-build-docker-images: name: ghcr-docker-build-${{ matrix.docker_image }} needs: run-tests diff --git a/Gemfile b/Gemfile index 4c4b817d54..8087eb7c14 100644 --- a/Gemfile +++ b/Gemfile @@ -159,9 +159,9 @@ group :development do group :test do gem 'capybara', '~> 2.18' gem 'capybara-screenshot' - gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', + gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', + ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', require: false - gem 'coveralls', require: false gem 'poltergeist' gem 'rails-controller-testing' gem 'rr', require: false @@ -171,6 +171,8 @@ group :development do gem 'rspec-mocks' gem 'rspec-rails' gem 'shoulda-matchers' + gem 'simplecov', require: false + gem 'simplecov-lcov', '~> 0.8.0', require: false gem 'vcr' gem 'webmock', '~> 3.5.1' end diff --git a/Gemfile.lock b/Gemfile.lock index 1c3fccedfe..04ba3c7a15 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,12 +218,6 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.2.0) cookiejar (0.3.3) - coveralls (0.8.23) - json (>= 1.8, < 3) - simplecov (~> 0.16.1) - term-ansicolor (~> 1.3) - thor (>= 0.19.4, < 2.0) - tins (~> 1.6) crack (0.4.5) rexml crass (1.0.6) @@ -706,11 +700,13 @@ GEM jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simple_oauth (0.3.1) - simplecov (0.16.1) + simplecov (0.22.0) docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov-lcov (0.8.0) + simplecov_json_formatter (0.1.4) slack-notifier (1.0.0) spectrum-rails (1.3.4) railties (>= 3.1) @@ -730,17 +726,12 @@ GEM sshkit (1.21.2) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - sync (0.5.0) - term-ansicolor (1.7.1) - tins (~> 1.0) thor (1.2.1) thread_safe (0.3.6) tilt (2.0.10) time (0.2.2) date timeout (0.3.1) - tins (1.31.0) - sync trailblazer-option (0.1.2) treetop (1.6.10) polyglot (~> 0.3) @@ -806,7 +797,6 @@ DEPENDENCIES capybara-screenshot capybara-select-2! coffee-rails (~> 5) - coveralls daemons (~> 1.1.9) delayed_job delayed_job_active_record @@ -884,6 +874,8 @@ DEPENDENCIES sass-rails (>= 6.0) select2-rails (~> 3.5.4) shoulda-matchers + simplecov + simplecov-lcov (~> 0.8.0) slack-notifier (~> 1.0.0) spectrum-rails spring diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 22c342a8ad..f821104eb2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -4,11 +4,19 @@ require 'simplecov' SimpleCov.start 'rails' elsif ENV['CI'] == 'true' - require 'coveralls' - Coveralls.wear!('rails') + require 'simplecov' + SimpleCov.start 'rails' do + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.single_report_path = 'coverage/lcov.info' + end + formatter SimpleCov::Formatter::LcovFormatter + add_filter %w[version.rb initializer.rb] + end end -require File.expand_path("../../config/environment", __FILE__) +require File.expand_path('../config/environment', __dir__) require 'rspec/rails' require 'webmock/rspec' From c1e43265189f9563c177b97d4a049ddcff30402b Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 03:40:48 +0900 Subject: [PATCH 11/32] Use the keyword parameter syntax --- app/concerns/dry_runnable.rb | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/app/concerns/dry_runnable.rb b/app/concerns/dry_runnable.rb index 3e7cf448be..023c1e287b 100644 --- a/app/concerns/dry_runnable.rb +++ b/app/concerns/dry_runnable.rb @@ -21,23 +21,25 @@ def dry_run!(event = nil) begin raise "#{short_type} does not support dry-run" unless can_dry_run? + readonly! @dry_run_started_at = Time.zone.now @dry_run_logger.info('Dry Run started') if event raise "This agent cannot receive an event!" unless can_receive_events? + receive([event]) else check end @dry_run_logger.info('Dry Run finished') - rescue => e + rescue StandardError => e @dry_run_logger.info('Dry Run failed') error "Exception during dry-run. #{e.message}: #{e.backtrace.join("\n")}" end @dry_run_results.update( - memory: memory, + memory:, log: log.string, ) ensure @@ -57,35 +59,41 @@ module Wrapper def logger return super unless dry_run? + @dry_run_logger end - def save(options = {}) + def save(**options) return super unless dry_run? + perform_validations(options) end - def save!(options = {}) + def save!(**options) return super unless dry_run? - save(options) or raise_record_invalid + + save(**options) or raise_record_invalid end def log(message, options = {}) return super unless dry_run? - case options[:level] || 3 - when 0..2 - sev = Logger::DEBUG - when 3 - sev = Logger::INFO - else - sev = Logger::ERROR - end + + sev = + case options[:level] || 3 + when 0..2 + Logger::DEBUG + when 3 + Logger::INFO + else + Logger::ERROR + end logger.log(sev, message) end def create_event(event) return super unless dry_run? + if can_create_events? event = build_event(event) @dry_run_results[:events] << event.payload From 953d0287a5fe0b8777369ccde6bcb7afae413edf Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 04:40:53 +0900 Subject: [PATCH 12/32] Upgrade Sprockets to 4 and decaffinate --- Gemfile | 6 +- Gemfile.lock | 22 +- app/assets/config/manifest.js | 4 + app/assets/javascripts/ace.js | 1 + app/assets/javascripts/ace.js.coffee | 8 - app/assets/javascripts/application.js | 13 + app/assets/javascripts/application.js.coffee | 13 - app/assets/javascripts/components/core.js | 56 +++ .../javascripts/components/core.js.coffee | 36 -- .../components/form_configurable.js | 127 +++++++ .../components/form_configurable.js.coffee | 81 ----- .../components/json-editor.js.coffee.erb | 14 - .../javascripts/components/json-editor.js.erb | 22 ++ app/assets/javascripts/components/search.js | 51 +++ .../javascripts/components/search.js.coffee | 29 -- app/assets/javascripts/components/utils.js | 221 ++++++++++++ .../javascripts/components/utils.js.coffee | 138 ------- .../javascripts/components/worker-checker.js | 90 +++++ .../components/worker-checker.js.coffee | 51 --- app/assets/javascripts/diagram.js | 29 ++ app/assets/javascripts/diagram.js.coffee | 23 -- app/assets/javascripts/graphing.js | 76 ++++ app/assets/javascripts/graphing.js.coffee | 62 ---- app/assets/javascripts/map_marker.js | 48 +++ app/assets/javascripts/map_marker.js.coffee | 43 --- .../javascripts/pages/agent-edit-page.js | 340 ++++++++++++++++++ .../pages/agent-edit-page.js.coffee | 231 ------------ .../javascripts/pages/agent-show-page.js | 112 ++++++ .../pages/agent-show-page.js.coffee | 68 ---- .../javascripts/pages/scenario-form-page.js | 23 ++ .../pages/scenario-form-page.js.coffee | 15 - .../javascripts/pages/scenario-show-page.js | 24 ++ .../pages/scenario-show-page.js.coffee | 15 - .../javascripts/pages/user-credential-page.js | 33 ++ .../pages/user-credential-page.js.coffee | 25 -- app/assets/javascripts/tweets.js | 15 + app/assets/javascripts/tweets.js.coffee | 11 - app/helpers/dot_helper.rb | 10 +- config/initializers/assets.rb | 8 - 39 files changed, 1304 insertions(+), 890 deletions(-) create mode 100644 app/assets/config/manifest.js create mode 100644 app/assets/javascripts/ace.js delete mode 100644 app/assets/javascripts/ace.js.coffee create mode 100644 app/assets/javascripts/application.js delete mode 100644 app/assets/javascripts/application.js.coffee create mode 100644 app/assets/javascripts/components/core.js delete mode 100644 app/assets/javascripts/components/core.js.coffee create mode 100644 app/assets/javascripts/components/form_configurable.js delete mode 100644 app/assets/javascripts/components/form_configurable.js.coffee delete mode 100644 app/assets/javascripts/components/json-editor.js.coffee.erb create mode 100644 app/assets/javascripts/components/json-editor.js.erb create mode 100644 app/assets/javascripts/components/search.js delete mode 100644 app/assets/javascripts/components/search.js.coffee create mode 100644 app/assets/javascripts/components/utils.js delete mode 100644 app/assets/javascripts/components/utils.js.coffee create mode 100644 app/assets/javascripts/components/worker-checker.js delete mode 100644 app/assets/javascripts/components/worker-checker.js.coffee create mode 100644 app/assets/javascripts/diagram.js delete mode 100644 app/assets/javascripts/diagram.js.coffee create mode 100644 app/assets/javascripts/graphing.js delete mode 100644 app/assets/javascripts/graphing.js.coffee create mode 100644 app/assets/javascripts/map_marker.js delete mode 100644 app/assets/javascripts/map_marker.js.coffee create mode 100644 app/assets/javascripts/pages/agent-edit-page.js delete mode 100644 app/assets/javascripts/pages/agent-edit-page.js.coffee create mode 100644 app/assets/javascripts/pages/agent-show-page.js delete mode 100644 app/assets/javascripts/pages/agent-show-page.js.coffee create mode 100644 app/assets/javascripts/pages/scenario-form-page.js delete mode 100644 app/assets/javascripts/pages/scenario-form-page.js.coffee create mode 100644 app/assets/javascripts/pages/scenario-show-page.js delete mode 100644 app/assets/javascripts/pages/scenario-show-page.js.coffee create mode 100644 app/assets/javascripts/pages/user-credential-page.js delete mode 100644 app/assets/javascripts/pages/user-credential-page.js.coffee create mode 100644 app/assets/javascripts/tweets.js delete mode 100644 app/assets/javascripts/tweets.js.coffee diff --git a/Gemfile b/Gemfile index 8087eb7c14..7c8a93e7af 100644 --- a/Gemfile +++ b/Gemfile @@ -89,7 +89,7 @@ unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0') exit 1 end -gem 'ace-rails-ap', '~> 2.0.1' +gem 'ace-rails-ap' gem 'bootsnap', require: false gem 'bootstrap-kaminari-views', '~> 0.0.3' gem 'bundler', '>= 1.5.0' @@ -128,7 +128,7 @@ gem 'rufus-scheduler', '~> 3.4', require: false gem 'sass-rails', '>= 6.0' gem 'select2-rails', '~> 3.5.4' gem 'spectrum-rails' -gem 'sprockets', '~> 3.7.2' +gem 'sprockets' gem 'typhoeus', '~> 1.3.1' gem 'uglifier', '~> 2.7.2' @@ -157,7 +157,7 @@ group :development do end group :test do - gem 'capybara', '~> 2.18' + gem 'capybara' gem 'capybara-screenshot' gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', diff --git a/Gemfile.lock b/Gemfile.lock index 04ba3c7a15..f877b24da0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,7 +91,7 @@ PATH GEM remote: https://rubygems.org/ specs: - ace-rails-ap (2.0.1) + ace-rails-ap (4.5) actioncable (6.1.7.2) actionpack (= 6.1.7.2) activesupport (= 6.1.7.2) @@ -151,7 +151,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.1) + addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) @@ -216,7 +216,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) cookiejar (0.3.3) crack (0.4.5) rexml @@ -559,13 +559,13 @@ GEM pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (5.0.0) + public_suffix (5.0.1) raabro (1.4.0) racc (1.6.2) rack (2.2.6.4) rack-livereload (0.3.17) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) rails (6.1.7.2) actioncable (= 6.1.7.2) @@ -716,9 +716,9 @@ GEM spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) spring (>= 4) - sprockets (3.7.2) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -771,7 +771,7 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xmpp4r (0.5.6) - xpath (3.0.0) + xpath (3.2.0) nokogiri (~> 1.8) zeitwerk (2.6.7) @@ -783,7 +783,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - ace-rails-ap (~> 2.0.1) + ace-rails-ap aws-sdk-s3 (~> 1) better_errors binding_of_caller @@ -793,7 +793,7 @@ DEPENDENCIES capistrano capistrano-bundler capistrano-rails - capybara (~> 2.18) + capybara capybara-screenshot capybara-select-2! coffee-rails (~> 5) @@ -881,7 +881,7 @@ DEPENDENCIES spring spring-commands-rspec spring-watcher-listen - sprockets (~> 3.7.2) + sprockets tumblr_client! twilio-ruby (~> 5.62.0) twitter! diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000000..6453ef86db --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,4 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../javascripts .coffee +//= link_directory ../stylesheets .css diff --git a/app/assets/javascripts/ace.js b/app/assets/javascripts/ace.js new file mode 100644 index 0000000000..7ca68dcfa3 --- /dev/null +++ b/app/assets/javascripts/ace.js @@ -0,0 +1 @@ +//= require ace-rails-ap diff --git a/app/assets/javascripts/ace.js.coffee b/app/assets/javascripts/ace.js.coffee deleted file mode 100644 index 1b9d6f4866..0000000000 --- a/app/assets/javascripts/ace.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -#= require ace/ace -#= require ace/mode-javascript.js -#= require ace/mode-markdown.js -#= require ace/mode-coffee.js -#= require ace/mode-sql.js -#= require ace/mode-json.js -#= require ace/mode-yaml.js -#= require ace/mode-text.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000000..28e2e537ee --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,13 @@ +//= require jquery +//= require rails-ujs +//= require typeahead.bundle +//= require bootstrap +//= require select2 +//= require json2 +//= require jquery.json-editor +//= require jquery.serializeObject +//= require latlon_and_geo +//= require spectrum +//= require_tree ./components +//= require_tree ./pages +//= require_self diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee deleted file mode 100644 index 564fe9a695..0000000000 --- a/app/assets/javascripts/application.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -#= require jquery -#= require rails-ujs -#= require typeahead.bundle -#= require bootstrap -#= require select2 -#= require json2 -#= require jquery.json-editor -#= require jquery.serializeObject -#= require latlon_and_geo -#= require spectrum -#= require_tree ./components -#= require_tree ./pages -#= require_self diff --git a/app/assets/javascripts/components/core.js b/app/assets/javascripts/components/core.js new file mode 100644 index 0000000000..1cedbd21f9 --- /dev/null +++ b/app/assets/javascripts/components/core.js @@ -0,0 +1,56 @@ +$(function () { + // Flash + if ($(".flash").length) { + setTimeout(() => $(".flash").slideUp(() => $(".flash").remove()), 5000); + } + + // Help popovers + $(".hover-help").popover({ trigger: "hover", html: true }); + + // Pressing '/' selects the search box. + $("body").on("keypress", function (e) { + if (e.keyCode === 47) { + // The '/' key + if (e.target.nodeName === "BODY") { + e.preventDefault(); + return $agentNavigate.focus(); + } + } + }); + + // Select2 Selects + $(".select2").select2({ width: "resolve" }); + + $(".select2-linked-tags").select2({ + width: "resolve", + formatSelection(obj) { + return `${Utils.escape( + obj.text + )}`; + }, + }); + + // Helper for selecting text when clicked + $(".selectable-text").each(function () { + return $(this).click(function () { + const range = document.createRange(); + range.setStartBefore(this.firstChild); + range.setEndAfter(this.lastChild); + const sel = window.getSelection(); + sel.removeAllRanges(); + return sel.addRange(range); + }); + }); + + // Agent navbar dropdown + return $(".navbar .dropdown.dropdown-hover").hover( + function () { + return $(this).addClass("open"); + }, + function () { + return $(this).removeClass("open"); + } + ); +}); diff --git a/app/assets/javascripts/components/core.js.coffee b/app/assets/javascripts/components/core.js.coffee deleted file mode 100644 index aea4e1ef6b..0000000000 --- a/app/assets/javascripts/components/core.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -$ -> - # Flash - if $(".flash").length - setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) - - # Help popovers - $('.hover-help').popover(trigger: 'hover', html: true) - - # Pressing '/' selects the search box. - $("body").on "keypress", (e) -> - if e.keyCode == 47 # The '/' key - if e.target.nodeName == "BODY" - e.preventDefault() - $agentNavigate.focus() - - # Select2 Selects - $(".select2").select2(width: 'resolve') - - $(".select2-linked-tags").select2( - width: 'resolve', - formatSelection: (obj) -> - "#{Utils.escape(obj.text)}" - ) - - # Helper for selecting text when clicked - $('.selectable-text').each -> - $(this).click -> - range = document.createRange() - range.setStartBefore(this.firstChild) - range.setEndAfter(this.lastChild) - sel = window.getSelection() - sel.removeAllRanges(); - sel.addRange(range) - - # Agent navbar dropdown - $('.navbar .dropdown.dropdown-hover').hover (-> $(this).addClass('open')), (-> $(this).removeClass('open')) \ No newline at end of file diff --git a/app/assets/javascripts/components/form_configurable.js b/app/assets/javascripts/components/form_configurable.js new file mode 100644 index 0000000000..f47aef8f5d --- /dev/null +++ b/app/assets/javascripts/components/form_configurable.js @@ -0,0 +1,127 @@ +$(function () { + const getFormData = function (elem) { + const form_data = $("#edit_agent, #new_agent").serializeObject(); + const attribute = $(elem).data("attribute"); + form_data["attribute"] = attribute; + delete form_data["_method"]; + return form_data; + }; + + return (window.initializeFormCompletable = function () { + let returnedResults = {}; + const completableDefaultOptions = (input) => ({ + results: [ + returnedResults[$(input).data("attribute")] || { + text: "Options", + children: [{ id: undefined, text: "loading ..." }], + }, + { + text: "Current", + children: [{ id: $(input).val(), text: $(input).val() }], + }, + { + text: "Custom", + children: [{ id: "manualInput", text: "manual input" }], + }, + ], + }); + + $("input[role~=validatable], select[role~=validatable]").on( + "change", + (e) => { + const form_data = getFormData(e.currentTarget); + const form_group = $(e.currentTarget).closest(".form-group"); + return $.ajax("/agents/validate", { + type: "POST", + data: form_data, + success: (data) => { + form_group.addClass("has-feedback").removeClass("has-error"); + form_group.find("span").addClass("hidden"); + form_group.find(".glyphicon-ok").removeClass("hidden"); + return (returnedResults = {}); + }, + error: (data) => { + form_group.addClass("has-feedback").addClass("has-error"); + form_group.find("span").addClass("hidden"); + form_group.find(".glyphicon-remove").removeClass("hidden"); + return (returnedResults = {}); + }, + }); + } + ); + + $("input[role~=validatable], select[role~=validatable]").trigger("change"); + + $.each($("input[role~=completable]"), (i, input) => + $(input) + .select2({ + data: () => completableDefaultOptions(input), + }) + .on("change", function (e) { + if (e.added && e.added.id === "manualInput") { + $(e.currentTarget).select2("destroy"); + return $(e.currentTarget).val(e.removed.id); + } + }) + ); + + const updateDropdownData = function (form_data, element, data) { + returnedResults[form_data.attribute] = { + text: "Options", + children: data, + }; + $(element).trigger("change"); + $("input[role~=completable]").off( + "select2-opening", + select2OpeningCallback + ); + $(element).select2("open"); + return $("input[role~=completable]").on( + "select2-opening", + select2OpeningCallback + ); + }; + + var select2OpeningCallback = function (e) { + const form_data = getFormData(e.currentTarget); + if ( + returnedResults[form_data.attribute] && + !$(e.currentTarget).data("cacheResponse") + ) { + delete returnedResults[form_data.attribute]; + } + if (returnedResults[form_data.attribute]) { + return; + } + + return $.ajax("/agents/complete", { + type: "POST", + data: form_data, + success: (data) => { + return updateDropdownData(form_data, e.currentTarget, data); + }, + error: (data) => { + return updateDropdownData(form_data, e.currentTarget, [ + { id: undefined, text: "Error loading data." }, + ]); + }, + }); + }; + + $("input[role~=completable]").on("select2-opening", select2OpeningCallback); + + return $("input[type=radio][role~=form-configurable]").change(function (e) { + const input = $(e.currentTarget) + .parents() + .siblings( + `input[data-attribute=${$(e.currentTarget).data("attribute")}]` + ); + if ($(e.currentTarget).val() === "manual") { + return input.removeClass("hidden"); + } else { + input.val($(e.currentTarget).val()); + return input.addClass("hidden"); + } + }); + }); +}); diff --git a/app/assets/javascripts/components/form_configurable.js.coffee b/app/assets/javascripts/components/form_configurable.js.coffee deleted file mode 100644 index ac6f8512fc..0000000000 --- a/app/assets/javascripts/components/form_configurable.js.coffee +++ /dev/null @@ -1,81 +0,0 @@ -$ -> - getFormData = (elem) -> - form_data = $("#edit_agent, #new_agent").serializeObject() - attribute = $(elem).data('attribute') - form_data['attribute'] = attribute - delete form_data['_method'] - form_data - - window.initializeFormCompletable = -> - returnedResults = {} - completableDefaultOptions = (input) -> - results: [ - (returnedResults[$(input).data('attribute')] || {text: 'Options', children: [{id: undefined, text: 'loading ...'}]}), - { - text: 'Current', - children: [id: $(input).val(), text: $(input).val()] - }, - { - text: 'Custom', - children: [id: 'manualInput', text: 'manual input'] - }, - ] - - $("input[role~=validatable], select[role~=validatable]").on 'change', (e) => - form_data = getFormData(e.currentTarget) - form_group = $(e.currentTarget).closest('.form-group') - $.ajax '/agents/validate', - type: 'POST', - data: form_data - success: (data) -> - form_group.addClass('has-feedback').removeClass('has-error') - form_group.find('span').addClass('hidden') - form_group.find('.glyphicon-ok').removeClass('hidden') - returnedResults = {} - error: (data) -> - form_group.addClass('has-feedback').addClass('has-error') - form_group.find('span').addClass('hidden') - form_group.find('.glyphicon-remove').removeClass('hidden') - returnedResults = {} - - $("input[role~=validatable], select[role~=validatable]").trigger('change') - - $.each $("input[role~=completable]"), (i, input) -> - $(input).select2( - data: -> - completableDefaultOptions(input) - ).on("change", (e) -> - if e.added && e.added.id == 'manualInput' - $(e.currentTarget).select2("destroy") - $(e.currentTarget).val(e.removed.id) - ) - - updateDropdownData = (form_data, element, data) -> - returnedResults[form_data.attribute] = {text: 'Options', children: data} - $(element).trigger('change') - $("input[role~=completable]").off 'select2-opening', select2OpeningCallback - $(element).select2('open') - $("input[role~=completable]").on 'select2-opening', select2OpeningCallback - - select2OpeningCallback = (e) -> - form_data = getFormData(e.currentTarget) - delete returnedResults[form_data.attribute] if returnedResults[form_data.attribute] && !$(e.currentTarget).data('cacheResponse') - return if returnedResults[form_data.attribute] - - $.ajax '/agents/complete', - type: 'POST', - data: form_data - success: (data) -> - updateDropdownData(form_data, e.currentTarget, data) - error: (data) -> - updateDropdownData(form_data, e.currentTarget, [{id: undefined, text: 'Error loading data.'}]) - - $("input[role~=completable]").on 'select2-opening', select2OpeningCallback - - $("input[type=radio][role~=form-configurable]").change (e) -> - input = $(e.currentTarget).parents().siblings("input[data-attribute=#{$(e.currentTarget).data('attribute')}]") - if $(e.currentTarget).val() == 'manual' - input.removeClass('hidden') - else - input.val($(e.currentTarget).val()) - input.addClass('hidden') diff --git a/app/assets/javascripts/components/json-editor.js.coffee.erb b/app/assets/javascripts/components/json-editor.js.coffee.erb deleted file mode 100644 index c9a10cb08d..0000000000 --- a/app/assets/javascripts/components/json-editor.js.coffee.erb +++ /dev/null @@ -1,14 +0,0 @@ -window.setupJsonEditor = ($editors = $(".live-json-editor")) -> - JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' - JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' - editors = [] - $editors.each -> - $editor = $(this) - jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500) - jsonEditor.doTruncation true - jsonEditor.showFunctionButtons() - editors.push jsonEditor - return editors - -$ -> - window.jsonEditor = setupJsonEditor()[0] diff --git a/app/assets/javascripts/components/json-editor.js.erb b/app/assets/javascripts/components/json-editor.js.erb new file mode 100644 index 0000000000..519ad5b276 --- /dev/null +++ b/app/assets/javascripts/components/json-editor.js.erb @@ -0,0 +1,22 @@ +window.setupJsonEditor = function ($editors) { + if ($editors == null) { + $editors = $(".live-json-editor"); + } + JSONEditor.prototype.ADD_IMG = "<%= image_path 'json-editor/add.png' %>"; + JSONEditor.prototype.DELETE_IMG = "<%= image_path 'json-editor/delete.png' %>"; + const editors = []; + $editors.each(function () { + const $editor = $(this); + const jsonEditor = new JSONEditor( + $editor, + $editor.data("width") || 400, + $editor.data("height") || 500 + ); + jsonEditor.doTruncation(true); + jsonEditor.showFunctionButtons(); + return editors.push(jsonEditor); + }); + return editors; +}; + +$(() => (window.jsonEditor = setupJsonEditor()[0])); diff --git a/app/assets/javascripts/components/search.js b/app/assets/javascripts/components/search.js new file mode 100644 index 0000000000..e0213a05ce --- /dev/null +++ b/app/assets/javascripts/components/search.js @@ -0,0 +1,51 @@ +$(function () { + const $agentNavigate = $("#agent-navigate"); + + // initialize typeahead listener + $agentNavigate.bind("typeahead:selected", function (event, object, name) { + const item = object["value"]; + $agentNavigate.typeahead("val", ""); + if (window.agentPaths[item]) { + $(".spinner").show(); + const navigationData = window.agentPaths[item]; + if ( + !(navigationData instanceof Object) || + !navigationData.method || + navigationData.method === "GET" + ) { + return (window.location = navigationData.url || navigationData); + } else { + return $.rails.handleMethod.apply( + $( + `` + ) + .appendTo($("body")) + .get(0) + ); + } + } + }); + + // substring matcher for typeahead + const substringMatcher = function (strings) { + let findMatches; + return (findMatches = function (query, callback) { + const matches = []; + const substrRegex = new RegExp(query, "i"); + $.each(strings, function (i, str) { + if (substrRegex.test(str)) { + return matches.push({ value: str }); + } + }); + return callback(matches.slice(0, 6)); + }); + }; + + return $agentNavigate.typeahead( + { + minLength: 1, + highlight: true, + }, + { source: substringMatcher(window.agentNames) } + ); +}); diff --git a/app/assets/javascripts/components/search.js.coffee b/app/assets/javascripts/components/search.js.coffee deleted file mode 100644 index f8c2519abd..0000000000 --- a/app/assets/javascripts/components/search.js.coffee +++ /dev/null @@ -1,29 +0,0 @@ -$ -> - $agentNavigate = $('#agent-navigate') - - # initialize typeahead listener - $agentNavigate.bind "typeahead:selected", (event, object, name) -> - item = object['value'] - $agentNavigate.typeahead('val', '') - if window.agentPaths[item] - $(".spinner").show() - navigationData = window.agentPaths[item] - if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET' - window.location = navigationData.url || navigationData - else - $.rails.handleMethod.apply $("").appendTo($("body")).get(0) - - # substring matcher for typeahead - substringMatcher = (strings) -> - findMatches = (query, callback) -> - matches = [] - substrRegex = new RegExp(query, "i") - $.each strings, (i, str) -> - matches.push value: str if substrRegex.test(str) - callback(matches.slice(0,6)) - - $agentNavigate.typeahead - minLength: 1, - highlight: true, - , - source: substringMatcher(window.agentNames) diff --git a/app/assets/javascripts/components/utils.js b/app/assets/javascripts/components/utils.js new file mode 100644 index 0000000000..d86bdc0f6b --- /dev/null +++ b/app/assets/javascripts/components/utils.js @@ -0,0 +1,221 @@ +(function () { + let escapeMap = undefined; + let createEscaper = undefined; + const Cls = (this.Utils = class Utils { + static initClass() { + // _.escape from underscore: https://github.com/jashkenas/underscore/blob/1e68f06610fa4ecb7f2c45d1eb2ad0173d6a2cc1/underscore.js#L1411-L1436 + escapeMap = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`", + }; + + createEscaper = function (map) { + const escaper = (match) => map[match]; + + // Regexes for identifying a key that needs to be escaped. + const source = "(?:" + Object.keys(map).join("|") + ")"; + const testRegexp = RegExp(source); + const replaceRegexp = RegExp(source, "g"); + return function (string) { + string = string === null ? "" : "" + string; + if (testRegexp.test(string)) { + return string.replace(replaceRegexp, escaper); + } else { + return string; + } + }; + }; + + this.escape = createEscaper(escapeMap); + } + static navigatePath(path) { + if (!path.match(/^\//)) { + path = "/" + path; + } + return (window.location.href = path); + } + + static currentPath() { + return window.location.href.replace(/https?:\/\/.*?\//g, ""); + } + + static registerPage(klass, options) { + if (options == null) { + options = {}; + } + if (options.forPathsMatching != null) { + if (Utils.currentPath().match(options.forPathsMatching)) { + return (window.currentPage = new klass()); + } + } else { + return new klass(); + } + } + + static showDynamicModal(content, param) { + if (content == null) { + content = ""; + } + if (param == null) { + param = {}; + } + const { title, body, onHide } = param; + $("body").append(`\ +\ +`); + const modal = document.querySelector("#dynamic-modal"); + $(modal) + .find(".modal-title") + .text(title || "") + .end() + .on("hidden.bs.modal", function () { + $("#dynamic-modal").remove(); + return typeof onHide === "function" ? onHide() : undefined; + }); + if (typeof body === "function") { + body(modal.querySelector(".modal-body")); + } + return $(modal).modal("show"); + } + + static handleDryRunButton(button, data) { + if (data == null) { + data = button.form + ? $(':input[name!="_method"]', button.form).serialize() + : ""; + } + $(button).prop("disabled", true); + const cleanup = () => $(button).prop("disabled", false); + + const url = $(button).data("action-url"); + const with_event_mode = $(button).data("with-event-mode"); + + if (with_event_mode === "no") { + return this.invokeDryRun(url, data, cleanup); + } + return $.ajax(url, { + method: "GET", + data: { + with_event_mode, + source_ids: $.map($(".link-region select option:selected"), (el) => + $(el).val() + ), + }, + success: (modal_data) => { + return Utils.showDynamicModal(modal_data, { + body: (body) => { + let previous; + const form = $(body).find(".dry-run-form"); + const payload_editor = form.find(".payload-editor"); + + if ((previous = $(button).data("payload"))) { + payload_editor.text(previous); + } + + const editor = window.setupJsonEditor(payload_editor)[0]; + + $(body) + .find(".dry-run-event-sample") + .click((e) => { + e.preventDefault(); + editor.json = $(e.currentTarget).data("payload"); + return editor.rebuild(); + }); + + form.submit((e) => { + let dry_run_data; + e.preventDefault(); + let json = $(e.target).find(".payload-editor").val(); + if (json === "") { + json = "{}"; + } + try { + const payload = JSON.parse( + json.replace(/\\\\([n|r|t])/g, "\\$1") + ); + if (payload.constructor !== Object) { + throw true; + } + if (Object.keys(payload).length === 0) { + json = ""; + } else { + json = JSON.stringify(payload); + } + } catch (error) { + alert("Invalid JSON object."); + return; + } + if (json === "") { + if (with_event_mode === "yes") { + alert("Event is required for this agent to run."); + return; + } + dry_run_data = data; + $(button).data("payload", null); + } else { + dry_run_data = `event=${encodeURIComponent(json)}&${data}`; + $(button).data("payload", json); + } + return $(body) + .closest("[role=dialog]") + .on("hidden.bs.modal", () => { + return this.invokeDryRun(url, dry_run_data, cleanup); + }) + .modal("hide"); + }); + return $(body) + .closest("[role=dialog]") + .on("shown.bs.modal", function () { + return $(this).find(".btn-primary").focus(); + }); + }, + title: "Dry Run", + onHide: cleanup, + }); + }, + }); + } + + static invokeDryRun(url, data, callback) { + $("body").css({ cursor: "progress" }); + return $.ajax({ type: "POST", url, dataType: "html", data }) + .always(() => { + return $("body").css({ cursor: "auto" }); + }) + .done((modal_data) => { + return Utils.showDynamicModal(modal_data, { + title: "Dry Run Results", + onHide: callback, + }); + }) + .fail(function (xhr, status, error) { + alert("Error: " + error); + return callback(); + }); + } + + static select2TagClickHandler(e, elem) { + if (e.which === 1) { + return (window.location = $(elem).attr("href")); + } else { + return window.open($(elem).attr("href")); + } + } + }); + Cls.initClass(); + return Cls; +})(); diff --git a/app/assets/javascripts/components/utils.js.coffee b/app/assets/javascripts/components/utils.js.coffee deleted file mode 100644 index b53ea9dd64..0000000000 --- a/app/assets/javascripts/components/utils.js.coffee +++ /dev/null @@ -1,138 +0,0 @@ -class @Utils - @navigatePath: (path) -> - path = "/" + path unless path.match(/^\//) - window.location.href = path - - @currentPath: -> - window.location.href.replace(/https?:\/\/.*?\//g, '') - - @registerPage: (klass, options = {}) -> - if options.forPathsMatching? - if Utils.currentPath().match(options.forPathsMatching) - window.currentPage = new klass() - else - new klass() - - @showDynamicModal: (content = '', { title, body, onHide } = {}) -> - $("body").append """ - - """ - modal = document.querySelector('#dynamic-modal') - $(modal).find('.modal-title').text(title || '').end().on 'hidden.bs.modal', -> - $('#dynamic-modal').remove() - onHide?() - body?(modal.querySelector('.modal-body')) - $(modal).modal('show') - - @handleDryRunButton: (button, data = if button.form then $(':input[name!="_method"]', button.form).serialize() else '') -> - $(button).prop('disabled', true) - cleanup = -> $(button).prop('disabled', false) - - url = $(button).data('action-url') - with_event_mode = $(button).data('with-event-mode') - - if with_event_mode is 'no' - return @invokeDryRun(url, data, cleanup) - $.ajax url, - method: 'GET', - data: - with_event_mode: with_event_mode - source_ids: $.map($(".link-region select option:selected"), (el) -> $(el).val() ) - success: (modal_data) => - Utils.showDynamicModal modal_data, - body: (body) => - form = $(body).find('.dry-run-form') - payload_editor = form.find('.payload-editor') - - if previous = $(button).data('payload') - payload_editor.text(previous) - - editor = window.setupJsonEditor(payload_editor)[0] - - $(body).find('.dry-run-event-sample').click (e) => - e.preventDefault() - editor.json = $(e.currentTarget).data('payload') - editor.rebuild() - - form.submit (e) => - e.preventDefault() - json = $(e.target).find('.payload-editor').val() - json = '{}' if json == '' - try - payload = JSON.parse(json.replace(/\\\\([n|r|t])/g, "\\$1")) - throw true unless payload.constructor is Object - if Object.keys(payload).length == 0 - json = '' - else - json = JSON.stringify(payload) - catch - alert 'Invalid JSON object.' - return - if json == '' - if with_event_mode is 'yes' - alert 'Event is required for this agent to run.' - return - dry_run_data = data - $(button).data('payload', null) - else - dry_run_data = "event=#{encodeURIComponent(json)}&#{data}" - $(button).data('payload', json) - $(body).closest('[role=dialog]').on 'hidden.bs.modal', => - @invokeDryRun(url, dry_run_data, cleanup) - .modal('hide') - $(body).closest('[role=dialog]').on 'shown.bs.modal', -> - $(this).find('.btn-primary').focus() - title: 'Dry Run' - onHide: cleanup - - @invokeDryRun: (url, data, callback) -> - $('body').css(cursor: 'progress') - $.ajax type: 'POST', url: url, dataType: 'html', data: data - .always => - $('body').css(cursor: 'auto') - .done (modal_data) => - Utils.showDynamicModal modal_data, - title: 'Dry Run Results', - onHide: callback - .fail (xhr, status, error) -> - alert('Error: ' + error) - callback() - - @select2TagClickHandler: (e, elem) -> - if e.which == 1 - window.location = $(elem).attr('href') - else - window.open($(elem).attr('href')) - - # _.escape from underscore: https://github.com/jashkenas/underscore/blob/1e68f06610fa4ecb7f2c45d1eb2ad0173d6a2cc1/underscore.js#L1411-L1436 - escapeMap = - '&': '&' - '<': '<' - '>': '>' - '"': '"' - '\'': ''' - '`': '`' - - createEscaper = (map) -> - escaper = (match) -> - map[match] - - # Regexes for identifying a key that needs to be escaped. - source = '(?:' + Object.keys(map).join('|') + ')' - testRegexp = RegExp(source) - replaceRegexp = RegExp(source, 'g') - (string) -> - string = if string == null then '' else '' + string - if testRegexp.test(string) then string.replace(replaceRegexp, escaper) else string - - @escape = createEscaper(escapeMap) diff --git a/app/assets/javascripts/components/worker-checker.js b/app/assets/javascripts/components/worker-checker.js new file mode 100644 index 0000000000..ffe5d1efd7 --- /dev/null +++ b/app/assets/javascripts/components/worker-checker.js @@ -0,0 +1,90 @@ +$(function () { + let sinceId = null; + let previousJobs = null; + + if ($(".job-indicator").length) { + var check = function () { + const query = sinceId != null ? "?since_id=" + sinceId : ""; + return $.getJSON("/worker_status" + query, function (json) { + for (var method of ["pending", "awaiting_retry", "recent_failures"]) { + var count = json[method]; + var elem = $(`.job-indicator[role=${method}]`); + if (count > 0) { + var tooltipOptions = { + title: `${count} jobs ${method.split("_").join(" ")}`, + delay: 0, + placement: "bottom", + trigger: "hover", + }; + if (elem.is(":visible")) { + elem + .tooltip("destroy") + .tooltip(tooltipOptions) + .find(".number") + .text(count); + } else { + elem + .tooltip("destroy") + .tooltip(tooltipOptions) + .fadeIn() + .find(".number") + .text(count); + } + } else { + if (elem.is(":visible")) { + elem.tooltip("destroy").fadeOut(); + } + } + } + + if (sinceId != null && json.event_count > 0) { + $("#event-indicator") + .tooltip("destroy") + .tooltip({ + title: "Click to see the events", + delay: 0, + placement: "bottom", + trigger: "hover", + }) + .find("a") + .attr({ href: json.events_url }) + .end() + .fadeIn() + .find(".number") + .text(json.event_count); + } else { + $("#event-indicator").tooltip("destroy").fadeOut(); + } + + if (sinceId == null) { + sinceId = json.max_id; + } + const currentJobs = [ + json.pending, + json.awaiting_retry, + json.recent_failures, + ]; + if ( + document.location.pathname === "/jobs" && + $(".modal[aria-hidden=false]").length === 0 && + previousJobs != null && + previousJobs.join(",") !== currentJobs.join(",") + ) { + if ( + !document.location.search || + document.location.search === "?page=1" + ) { + $.get("/jobs", (data) => { + return $("#main-content").html(data); + }); + } + } + previousJobs = currentJobs; + + return (window.workerCheckTimeout = setTimeout(check, 2000)); + }); + }; + + return check(); + } +}); diff --git a/app/assets/javascripts/components/worker-checker.js.coffee b/app/assets/javascripts/components/worker-checker.js.coffee deleted file mode 100644 index 2a267f14f7..0000000000 --- a/app/assets/javascripts/components/worker-checker.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -$ -> - sinceId = null - previousJobs = null - - if $(".job-indicator").length - check = -> - query = - if sinceId? - '?since_id=' + sinceId - else - '' - $.getJSON "/worker_status" + query, (json) -> - for method in ['pending', 'awaiting_retry', 'recent_failures'] - count = json[method] - elem = $(".job-indicator[role=#{method}]") - if count > 0 - tooltipOptions = { - title: "#{count} jobs #{method.split('_').join(' ')}" - delay: 0 - placement: "bottom" - trigger: "hover" - } - if elem.is(":visible") - elem.tooltip('destroy').tooltip(tooltipOptions).find(".number").text(count) - else - elem.tooltip('destroy').tooltip(tooltipOptions).fadeIn().find(".number").text(count) - else - if elem.is(":visible") - elem.tooltip('destroy').fadeOut() - - if sinceId? && json.event_count > 0 - $("#event-indicator").tooltip('destroy'). - tooltip(title: "Click to see the events", delay: 0, placement: "bottom", trigger: "hover"). - find('a').attr(href: json.events_url).end(). - fadeIn(). - find(".number"). - text(json.event_count) - else - $("#event-indicator").tooltip('destroy').fadeOut() - - sinceId ?= json.max_id - currentJobs = [json.pending, json.awaiting_retry, json.recent_failures] - if document.location.pathname == '/jobs' && $(".modal[aria-hidden=false]").length == 0 && previousJobs? && previousJobs.join(',') != currentJobs.join(',') - if !document.location.search || document.location.search == '?page=1' - $.get '/jobs', (data) => - $("#main-content").html(data) - previousJobs = currentJobs - - window.workerCheckTimeout = setTimeout check, 2000 - - check() diff --git a/app/assets/javascripts/diagram.js b/app/assets/javascripts/diagram.js new file mode 100644 index 0000000000..e8721bc49a --- /dev/null +++ b/app/assets/javascripts/diagram.js @@ -0,0 +1,29 @@ +// This is not included in the core application.js bundle. + +$(function () { + const svg = document.querySelector(".agent-diagram svg.diagram"); + const overlay = document.querySelector(".agent-diagram .overlay"); + $(overlay).width($(svg).width()).height($(svg).height()); + const getTopLeft = function (node) { + const bbox = node.getBBox(); + const point = svg.createSVGPoint(); + point.x = bbox.x + bbox.width; + point.y = bbox.y; + return point.matrixTransform(node.getCTM()); + }; + return $(svg) + .find("g.node[data-badge-id]") + .each(function () { + const tl = getTopLeft(this); + $("#" + this.getAttribute("data-badge-id"), overlay).each(function () { + const badge = $(this); + badge + .css({ + left: tl.x - badge.outerWidth() * (2 / 3), + top: tl.y - badge.outerHeight() * (1 / 3), + "background-color": badge.find(".label").css("background-color"), + }) + .show(); + }); + }); +}); diff --git a/app/assets/javascripts/diagram.js.coffee b/app/assets/javascripts/diagram.js.coffee deleted file mode 100644 index 70275f734d..0000000000 --- a/app/assets/javascripts/diagram.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -# This is not included in the core application.js bundle. - -$ -> - svg = document.querySelector('.agent-diagram svg.diagram') - overlay = document.querySelector('.agent-diagram .overlay') - $(overlay).width($(svg).width()).height($(svg).height()) - getTopLeft = (node) -> - bbox = node.getBBox() - point = svg.createSVGPoint() - point.x = bbox.x + bbox.width - point.y = bbox.y - point.matrixTransform(node.getCTM()) - $(svg).find('g.node[data-badge-id]').each -> - tl = getTopLeft(this) - $('#' + this.getAttribute('data-badge-id'), overlay).each -> - badge = $(this) - badge.css - left: tl.x - badge.outerWidth() * (2/3) - top: tl.y - badge.outerHeight() * (1/3) - 'background-color': badge.find('.label').css('background-color') - .show() - return - return diff --git a/app/assets/javascripts/graphing.js b/app/assets/javascripts/graphing.js new file mode 100644 index 0000000000..f1030784a3 --- /dev/null +++ b/app/assets/javascripts/graphing.js @@ -0,0 +1,76 @@ +//= require d3 +//= require rickshaw +//= require_self + +// This is not included in the core application.js bundle. + +window.renderGraph = function ($chart, data, peaks, name) { + const graph = new Rickshaw.Graph({ + element: $chart.find(".chart").get(0), + width: 700, + height: 240, + series: [ + { + data, + name, + color: "steelblue", + }, + ], + }); + + const x_axis = new Rickshaw.Graph.Axis.Time({ graph }); + + const annotator = new Rickshaw.Graph.Annotate({ + graph, + element: $chart.find(".timeline").get(0), + }); + $.each(peaks, function () { + return annotator.add(this, "Peak"); + }); + + const y_axis = new Rickshaw.Graph.Axis.Y({ + graph, + orientation: "left", + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + element: $chart.find(".y-axis").get(0), + }); + + graph.onUpdate(function () { + const mean = d3.mean(data, (i) => i.y); + const standard_deviation = Math.sqrt( + d3.mean(data.map((i) => Math.pow(i.y - mean, 2))) + ); + const minX = d3.min(data, (i) => i.x); + const maxX = d3.max(data, (i) => i.x); + graph.vis + .append("svg:line") + .attr("x1", graph.x(minX)) + .attr("x2", graph.x(maxX)) + .attr("y1", graph.y(mean)) + .attr("y2", graph.y(mean)) + .attr("class", "summary-statistic mean"); + graph.vis + .append("svg:line") + .attr("x1", graph.x(minX)) + .attr("x2", graph.x(maxX)) + .attr("y1", graph.y(mean + standard_deviation)) + .attr("y2", graph.y(mean + standard_deviation)) + .attr("class", "summary-statistic one-std"); + graph.vis + .append("svg:line") + .attr("x1", graph.x(minX)) + .attr("x2", graph.x(maxX)) + .attr("y1", graph.y(mean + 2 * standard_deviation)) + .attr("y2", graph.y(mean + 2 * standard_deviation)) + .attr("class", "summary-statistic two-std"); + return graph.vis + .append("svg:line") + .attr("x1", graph.x(minX)) + .attr("x2", graph.x(maxX)) + .attr("y1", graph.y(mean + 3 * standard_deviation)) + .attr("y2", graph.y(mean + 3 * standard_deviation)) + .attr("class", "summary-statistic three-std"); + }); + + return graph.render(); +}; diff --git a/app/assets/javascripts/graphing.js.coffee b/app/assets/javascripts/graphing.js.coffee deleted file mode 100644 index 21aac67224..0000000000 --- a/app/assets/javascripts/graphing.js.coffee +++ /dev/null @@ -1,62 +0,0 @@ -#= require d3 -#= require rickshaw -#= require_self - -# This is not included in the core application.js bundle. - -window.renderGraph = ($chart, data, peaks, name) -> - graph = new Rickshaw.Graph - element: $chart.find(".chart").get(0) - width: 700 - height: 240 - series: [ - data: data - name: name - color: 'steelblue' - ] - - x_axis = new Rickshaw.Graph.Axis.Time(graph: graph) - - annotator = new Rickshaw.Graph.Annotate - graph: graph - element: $chart.find(".timeline").get(0) - $.each peaks, -> - annotator.add this, "Peak" - - y_axis = new Rickshaw.Graph.Axis.Y - graph: graph - orientation: 'left' - tickFormat: Rickshaw.Fixtures.Number.formatKMBT - element: $chart.find(".y-axis").get(0) - - graph.onUpdate -> - mean = d3.mean data, (i) -> i.y - standard_deviation = Math.sqrt(d3.mean(data.map((i) -> Math.pow(i.y - mean, 2)))) - minX = d3.min data, (i) -> i.x - maxX = d3.max data, (i) -> i.x - graph.vis.append("svg:line") - .attr('x1', graph.x(minX)) - .attr('x2', graph.x(maxX)) - .attr('y1', graph.y(mean)) - .attr('y2', graph.y(mean)) - .attr 'class', 'summary-statistic mean' - graph.vis.append("svg:line") - .attr('x1', graph.x(minX)) - .attr('x2', graph.x(maxX)) - .attr('y1', graph.y(mean + standard_deviation)) - .attr('y2', graph.y(mean + standard_deviation)) - .attr 'class', 'summary-statistic one-std' - graph.vis.append("svg:line") - .attr('x1', graph.x(minX)) - .attr('x2', graph.x(maxX)) - .attr('y1', graph.y(mean + 2 * standard_deviation)) - .attr('y2', graph.y(mean + 2 * standard_deviation)) - .attr 'class', 'summary-statistic two-std' - graph.vis.append("svg:line") - .attr('x1', graph.x(minX)) - .attr('x2', graph.x(maxX)) - .attr('y1', graph.y(mean + 3 * standard_deviation)) - .attr('y2', graph.y(mean + 3 * standard_deviation)) - .attr 'class', 'summary-statistic three-std' - - graph.render() diff --git a/app/assets/javascripts/map_marker.js b/app/assets/javascripts/map_marker.js new file mode 100644 index 0000000000..c599c27cf6 --- /dev/null +++ b/app/assets/javascripts/map_marker.js @@ -0,0 +1,48 @@ +window.map_marker = function (map, options) { + let marker; + if (options == null) { + options = {}; + } + const pos = new google.maps.LatLng(options.lat, options.lng); + + if (options.radius > 0) { + marker = new google.maps.Circle({ + map, + strokeColor: "#FF0000", + strokeOpacity: 0.8, + strokeWeight: 2, + fillColor: "#FF0000", + fillOpacity: 0.35, + center: pos, + radius: options.radius, + }); + return marker; + } else if (options.course) { + const p1 = new LatLon(pos.lat(), pos.lng()); + const speed = options.speed != null ? options.speed : 1; + const p2 = p1.destinationPoint(options.course, Math.max(0.2, speed) * 0.1); + + const lineCoordinates = [pos, new google.maps.LatLng(p2.lat(), p2.lon())]; + + const lineSymbol = { path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW }; + + const arrow = new google.maps.Polyline({ + map, + path: lineCoordinates, + icons: [ + { + icon: lineSymbol, + offset: "100%", + }, + ], + }); + return arrow; + } else { + marker = new google.maps.Marker({ + map, + position: pos, + title: "Recorded Location", + }); + return marker; + } +}; diff --git a/app/assets/javascripts/map_marker.js.coffee b/app/assets/javascripts/map_marker.js.coffee deleted file mode 100644 index 661b0311a1..0000000000 --- a/app/assets/javascripts/map_marker.js.coffee +++ /dev/null @@ -1,43 +0,0 @@ -window.map_marker = (map, options = {}) -> - pos = new google.maps.LatLng(options.lat, options.lng) - - if options.radius > 0 - marker = new google.maps.Circle - map: map - strokeColor: '#FF0000' - strokeOpacity: 0.8 - strokeWeight: 2 - fillColor: '#FF0000' - fillOpacity: 0.35 - center: pos - radius: options.radius - return marker - else if options.course - p1 = new LatLon(pos.lat(), pos.lng()) - speed = options.speed ? 1 - p2 = p1.destinationPoint(options.course, Math.max(0.2, speed) * 0.1) - - lineCoordinates = [ - pos - new google.maps.LatLng(p2.lat(), p2.lon()) - ] - - lineSymbol = - path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW - - arrow = new google.maps.Polyline - map: map - path: lineCoordinates - icons: [ - { - icon: lineSymbol - offset: '100%' - } - ] - return arrow - else - marker = new google.maps.Marker - map: map - position: pos - title: 'Recorded Location' - return marker diff --git a/app/assets/javascripts/pages/agent-edit-page.js b/app/assets/javascripts/pages/agent-edit-page.js new file mode 100644 index 0000000000..494601546f --- /dev/null +++ b/app/assets/javascripts/pages/agent-edit-page.js @@ -0,0 +1,340 @@ +(function () { + let formatAgentForSelect = undefined; + const Cls = (this.AgentEditPage = class AgentEditPage { + static initClass() { + formatAgentForSelect = function (agent) { + const originalOption = agent.element; + const description = agent.element[0].title; + return "" + agent.text + "
" + description; + }; + } + constructor() { + this.invokeDryRun = this.invokeDryRun.bind(this); + $("#agent_source_ids").on("change", this.showEventDescriptions); + this.showCorrectRegionsOnStartup(); + $("form.agent-form").on("submit", () => this.updateFromEditors()); + + // Validate agents_options Json on form submit + $("form.agent-form").submit(function (e) { + if ($("textarea#agent_options").length) { + try { + JSON.parse($("#agent_options").val()); + } catch (err) { + e.preventDefault(); + alert( + "Sorry, there appears to be an error in your JSON input. Please fix it before continuing." + ); + return false; + } + } + + if ( + $(".link-region").length && + $(".link-region").data("can-receive-events") === false + ) { + $(".link-region .select2-linked-tags option:selected").removeAttr( + "selected" + ); + } + + if ( + $(".control-link-region").length && + $(".control-link-region").data("can-control-other-agents") === false + ) { + $( + ".control-link-region .select2-linked-tags option:selected" + ).removeAttr("selected"); + } + + if ( + $(".event-related-region").length && + $(".event-related-region").data("can-create-events") === false + ) { + return $( + ".event-related-region .select2-linked-tags option:selected" + ).removeAttr("selected"); + } + }); + + $("#agent_name").each(function () { + // Select the number suffix if this is a cloned agent. + let matches; + if ((matches = this.value.match(/ \(\d+\)$/))) { + this.focus(); + if (this.selectionStart != null) { + this.selectionStart = matches.index; + return (this.selectionEnd = this.value.length); + } + } + }); + + // The type selector is only available on the new agent form. + if ($("#agent_type").length) { + $("#agent_type").on("change", () => this.handleTypeChange(false)); + this.handleTypeChange(true); + + // Update the dropdown to match agent description as well as agent name + $("select#agent_type").select2({ + width: "resolve", + formatResult: formatAgentForSelect, + escapeMarkup(m) { + return m; + }, + matcher(term, text, opt) { + const description = opt.attr("title"); + return ( + text.toUpperCase().indexOf(term.toUpperCase()) >= 0 || + description.toUpperCase().indexOf(term.toUpperCase()) >= 0 + ); + }, + }); + } else { + this.enableDryRunButton(); + this.buildAce(); + } + } + + handleTypeChange(firstTime) { + $(".event-descriptions").html("").hide(); + const type = $("#agent_type").val(); + + if (type === "Agent") { + $(".agent-settings").hide(); + return $(".description").hide(); + } else { + $(".agent-settings").show(); + $("#agent-spinner").fadeIn(); + if (!firstTime) { + $(".model-errors").hide(); + } + return $.getJSON("/agents/type_details", { type }, (json) => { + if (json.can_be_scheduled) { + if (firstTime) { + this.showSchedule(); + } else { + this.showSchedule(json.default_schedule); + } + } else { + this.hideSchedule(); + } + + if (json.can_receive_events) { + this.showLinks(); + } else { + this.hideLinks(); + } + + if (json.can_control_other_agents) { + this.showControlLinks(); + } else { + this.hideControlLinks(); + } + + if (json.can_create_events) { + this.showEventCreation(); + } else { + this.hideEventCreation(); + } + + if (json.description_html != null) { + $(".description").show().html(json.description_html); + } + + if (!firstTime) { + if (json.oauthable != null) { + $(".oauthable-form").html(json.oauthable); + } + if (json.form_options != null) { + $(".agent-options").html(json.form_options); + } + window.jsonEditor = setupJsonEditor()[0]; + } + + this.enableDryRunButton(); + this.buildAce(); + + window.initializeFormCompletable(); + + return $("#agent-spinner").stop(true, true).fadeOut(); + }); + } + } + + hideSchedule() { + $(".schedule-region .can-be-scheduled").hide(); + return $(".schedule-region .cannot-be-scheduled").show(); + } + + showSchedule(defaultSchedule = null) { + if (defaultSchedule != null) { + $(".schedule-region select").val(defaultSchedule).change(); + } + $(".schedule-region .can-be-scheduled").show(); + return $(".schedule-region .cannot-be-scheduled").hide(); + } + + hideLinks() { + $(".link-region .select2-container").hide(); + $(".link-region .propagate-immediately").hide(); + $(".link-region .cannot-receive-events").show(); + return $(".link-region").data("can-receive-events", false); + } + + showLinks() { + $(".link-region .select2-container").show(); + $(".link-region .propagate-immediately").show(); + $(".link-region .cannot-receive-events").hide(); + $(".link-region").data("can-receive-events", true); + return this.showEventDescriptions(); + } + + hideControlLinks() { + $(".control-link-region").hide(); + return $(".control-link-region").data("can-control-other-agents", false); + } + + showControlLinks() { + $(".control-link-region").show(); + return $(".control-link-region").data("can-control-other-agents", true); + } + + hideEventCreation() { + $(".event-related-region .select2-container").hide(); + $(".event-related-region .cannot-create-events").show(); + return $(".event-related-region").data("can-create-events", false); + } + + showEventCreation() { + $(".event-related-region .select2-container").show(); + $(".event-related-region .cannot-create-events").hide(); + return $(".event-related-region").data("can-create-events", true); + } + + showEventDescriptions() { + if ($("#agent_source_ids").val()) { + return $.getJSON( + "/agents/event_descriptions", + { ids: $("#agent_source_ids").val().join(",") }, + (json) => { + if (json.description_html != null) { + return $(".event-descriptions") + .show() + .html(json.description_html); + } else { + return $(".event-descriptions").hide(); + } + } + ); + } else { + return $(".event-descriptions").html("").hide(); + } + } + + showCorrectRegionsOnStartup() { + if ($(".schedule-region")) { + if ($(".schedule-region").data("can-be-scheduled") === true) { + this.showSchedule(); + } else { + this.hideSchedule(); + } + } + + if ($(".link-region")) { + if ($(".link-region").data("can-receive-events") === true) { + this.showLinks(); + } else { + this.hideLinks(); + } + } + + if ($(".control-link-region")) { + if ( + $(".control-link-region").data("can-control-other-agents") === true + ) { + this.showControlLinks(); + } else { + this.hideControlLinks(); + } + } + + if ($(".event-related-region")) { + if ($(".event-related-region").data("can-create-events") === true) { + return this.showEventCreation(); + } else { + return this.hideEventCreation(); + } + } + } + + buildAce() { + return $(".ace-editor").each(function () { + if (!$(this).data("initialized")) { + const $this = $(this); + $this.data("initialized", true); + const $source = $($this.data("source")).hide(); + const editor = ace.edit(this); + $this.data("ace-editor", editor); + const session = editor.getSession(); + session.setTabSize(2); + session.setUseSoftTabs(true); + session.setUseWrapMode(false); + + const setSyntax = function () { + let mode, theme; + if ((mode = $this.data("mode"))) { + session.setMode("ace/mode/" + mode); + } + + if ((theme = $this.data("theme"))) { + editor.setTheme("ace/theme/" + theme); + } + + if ((mode = $("[name='agent[options][language]']").val())) { + switch (mode) { + case "JavaScript": + return session.setMode("ace/mode/javascript"); + case "CoffeeScript": + return session.setMode("ace/mode/coffee"); + default: + return session.setMode("ace/mode/" + mode); + } + } + }; + + $("[name='agent[options][language]']").on("change", setSyntax); + setSyntax(); + + return session.setValue($source.val()); + } + }); + } + + updateFromEditors() { + return $(".ace-editor").each(function () { + const $source = $($(this).data("source")); + return $source.val($(this).data("ace-editor").getSession().getValue()); + }); + } + + enableDryRunButton() { + return $(".agent-dry-run-button") + .prop("disabled", false) + .off() + .on("click", this.invokeDryRun); + } + + disableDryRunButton() { + return $(".agent-dry-run-button").prop("disabled", true); + } + + invokeDryRun(e) { + e.preventDefault(); + this.updateFromEditors(); + return Utils.handleDryRunButton(e.currentTarget); + } + }); + Cls.initClass(); + return Cls; +})(); + +$(() => Utils.registerPage(AgentEditPage, { forPathsMatching: /^agents/ })); diff --git a/app/assets/javascripts/pages/agent-edit-page.js.coffee b/app/assets/javascripts/pages/agent-edit-page.js.coffee deleted file mode 100644 index 0eb5e01cc3..0000000000 --- a/app/assets/javascripts/pages/agent-edit-page.js.coffee +++ /dev/null @@ -1,231 +0,0 @@ -class @AgentEditPage - constructor: -> - $("#agent_source_ids").on "change", @showEventDescriptions - @showCorrectRegionsOnStartup() - $("form.agent-form").on "submit", => @updateFromEditors() - - # Validate agents_options Json on form submit - $('form.agent-form').submit (e) -> - if $('textarea#agent_options').length - try - JSON.parse $('#agent_options').val() - catch err - e.preventDefault() - alert 'Sorry, there appears to be an error in your JSON input. Please fix it before continuing.' - return false - - if $(".link-region").length && $(".link-region").data("can-receive-events") == false - $(".link-region .select2-linked-tags option:selected").removeAttr('selected') - - if $(".control-link-region").length && $(".control-link-region").data("can-control-other-agents") == false - $(".control-link-region .select2-linked-tags option:selected").removeAttr('selected') - - if $(".event-related-region").length && $(".event-related-region").data("can-create-events") == false - $(".event-related-region .select2-linked-tags option:selected").removeAttr('selected') - - $("#agent_name").each -> - # Select the number suffix if this is a cloned agent. - if matches = this.value.match(/ \(\d+\)$/) - this.focus() - if this.selectionStart? - this.selectionStart = matches.index - this.selectionEnd = this.value.length - - # The type selector is only available on the new agent form. - if $("#agent_type").length - $("#agent_type").on "change", => @handleTypeChange(false) - @handleTypeChange(true) - - # Update the dropdown to match agent description as well as agent name - $('select#agent_type').select2 - width: 'resolve' - formatResult: formatAgentForSelect - escapeMarkup: (m) -> - m - matcher: (term, text, opt) -> - description = opt.attr('title') - text.toUpperCase().indexOf(term.toUpperCase()) >= 0 or description.toUpperCase().indexOf(term.toUpperCase()) >= 0 - - else - @enableDryRunButton() - @buildAce() - - handleTypeChange: (firstTime) -> - $(".event-descriptions").html("").hide() - type = $('#agent_type').val() - - if type == 'Agent' - $(".agent-settings").hide() - $(".description").hide() - else - $(".agent-settings").show() - $("#agent-spinner").fadeIn() - $(".model-errors").hide() unless firstTime - $.getJSON "/agents/type_details", { type: type }, (json) => - if json.can_be_scheduled - if firstTime - @showSchedule() - else - @showSchedule(json.default_schedule) - else - @hideSchedule() - - if json.can_receive_events - @showLinks() - else - @hideLinks() - - if json.can_control_other_agents - @showControlLinks() - else - @hideControlLinks() - - if json.can_create_events - @showEventCreation() - else - @hideEventCreation() - - $(".description").show().html(json.description_html) if json.description_html? - - unless firstTime - $('.oauthable-form').html(json.oauthable) if json.oauthable? - $('.agent-options').html(json.form_options) if json.form_options? - window.jsonEditor = setupJsonEditor()[0] - - @enableDryRunButton() - @buildAce() - - window.initializeFormCompletable() - - $("#agent-spinner").stop(true, true).fadeOut(); - - hideSchedule: -> - $(".schedule-region .can-be-scheduled").hide() - $(".schedule-region .cannot-be-scheduled").show() - - showSchedule: (defaultSchedule = null) -> - if defaultSchedule? - $(".schedule-region select").val(defaultSchedule).change() - $(".schedule-region .can-be-scheduled").show() - $(".schedule-region .cannot-be-scheduled").hide() - - hideLinks: -> - $(".link-region .select2-container").hide() - $(".link-region .propagate-immediately").hide() - $(".link-region .cannot-receive-events").show() - $(".link-region").data("can-receive-events", false) - - showLinks: -> - $(".link-region .select2-container").show() - $(".link-region .propagate-immediately").show() - $(".link-region .cannot-receive-events").hide() - $(".link-region").data("can-receive-events", true) - @showEventDescriptions() - - hideControlLinks: -> - $(".control-link-region").hide() - $(".control-link-region").data("can-control-other-agents", false) - - showControlLinks: -> - $(".control-link-region").show() - $(".control-link-region").data("can-control-other-agents", true) - - hideEventCreation: -> - $(".event-related-region .select2-container").hide() - $(".event-related-region .cannot-create-events").show() - $(".event-related-region").data("can-create-events", false) - - showEventCreation: -> - $(".event-related-region .select2-container").show() - $(".event-related-region .cannot-create-events").hide() - $(".event-related-region").data("can-create-events", true) - - showEventDescriptions: -> - if $("#agent_source_ids").val() - $.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) => - if json.description_html? - $(".event-descriptions").show().html(json.description_html) - else - $(".event-descriptions").hide() - else - $(".event-descriptions").html("").hide() - - showCorrectRegionsOnStartup: -> - if $(".schedule-region") - if $(".schedule-region").data("can-be-scheduled") == true - @showSchedule() - else - @hideSchedule() - - if $(".link-region") - if $(".link-region").data("can-receive-events") == true - @showLinks() - else - @hideLinks() - - if $(".control-link-region") - if $(".control-link-region").data("can-control-other-agents") == true - @showControlLinks() - else - @hideControlLinks() - - if $(".event-related-region") - if $(".event-related-region").data("can-create-events") == true - @showEventCreation() - else - @hideEventCreation() - - buildAce: -> - $(".ace-editor").each -> - unless $(this).data('initialized') - $this = $(this) - $this.data('initialized', true) - $source = $($this.data('source')).hide() - editor = ace.edit(this) - $this.data('ace-editor', editor) - session = editor.getSession() - session.setTabSize(2) - session.setUseSoftTabs(true) - session.setUseWrapMode(false) - - setSyntax = -> - if mode = $this.data('mode') - session.setMode("ace/mode/" + mode) - - if theme = $this.data('theme') - editor.setTheme("ace/theme/" + theme); - - if mode = $("[name='agent[options][language]']").val() - switch mode - when 'JavaScript' then session.setMode("ace/mode/javascript") - when 'CoffeeScript' then session.setMode("ace/mode/coffee") - else session.setMode("ace/mode/" + mode) - - $("[name='agent[options][language]']").on 'change', setSyntax - setSyntax() - - session.setValue($source.val()) - - updateFromEditors: -> - $(".ace-editor").each -> - $source = $($(this).data('source')) - $source.val($(this).data('ace-editor').getSession().getValue()) - - enableDryRunButton: -> - $(".agent-dry-run-button").prop('disabled', false).off().on "click", @invokeDryRun - - disableDryRunButton: -> - $(".agent-dry-run-button").prop('disabled', true) - - invokeDryRun: (e) => - e.preventDefault() - @updateFromEditors() - Utils.handleDryRunButton(e.currentTarget) - - formatAgentForSelect = (agent) -> - originalOption = agent.element - description = agent.element[0].title - '' + agent.text + '
' + description - -$ -> - Utils.registerPage(AgentEditPage, forPathsMatching: /^agents/) diff --git a/app/assets/javascripts/pages/agent-show-page.js b/app/assets/javascripts/pages/agent-show-page.js new file mode 100644 index 0000000000..ab5cf63285 --- /dev/null +++ b/app/assets/javascripts/pages/agent-show-page.js @@ -0,0 +1,112 @@ +this.AgentShowPage = class AgentShowPage { + constructor() { + let tab; + $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on( + "click", + this.fetchLogs + ); + $(".agent-show #logs .clear").on("click", this.clearLogs); + $(".agent-show #memory .clear").on("click", this.clearMemory); + $("#toggle-memory").on("click", this.toggleMemory); + + // Trigger tabs when navigated to. + if ( + (tab = __guard__(window.location.href.match(/tab=(\w+)\b/i), (x) => x[1])) + ) { + if (["details", "logs"].includes(tab)) { + $(`.agent-show .nav-pills li a[href='#${tab}']`).click(); + } + } + } + + fetchLogs(e) { + const agentId = $(e.target).closest("[data-agent-id]").data("agent-id"); + e.preventDefault(); + $("#logs .spinner").show(); + $("#logs .refresh, #logs .clear").hide(); + return $.get(`/agents/${agentId}/logs`, (html) => { + $("#logs .logs").html(html); + $("#logs .logs .show-log-details").each(function () { + const $button = $(this); + return $button.on("click", function (e) { + e.preventDefault(); + return Utils.showDynamicModal("
", {
+            title: $button.data("modal-title"),
+            body(body) {
+              return $(body).find("pre").text($button.data("modal-content"));
+            },
+          });
+        });
+      });
+
+      return $("#logs .spinner")
+        .stop(true, true)
+        .fadeOut(() => $("#logs .refresh, #logs .clear").show());
+    });
+  }
+
+  clearLogs(e) {
+    if (confirm("Are you sure you want to clear all logs for this Agent?")) {
+      const agentId = $(e.target).closest("[data-agent-id]").data("agent-id");
+      e.preventDefault();
+      $("#logs .spinner").show();
+      $("#logs .refresh, #logs .clear").hide();
+      return $.post(
+        `/agents/${agentId}/logs/clear`,
+        { _method: "DELETE" },
+        (html) => {
+          $("#logs .logs").html(html);
+          $("#show-tabs li a.recent-errors").removeClass("recent-errors");
+          return $("#logs .spinner")
+            .stop(true, true)
+            .fadeOut(() => $("#logs .refresh, #logs .clear").show());
+        }
+      );
+    }
+  }
+
+  toggleMemory(e) {
+    e.preventDefault();
+    if ($("pre.memory").hasClass("hidden")) {
+      $("pre.memory").removeClass("hidden");
+      return $("#toggle-memory").text("Hide");
+    } else {
+      $("pre.memory").addClass("hidden");
+      return $("#toggle-memory").text("Show");
+    }
+  }
+
+  clearMemory(e) {
+    if (
+      confirm(
+        "Are you sure you want to completely clear the memory of this Agent?"
+      )
+    ) {
+      const agentId = $(e.target).closest("[data-agent-id]").data("agent-id");
+      e.preventDefault();
+      $("#memory .spinner").css({ display: "inline-block" });
+      $("#memory .clear").hide();
+      return $.post(`/agents/${agentId}/memory`, { _method: "DELETE" })
+        .done(() =>
+          $("#memory .spinner").fadeOut(() =>
+            $("#memory + .memory").text("{\n}\n")
+          )
+        )
+        .fail(() =>
+          $("#memory .spinner").fadeOut(() =>
+            $("#memory .clear").css({ display: "inline-block" })
+          )
+        );
+    }
+  }
+};
+
+$(() =>
+  Utils.registerPage(AgentShowPage, { forPathsMatching: /^agents\/\d+/ })
+);
+
+function __guard__(value, transform) {
+  return typeof value !== "undefined" && value !== null
+    ? transform(value)
+    : undefined;
+}
diff --git a/app/assets/javascripts/pages/agent-show-page.js.coffee b/app/assets/javascripts/pages/agent-show-page.js.coffee
deleted file mode 100644
index f692982423..0000000000
--- a/app/assets/javascripts/pages/agent-show-page.js.coffee
+++ /dev/null
@@ -1,68 +0,0 @@
-class @AgentShowPage
-  constructor: ->
-    $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs
-    $(".agent-show #logs .clear").on "click", @clearLogs
-    $(".agent-show #memory .clear").on "click", @clearMemory
-    $('#toggle-memory').on "click", @toggleMemory
-
-    # Trigger tabs when navigated to.
-    if tab = window.location.href.match(/tab=(\w+)\b/i)?[1]
-      if tab in ["details", "logs"]
-        $(".agent-show .nav-pills li a[href='##{tab}']").click()
-
-  fetchLogs: (e) ->
-    agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
-    e.preventDefault()
-    $("#logs .spinner").show()
-    $("#logs .refresh, #logs .clear").hide()
-    $.get "/agents/#{agentId}/logs", (html) =>
-      $("#logs .logs").html html
-      $("#logs .logs .show-log-details").each ->
-        $button = $(this)
-        $button.on 'click', (e) ->
-          e.preventDefault()
-          Utils.showDynamicModal '
',
-            title: $button.data('modal-title'),
-            body: (body) ->
-              $(body).find('pre').text $button.data('modal-content')
-
-      $("#logs .spinner").stop(true, true).fadeOut ->
-        $("#logs .refresh, #logs .clear").show()
-
-  clearLogs: (e) ->
-    if confirm("Are you sure you want to clear all logs for this Agent?")
-      agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
-      e.preventDefault()
-      $("#logs .spinner").show()
-      $("#logs .refresh, #logs .clear").hide()
-      $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) =>
-        $("#logs .logs").html html
-        $("#show-tabs li a.recent-errors").removeClass 'recent-errors'
-        $("#logs .spinner").stop(true, true).fadeOut ->
-          $("#logs .refresh, #logs .clear").show()
-
-  toggleMemory: (e) ->
-    e.preventDefault()
-    if $('pre.memory').hasClass('hidden')
-      $('pre.memory').removeClass 'hidden'
-      $('#toggle-memory').text('Hide')
-    else
-      $('pre.memory').addClass 'hidden'
-      $('#toggle-memory').text('Show')
-
-  clearMemory: (e) ->
-    if confirm("Are you sure you want to completely clear the memory of this Agent?")
-      agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
-      e.preventDefault()
-      $("#memory .spinner").css(display: 'inline-block')
-      $("#memory .clear").hide()
-      $.post "/agents/#{agentId}/memory", { "_method": "DELETE" }
-        .done ->
-          $("#memory .spinner").fadeOut ->
-            $("#memory + .memory").text "{\n}\n"
-        .fail ->
-          $("#memory .spinner").fadeOut ->
-            $("#memory .clear").css(display: 'inline-block')
-
-$ ->
-  Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/)
diff --git a/app/assets/javascripts/pages/scenario-form-page.js b/app/assets/javascripts/pages/scenario-form-page.js
new file mode 100644
index 0000000000..77bc6bc79d
--- /dev/null
+++ b/app/assets/javascripts/pages/scenario-form-page.js
@@ -0,0 +1,23 @@
+this.ScenarioFormPage = class ScenarioFormPage {
+  constructor() {
+    this.enabledSelect2();
+  }
+
+  format(icon) {
+    const originalOption = icon.element;
+    return (
+      ' ' + icon.text
+    );
+  }
+
+  enabledSelect2() {
+    return $(".select2-fountawesome-icon").select2({
+      width: "100%",
+      formatResult: this.format,
+    });
+  }
+};
+
+$(() =>
+  Utils.registerPage(ScenarioFormPage, { forPathsMatching: /^scenarios/ })
+);
diff --git a/app/assets/javascripts/pages/scenario-form-page.js.coffee b/app/assets/javascripts/pages/scenario-form-page.js.coffee
deleted file mode 100644
index 04bdb2fbe6..0000000000
--- a/app/assets/javascripts/pages/scenario-form-page.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-class @ScenarioFormPage
-  constructor:() ->
-    @enabledSelect2()
-
-  format: (icon) ->
-    originalOption = icon.element
-    ' ' + icon.text
-
-  enabledSelect2: () ->
-    $('.select2-fountawesome-icon').select2
-      width: '100%'
-      formatResult: @format
-      
-$ ->
-  Utils.registerPage(ScenarioFormPage, forPathsMatching: /^scenarios/)
\ No newline at end of file
diff --git a/app/assets/javascripts/pages/scenario-show-page.js b/app/assets/javascripts/pages/scenario-show-page.js
new file mode 100644
index 0000000000..e3b858554d
--- /dev/null
+++ b/app/assets/javascripts/pages/scenario-show-page.js
@@ -0,0 +1,24 @@
+this.ScenarioShowPage = class ScenarioShowPage {
+  constructor() {
+    this.changeModalText();
+  }
+
+  changeModalText() {
+    $("#disable-all").click(function () {
+      $("#enable-disable-agents .modal-body").text(
+        "Would you like to disable all agents?"
+      );
+      return $("#scenario-disabled-value").val("true");
+    });
+    return $("#enable-all").click(function () {
+      $("#enable-disable-agents .modal-body").text(
+        "Would you like to enable all agents?"
+      );
+      return $("#scenario-disabled-value").val("false");
+    });
+  }
+};
+
+$(() =>
+  Utils.registerPage(ScenarioShowPage, { forPathsMatching: /^scenarios/ })
+);
diff --git a/app/assets/javascripts/pages/scenario-show-page.js.coffee b/app/assets/javascripts/pages/scenario-show-page.js.coffee
deleted file mode 100644
index 0a41554357..0000000000
--- a/app/assets/javascripts/pages/scenario-show-page.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-class @ScenarioShowPage
-  constructor:() ->
-    @changeModalText()
-
-  changeModalText: () ->
-    $('#disable-all').click ->
-      $('#enable-disable-agents .modal-body').text 'Would you like to disable all agents?'
-      $('#scenario-disabled-value').val 'true'
-    $('#enable-all').click ->
-      $('#enable-disable-agents .modal-body').text 'Would you like to enable all agents?'
-      $('#scenario-disabled-value').val 'false'
-
-$ ->
-  Utils.registerPage(ScenarioShowPage, forPathsMatching: /^scenarios/)
-
diff --git a/app/assets/javascripts/pages/user-credential-page.js b/app/assets/javascripts/pages/user-credential-page.js
new file mode 100644
index 0000000000..c1afba6e63
--- /dev/null
+++ b/app/assets/javascripts/pages/user-credential-page.js
@@ -0,0 +1,33 @@
+this.UserCredentialPage = class UserCredentialPage {
+  constructor() {
+    const editor = ace.edit("ace-credential-value");
+    editor.getSession().setTabSize(2);
+    editor.getSession().setUseSoftTabs(true);
+    editor.getSession().setUseWrapMode(false);
+
+    const setMode = function () {
+      const mode = $("#user_credential_mode").val();
+      if (mode === "java_script") {
+        return editor.getSession().setMode("ace/mode/javascript");
+      } else {
+        return editor.getSession().setMode("ace/mode/text");
+      }
+    };
+
+    setMode();
+    $("#user_credential_mode").on("change", setMode);
+
+    const $textarea = $("#user_credential_credential_value").hide();
+    editor.getSession().setValue($textarea.val());
+
+    $textarea
+      .closest("form")
+      .on("submit", () => $textarea.val(editor.getSession().getValue()));
+  }
+};
+
+$(() =>
+  Utils.registerPage(UserCredentialPage, {
+    forPathsMatching: /^user_credentials\/(\d+|new)/,
+  })
+);
diff --git a/app/assets/javascripts/pages/user-credential-page.js.coffee b/app/assets/javascripts/pages/user-credential-page.js.coffee
deleted file mode 100644
index 733f5568de..0000000000
--- a/app/assets/javascripts/pages/user-credential-page.js.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-class @UserCredentialPage
-  constructor: ->
-    editor = ace.edit("ace-credential-value")
-    editor.getSession().setTabSize(2)
-    editor.getSession().setUseSoftTabs(true)
-    editor.getSession().setUseWrapMode(false)
-
-    setMode = ->
-      mode = $("#user_credential_mode").val()
-      if mode == 'java_script'
-        editor.getSession().setMode("ace/mode/javascript")
-      else
-        editor.getSession().setMode("ace/mode/text")
-
-    setMode()
-    $("#user_credential_mode").on 'change', setMode
-
-    $textarea = $('#user_credential_credential_value').hide()
-    editor.getSession().setValue($textarea.val())
-
-    $textarea.closest('form').on 'submit', ->
-      $textarea.val(editor.getSession().getValue())
-
-$ ->
-  Utils.registerPage(UserCredentialPage, forPathsMatching: /^user_credentials\/(\d+|new)/)
diff --git a/app/assets/javascripts/tweets.js b/app/assets/javascripts/tweets.js
new file mode 100644
index 0000000000..6e9eabb455
--- /dev/null
+++ b/app/assets/javascripts/tweets.js
@@ -0,0 +1,15 @@
+$(() =>
+  $(".tweet-body").each(function () {
+    return $(this).click(function () {
+      $(this).off("click");
+      return twttr.widgets
+        .createTweet(
+          this.dataset.tweetId,
+          this
+          // conversation: 'none'
+          // cards: 'hidden'
+        )
+        .then((el) => (el.previousSibling.style.display = "none"));
+    });
+  })
+);
diff --git a/app/assets/javascripts/tweets.js.coffee b/app/assets/javascripts/tweets.js.coffee
deleted file mode 100644
index 126d82417f..0000000000
--- a/app/assets/javascripts/tweets.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-$ ->
-  $('.tweet-body').each ->
-    $(this).click ->
-      $(this).off('click')
-      twttr.widgets.createTweet(
-        this.dataset.tweetId
-        this
-        # conversation: 'none'
-        # cards: 'hidden'
-      ).then (el) ->
-        el.previousSibling.style.display = 'none'
diff --git a/app/helpers/dot_helper.rb b/app/helpers/dot_helper.rb
index 1a32eaf006..807ffb3ed1 100644
--- a/app/helpers/dot_helper.rb
+++ b/app/helpers/dot_helper.rb
@@ -1,6 +1,6 @@
 module DotHelper
   def render_agents_diagram(agents, layout: nil)
-    if svg = dot_to_svg(agents_dot(agents, rich: true, layout: layout))
+    if svg = dot_to_svg(agents_dot(agents, rich: true, layout:))
       decorate_svg(svg, agents).html_safe
     else
       # Google chart request url
@@ -141,7 +141,7 @@ def draw(vars = {}, &block)
   end
 
   def agents_dot(agents, rich: false, layout: nil)
-    draw(agents: agents,
+    draw(agents:,
          agent_id: ->(agent) { 'a%d' % agent.id },
          agent_label: ->(agent) {
            agent.name.gsub(/(.{20}\S*)\s+/) {
@@ -150,7 +150,7 @@ def agents_dot(agents, rich: false, layout: nil)
            }
          },
          agent_url: ->(agent) { agent_path(agent.id) },
-         rich: rich) {
+         rich:) {
       @disabled = '#999999'
 
       def agent_node(agent)
@@ -175,7 +175,7 @@ def agent_edge(agent, receiver)
       block('digraph', 'Agent Event Flow') {
         layout ||= ENV['DIAGRAM_DEFAULT_LAYOUT'].presence
         if rich && /\A[a-z]+\z/ === layout
-          statement 'graph', layout: layout, overlap: 'false'
+          statement 'graph', layout:, overlap: 'false'
         end
         statement 'node',
                   shape: 'box',
@@ -253,7 +253,7 @@ def decorate_svg(xml, agents)
           }
         }
       }
-      # See also: app/assets/diagram.js.coffee
+      # See also: app/assets/diagram.js
     }.at('div.agent-diagram').to_s
   end
 end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index a8ed9e5f0e..62da61a9a0 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -8,11 +8,3 @@
 
 # Add additional assets to the asset load path.
 # Rails.application.config.assets.paths << Emoji.images_path
-
-# Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in the app/assets
-# folder are already added.
-Rails.application.config.assets.precompile += %w( diagram.js graphing.js map_marker.js ace.js tweets.js )
-
-Rails.application.config.assets.precompile += %w(*.png *.jpg *.jpeg *.gif)
-Rails.application.config.assets.precompile += %w(*.woff *.eot *.svg *.ttf) # Bootstrap fonts

From 966ab53d36d6440ddd2dea5ff9fbe2afbb7eb298 Mon Sep 17 00:00:00 2001
From: Akinori MUSHA 
Date: Mon, 17 Apr 2023 05:41:42 +0900
Subject: [PATCH 13/32] Use headless Chrome in feature specs

- Drop poltergeist (PhantomJS)
- Drop capybara-screenshot to upgrade capybara
- Upgrade webmock and vcr
- Enable puma for testing
---
 Gemfile                 |  6 +++---
 Gemfile.lock            | 44 +++++++++++++++++++++--------------------
 spec/capybara_helper.rb | 10 +---------
 3 files changed, 27 insertions(+), 33 deletions(-)

diff --git a/Gemfile b/Gemfile
index 7c8a93e7af..5ef1f3c1e1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -158,11 +158,10 @@ group :development do
 
   group :test do
     gem 'capybara'
-    gem 'capybara-screenshot'
     gem 'capybara-select-2', github: 'Hirurg103/capybara_select2',
                              ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76',
                              require: false
-    gem 'poltergeist'
+    gem 'puma'
     gem 'rails-controller-testing'
     gem 'rr', require: false
     gem 'rspec'
@@ -170,11 +169,12 @@ group :development do
     gem 'rspec-html-matchers'
     gem 'rspec-mocks'
     gem 'rspec-rails'
+    gem 'selenium-webdriver'
     gem 'shoulda-matchers'
     gem 'simplecov', require: false
     gem 'simplecov-lcov', '~> 0.8.0', require: false
     gem 'vcr'
-    gem 'webmock', '~> 3.5.1'
+    gem 'webmock'
   end
 end
 
diff --git a/Gemfile.lock b/Gemfile.lock
index f877b24da0..5163c5540c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -197,17 +197,15 @@ GEM
     capistrano-rails (1.6.1)
       capistrano (~> 3.1)
       capistrano-bundler (>= 1.1, < 3)
-    capybara (2.18.0)
+    capybara (3.39.0)
       addressable
+      matrix
       mini_mime (>= 0.1.3)
-      nokogiri (>= 1.3.3)
-      rack (>= 1.0.0)
-      rack-test (>= 0.5.4)
-      xpath (>= 2.0, < 4.0)
-    capybara-screenshot (1.0.17)
-      capybara (>= 1.0, < 3)
-      launchy
-    cliver (0.3.2)
+      nokogiri (~> 1.8)
+      rack (>= 1.6.0)
+      rack-test (>= 0.6.3)
+      regexp_parser (>= 1.5, < 3.0)
+      xpath (~> 3.2)
     coderay (1.1.3)
     coffee-rails (5.0.0)
       coffee-script (>= 2.2.0)
@@ -457,6 +455,7 @@ GEM
       net-pop
       net-smtp
     marcel (1.0.2)
+    matrix (0.4.2)
     memoist (0.16.2)
     memoizable (0.4.2)
       thread_safe (~> 0.3, >= 0.3.1)
@@ -550,16 +549,13 @@ GEM
     parser (3.2.2.0)
       ast (~> 2.4.1)
     pg (1.4.4)
-    poltergeist (1.8.1)
-      capybara (~> 2.1)
-      cliver (~> 0.3.1)
-      multi_json (~> 1.0)
-      websocket-driver (>= 0.2.0)
     polyglot (0.3.5)
     pry (0.13.1)
       coderay (~> 1.1)
       method_source (~> 1.0)
     public_suffix (5.0.1)
+    puma (6.2.1)
+      nio4r (~> 2.0)
     raabro (1.4.0)
     racc (1.6.2)
     rack (2.2.6.4)
@@ -672,6 +668,7 @@ GEM
       rubocop (~> 1.33)
       rubocop-capybara (~> 2.17)
     ruby-progressbar (1.13.0)
+    rubyzip (2.3.2)
     rufus-scheduler (3.8.1)
       fugit (~> 1.1, >= 1.1.6)
     sass (3.7.4)
@@ -691,6 +688,10 @@ GEM
       tilt
     sax-machine (1.3.2)
     select2-rails (3.5.11)
+    selenium-webdriver (4.8.6)
+      rexml (~> 3.2, >= 3.2.5)
+      rubyzip (>= 1.2.2, < 3.0)
+      websocket (~> 1.0)
     shellany (0.0.1)
     shoulda-matchers (4.0.1)
       activesupport (>= 4.2.0)
@@ -754,7 +755,7 @@ GEM
     unicorn (6.1.0)
       kgio (~> 2.6)
       raindrops (~> 0.7)
-    vcr (3.0.3)
+    vcr (6.1.0)
     warden (1.2.9)
       rack (>= 2.0.9)
     web-console (4.2.0)
@@ -762,11 +763,12 @@ GEM
       activemodel (>= 6.0.0)
       bindex (>= 0.4.0)
       railties (>= 6.0.0)
-    webmock (3.5.1)
-      addressable (>= 2.3.6)
+    webmock (3.18.1)
+      addressable (>= 2.8.0)
       crack (>= 0.3.2)
-      hashdiff
+      hashdiff (>= 0.4.0, < 2.0.0)
     webrick (1.7.0)
+    websocket (1.2.9)
     websocket-driver (0.7.5)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.5)
@@ -794,7 +796,6 @@ DEPENDENCIES
   capistrano-bundler
   capistrano-rails
   capybara
-  capybara-screenshot
   capybara-select-2!
   coffee-rails (~> 5)
   daemons (~> 1.1.9)
@@ -854,7 +855,7 @@ DEPENDENCIES
   omniauth-tumblr
   omniauth-twitter
   pg (~> 1.1)
-  poltergeist
+  puma
   rack-livereload
   rails (~> 6.1.7)
   rails-controller-testing
@@ -873,6 +874,7 @@ DEPENDENCIES
   rufus-scheduler (~> 3.4)
   sass-rails (>= 6.0)
   select2-rails (~> 3.5.4)
+  selenium-webdriver
   shoulda-matchers
   simplecov
   simplecov-lcov (~> 0.8.0)
@@ -893,7 +895,7 @@ DEPENDENCIES
   unicorn
   vcr
   web-console (>= 3.3.0)
-  webmock (~> 3.5.1)
+  webmock
   weibo_2!
   xmpp4r (~> 0.5.6)
 
diff --git a/spec/capybara_helper.rb b/spec/capybara_helper.rb
index 3551f4c91f..81aeb085d8 100644
--- a/spec/capybara_helper.rb
+++ b/spec/capybara_helper.rb
@@ -1,20 +1,12 @@
 require 'rails_helper'
 require 'capybara/rails'
-require 'capybara/poltergeist'
-require 'capybara-screenshot/rspec'
 require 'capybara-select-2'
 
 CAPYBARA_TIMEOUT = ENV['CI'] == 'true' ? 60 : 5
 
-Capybara.register_driver :poltergeist do |app|
-  Capybara::Poltergeist::Driver.new(app, timeout: CAPYBARA_TIMEOUT)
-end
-
-Capybara.javascript_driver = :poltergeist
+Capybara.javascript_driver = ENV['USE_HEADED_CHROME'] ? :selenium_chrome : :selenium_chrome_headless
 Capybara.default_max_wait_time = CAPYBARA_TIMEOUT
 
-Capybara::Screenshot.prune_strategy = { keep: 3 }
-
 RSpec.configure do |config|
   config.include Warden::Test::Helpers
   config.include AlertConfirmer, type: :feature

From 0f9e2c333ce1959ac0c3cac4ad8b180921c7d8d1 Mon Sep 17 00:00:00 2001
From: Akinori MUSHA 
Date: Mon, 17 Apr 2023 11:29:51 +0900
Subject: [PATCH 14/32] Update rails-controller-testing

---
 Gemfile.lock | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 5163c5540c..bc95b2c895 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -471,7 +471,7 @@ GEM
     mini_portile2 (2.8.1)
     mini_racer (0.6.3)
       libv8-node (~> 16.10.0.0)
-    minitest (5.17.0)
+    minitest (5.18.0)
     mqtt (0.3.1)
     msgpack (1.4.2)
     multi_json (1.15.0)
@@ -578,10 +578,10 @@ GEM
       bundler (>= 1.15.0)
       railties (= 6.1.7.2)
       sprockets-rails (>= 2.0.0)
-    rails-controller-testing (1.0.4)
-      actionpack (>= 5.0.1.x)
-      actionview (>= 5.0.1.x)
-      activesupport (>= 5.0.1.x)
+    rails-controller-testing (1.0.5)
+      actionpack (>= 5.0.1.rc1)
+      actionview (>= 5.0.1.rc1)
+      activesupport (>= 5.0.1.rc1)
     rails-dom-testing (2.0.3)
       activesupport (>= 4.2.0)
       nokogiri (>= 1.6)

From 05b11fcaf83b1a24c0e1b975f4093d3f22a4f9dc Mon Sep 17 00:00:00 2001
From: Akinori MUSHA 
Date: Mon, 17 Apr 2023 18:39:30 +0900
Subject: [PATCH 15/32] Upgrade Liquid

---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index bc95b2c895..e72c772329 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -441,7 +441,7 @@ GEM
     libv8-node (16.10.0.0-arm64-darwin)
     libv8-node (16.10.0.0-x86_64-darwin)
     libv8-node (16.10.0.0-x86_64-linux)
-    liquid (5.3.0)
+    liquid (5.4.0)
     listen (3.0.8)
       rb-fsevent (~> 0.9, >= 0.9.4)
       rb-inotify (~> 0.9, >= 0.9.7)

From 7c2c7f3039e2f2e269605da6b214c120562819f0 Mon Sep 17 00:00:00 2001
From: Akinori MUSHA 
Date: Mon, 17 Apr 2023 18:43:42 +0900
Subject: [PATCH 16/32] Use to_h

---
 app/concerns/liquid_droppable.rb | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/concerns/liquid_droppable.rb b/app/concerns/liquid_droppable.rb
index bad29253d5..6191db560a 100644
--- a/app/concerns/liquid_droppable.rb
+++ b/app/concerns/liquid_droppable.rb
@@ -23,7 +23,8 @@ def each
 
     def as_json
       return {} unless defined?(self.class::METHODS)
-      Hash[self.class::METHODS.map { |m| [m, send(m).as_json]}]
+
+      self.class::METHODS.to_h { |m| [m, send(m).as_json] }
     end
   end
 

From e7021b770f0e93cb4cb3253e4f7213d25409fa9c Mon Sep 17 00:00:00 2001
From: Akinori MUSHA 
Date: Mon, 17 Apr 2023 18:59:18 +0900
Subject: [PATCH 17/32] Avoid complicated constant manipulation with
 LiquidDroppable

---
 app/concerns/liquid_droppable.rb            |  18 ---
 app/models/agent.rb                         |  70 +++++-----
 app/models/agents/commander_agent.rb        |   2 +-
 app/models/agents/event_formatting_agent.rb |   2 +-
 app/models/event.rb                         |  66 ++++-----
 lib/location.rb                             |  16 ++-
 spec/concerns/liquid_droppable_spec.rb      |  34 -----
 spec/concerns/liquid_interpolatable_spec.rb | 145 +++++++++++---------
 spec/models/agent_spec.rb                   |   8 +-
 spec/models/event_spec.rb                   |  24 ++--
 10 files changed, 179 insertions(+), 206 deletions(-)
 delete mode 100644 spec/concerns/liquid_droppable_spec.rb

diff --git a/app/concerns/liquid_droppable.rb b/app/concerns/liquid_droppable.rb
index 6191db560a..a0f293c253 100644
--- a/app/concerns/liquid_droppable.rb
+++ b/app/concerns/liquid_droppable.rb
@@ -1,11 +1,6 @@
 # frozen_string_literal: true
 
-# Include this mix-in to make a class droppable to Liquid, and adjust
-# its behavior in Liquid by implementing its dedicated Drop class
-# named with a "Drop" suffix.
 module LiquidDroppable
-  extend ActiveSupport::Concern
-
   class Drop < Liquid::Drop
     def initialize(object)
       @object = object
@@ -28,19 +23,6 @@ def as_json
     end
   end
 
-  included do
-    const_set :Drop,
-              if Kernel.const_defined?(drop_name = "#{name}Drop")
-                Kernel.const_get(drop_name)
-              else
-                Kernel.const_set(drop_name, Class.new(Drop))
-              end
-  end
-
-  def to_liquid
-    self.class::Drop.new(self)
-  end
-
   class MatchDataDrop < Drop
     METHODS = %w[pre_match post_match names size]
 
diff --git a/app/models/agent.rb b/app/models/agent.rb
index 9c17eb954d..d39062fe3a 100644
--- a/app/models/agent.rb
+++ b/app/models/agent.rb
@@ -11,7 +11,6 @@ class Agent < ActiveRecord::Base
   include WorkingHelpers
   include LiquidInterpolatable
   include HasGuid
-  include LiquidDroppable
   include DryRunnable
   include SortableEvents
 
@@ -478,40 +477,47 @@ def async_check(agent_id)
       AgentCheckJob.perform_later(agent_id)
     end
   end
-end
 
-class AgentDrop
-  def type
-    @object.short_type
-  end
-
-  METHODS = %i[
-    id
-    name
-    type
-    options
-    memory
-    sources
-    receivers
-    schedule
-    controllers
-    control_targets
-    disabled
-    keep_events_for
-    propagate_immediately
-  ]
+  public def to_liquid
+    Drop.new(self)
+  end
 
-  METHODS.each { |attr|
-    define_method(attr) {
-      @object.__send__(attr)
-    } unless method_defined?(attr)
-  }
+  class Drop < LiquidDroppable::Drop
+    def type
+      @object.short_type
+    end
 
-  def working
-    @object.working?
-  end
+    METHODS = %i[
+      id
+      name
+      type
+      options
+      memory
+      sources
+      receivers
+      schedule
+      controllers
+      control_targets
+      disabled
+      keep_events_for
+      propagate_immediately
+    ]
+
+    METHODS.each { |attr|
+      define_method(attr) {
+        @object.__send__(attr)
+      } unless method_defined?(attr)
+    }
+
+    def working
+      @object.working?
+    end
 
-  def url
-    Rails.application.routes.url_helpers.agent_url(@object, Rails.application.config.action_mailer.default_url_options)
+    def url
+      Rails.application.routes.url_helpers.agent_url(
+        @object,
+        Rails.application.config.action_mailer.default_url_options
+      )
+    end
   end
 end
diff --git a/app/models/agents/commander_agent.rb b/app/models/agents/commander_agent.rb
index f49f77f10a..aae045fc8a 100644
--- a/app/models/agents/commander_agent.rb
+++ b/app/models/agents/commander_agent.rb
@@ -28,7 +28,7 @@ class CommanderAgent < Agent
 
       - If you want to update a WeatherAgent based on a UserLocationAgent, you could use `'action': 'configure'` and set 'configure_options' to `{ 'location': '{{_location_.latlng}}' }`.
 
-      - In templating, you can use the variable `target` to refer to each target agent, which has the following attributes: #{AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.to_sentence}.
+      - In templating, you can use the variable `target` to refer to each target agent, which has the following attributes: #{Agent::Drop.instance_methods(false).map { |m| "`#{m}`" }.to_sentence}.
 
       # Targets
 
diff --git a/app/models/agents/event_formatting_agent.rb b/app/models/agents/event_formatting_agent.rb
index 898c0ef6c7..98f4d3f9ad 100644
--- a/app/models/agents/event_formatting_agent.rb
+++ b/app/models/agents/event_formatting_agent.rb
@@ -34,7 +34,7 @@ class EventFormattingAgent < Agent
 
       The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`.
 
-      The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}.
+      The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << Agent::Drop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}.
 
       Have a look at the [Wiki](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
 
diff --git a/app/models/event.rb b/app/models/event.rb
index ada52f9bad..0148ad9908 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -100,44 +100,48 @@ def possibly_propagate
     propagate_ids = agent.receivers.where(propagate_immediately: true).pluck(:id)
     Agent.receive!(only_receivers: propagate_ids) unless propagate_ids.empty?
   end
-end
 
-class EventDrop
-  def initialize(object)
-    @payload = object.payload
-    super
+  public def to_liquid
+    Drop.new(self)
   end
 
-  def liquid_method_missing(key)
-    @payload[key]
-  end
+  class Drop < LiquidDroppable::Drop
+    def initialize(object)
+      @payload = object.payload
+      super
+    end
 
-  def each(&block)
-    @payload.each(&block)
-  end
+    def liquid_method_missing(key)
+      @payload[key]
+    end
 
-  def agent
-    @payload.fetch(__method__) {
-      @object.agent
-    }
-  end
+    def each(&block)
+      @payload.each(&block)
+    end
 
-  def created_at
-    @payload.fetch(__method__) {
-      @object.created_at
-    }
-  end
+    def agent
+      @payload.fetch(__method__) {
+        @object.agent
+      }
+    end
 
-  def _location_
-    @object.location
-  end
+    def created_at
+      @payload.fetch(__method__) {
+        @object.created_at
+      }
+    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
-    }
+    def _location_
+      @object.location
+    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
+      }
+    end
   end
 end
diff --git a/lib/location.rb b/lib/location.rb
index 7cdb928e59..7eb45b4f2c 100644
--- a/lib/location.rb
+++ b/lib/location.rb
@@ -102,14 +102,18 @@ def floatify(value)
       end
     end
   end
-end
 
-class LocationDrop
-  KEYS = Location.members.map(&:to_s).concat(%w[latitude longitude latlng])
+  public def to_liquid
+    Drop.new(self)
+  end
+
+  class Drop < LiquidDroppable::Drop
+    KEYS = Location.members.map(&:to_s).concat(%w[latitude longitude latlng])
 
-  def liquid_method_missing(key)
-    if KEYS.include?(key)
-      @object.__send__(key)
+    def liquid_method_missing(key)
+      if KEYS.include?(key)
+        @object.__send__(key)
+      end
     end
   end
 end
diff --git a/spec/concerns/liquid_droppable_spec.rb b/spec/concerns/liquid_droppable_spec.rb
deleted file mode 100644
index 64dc384876..0000000000
--- a/spec/concerns/liquid_droppable_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'rails_helper'
-
-describe LiquidDroppable do
-  before do
-    class DroppableTest
-      include LiquidDroppable
-
-      def initialize(value)
-        @value = value
-      end
-
-      attr_reader :value
-
-      def to_s
-        "[value:#{value}]"
-      end
-    end
-
-    class DroppableTestDrop
-      def value
-        @object.value
-      end
-    end
-  end
-
-  describe 'test class' do
-    it 'should be droppable' do
-      five = DroppableTest.new(5)
-      expect(five.to_liquid.class).to eq(DroppableTestDrop)
-      expect(Liquid::Template.parse('{{ x.value | plus:3 }}').render('x' => five)).to eq('8')
-      expect(Liquid::Template.parse('{{ x }}').render('x' => five)).to eq('[value:5]')
-    end
-  end
-end
diff --git a/spec/concerns/liquid_interpolatable_spec.rb b/spec/concerns/liquid_interpolatable_spec.rb
index 127a43f0ed..084e17b471 100644
--- a/spec/concerns/liquid_interpolatable_spec.rb
+++ b/spec/concerns/liquid_interpolatable_spec.rb
@@ -23,7 +23,7 @@ class Agents::InterpolatableAgent < Agent
       include LiquidInterpolatable
 
       def check
-        create_event :payload => {}
+        create_event payload: {}
       end
 
       def validate_options
@@ -52,7 +52,7 @@ def validate_options
     let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
 
     it 'serializes data to json' do
-      agent.interpolation_context['something'] = {foo: 'bar'}
+      agent.interpolation_context['something'] = { foo: 'bar' }
       agent.options['cleaned'] = '{{ something | json }}'
       expect(agent.interpolated['cleaned']).to eq('{"foo":"bar"}')
     end
@@ -67,8 +67,8 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should escape a string for use in XPath expression' do
       [
-        %q{abc}.freeze,
-        %q{'a"bc'dfa""fds''fa}.freeze,
+        'abc'.freeze,
+        %q('a"bc'dfa""fds''fa).freeze,
       ].each { |string|
         expect(@filter.to_xpath_roundtrip(string)).to eq(string)
       }
@@ -82,7 +82,8 @@ def @filter.to_xpath_roundtrip(string)
 
   describe 'to_uri' do
     before do
-      @agent = Agents::InterpolatableAgent.new(name: "test", options: { 'foo' => '{% assign u = s | to_uri %}{{ u.path }}' })
+      @agent = Agents::InterpolatableAgent.new(name: "test",
+                                               options: { 'foo' => '{% assign u = s | to_uri %}{{ u.path }}' })
       @agent.interpolation_context['s'] = 'http://example.com/dir/1?q=test'
     end
 
@@ -132,21 +133,21 @@ def @filter.to_xpath_roundtrip(string)
 
   describe 'uri_expand' do
     before do
-      stub_request(:head, 'https://t.co.x/aaaa').
-        to_return(status: 301, headers: { Location: 'https://bit.ly.x/bbbb' })
-      stub_request(:head, 'https://bit.ly.x/bbbb').
-        to_return(status: 301, headers: { Location: 'http://tinyurl.com.x/cccc' })
-      stub_request(:head, 'http://tinyurl.com.x/cccc').
-        to_return(status: 301, headers: { Location: 'http://www.example.com/welcome' })
-      stub_request(:head, 'http://www.example.com/welcome').
-        to_return(status: 200)
+      stub_request(:head, 'https://t.co.x/aaaa')
+        .to_return(status: 301, headers: { Location: 'https://bit.ly.x/bbbb' })
+      stub_request(:head, 'https://bit.ly.x/bbbb')
+        .to_return(status: 301, headers: { Location: 'http://tinyurl.com.x/cccc' })
+      stub_request(:head, 'http://tinyurl.com.x/cccc')
+        .to_return(status: 301, headers: { Location: 'http://www.example.com/welcome' })
+      stub_request(:head, 'http://www.example.com/welcome')
+        .to_return(status: 200)
 
       (1..5).each do |i|
-        stub_request(:head, "http://2many.x/#{i}").
-          to_return(status: 301, headers: { Location: "http://2many.x/#{i+1}" })
+        stub_request(:head, "http://2many.x/#{i}")
+          .to_return(status: 301, headers: { Location: "http://2many.x/#{i + 1}" })
       end
-      stub_request(:head, 'http://2many.x/6').
-        to_return(status: 301, headers: { 'Content-Length' => '5' })
+      stub_request(:head, 'http://2many.x/6')
+        .to_return(status: 301, headers: { 'Content-Length' => '5' })
     end
 
     it 'should handle inaccessible URIs' do
@@ -171,20 +172,20 @@ def @filter.to_xpath_roundtrip(string)
     end
 
     it 'should detect a redirect loop' do
-      stub_request(:head, 'http://bad.x/aaaa').
-        to_return(status: 301, headers: { Location: 'http://bad.x/bbbb' })
-      stub_request(:head, 'http://bad.x/bbbb').
-        to_return(status: 301, headers: { Location: 'http://bad.x/aaaa' })
+      stub_request(:head, 'http://bad.x/aaaa')
+        .to_return(status: 301, headers: { Location: 'http://bad.x/bbbb' })
+      stub_request(:head, 'http://bad.x/bbbb')
+        .to_return(status: 301, headers: { Location: 'http://bad.x/aaaa' })
 
       expect(@filter.uri_expand('http://bad.x/aaaa')).to eq('http://bad.x/aaaa')
     end
 
     it 'should be able to handle an FTP URL' do
-      stub_request(:head, 'http://downloads.x/aaaa').
-        to_return(status: 301, headers: { Location: 'http://downloads.x/download?file=aaaa.zip' })
-      stub_request(:head, 'http://downloads.x/download').
-        with(query: { file: 'aaaa.zip' }).
-        to_return(status: 301, headers: { Location: 'ftp://downloads.x/pub/aaaa.zip' })
+      stub_request(:head, 'http://downloads.x/aaaa')
+        .to_return(status: 301, headers: { Location: 'http://downloads.x/download?file=aaaa.zip' })
+      stub_request(:head, 'http://downloads.x/download')
+        .with(query: { file: 'aaaa.zip' })
+        .to_return(status: 301, headers: { Location: 'ftp://downloads.x/pub/aaaa.zip' })
 
       expect(@filter.uri_expand('http://downloads.x/aaaa')).to eq('ftp://downloads.x/pub/aaaa.zip')
     end
@@ -223,7 +224,8 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should support escaped characters' do
       agent.interpolation_context['something'] = "foo\\1\n\nfoo\\bar\n\nfoo\\baz"
-      agent.options['test'] = "{{ something | regex_replace_first: '\\\\(\\w{2,})', '\\1\\\\' | regex_replace_first: '\\n+', '\\n'  }}"
+      agent.options['test'] =
+        "{{ something | regex_replace_first: '\\\\(\\w{2,})', '\\1\\\\' | regex_replace_first: '\\n+', '\\n'  }}"
       expect(agent.interpolated['test']).to eq("foo\\1\nfoobar\\\n\nfoo\\baz")
     end
   end
@@ -233,13 +235,15 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should extract the matched part' do
       agent.interpolation_context['something'] = "foo BAR BAZ"
-      agent.options['test'] = "{{ something | regex_extract: '[A-Z]+' }} / {{ something | regex_extract: '[A-Z]([A-Z]+)', 1 }} / {{ something | regex_extract: '(?.)AZ', 'x' }}"
+      agent.options['test'] =
+        "{{ something | regex_extract: '[A-Z]+' }} / {{ something | regex_extract: '[A-Z]([A-Z]+)', 1 }} / {{ something | regex_extract: '(?.)AZ', 'x' }}"
       expect(agent.interpolated['test']).to eq("BAR / AR / B")
     end
 
     it 'should return nil if not matched' do
       agent.interpolation_context['something'] = "foo BAR BAZ"
-      agent.options['test'] = "{% assign var = something | regex_extract: '[A-Z][a-z]+' %}{% if var == nil %}nil{% else %}non-nil{% endif %}"
+      agent.options['test'] =
+        "{% assign var = something | regex_extract: '[A-Z][a-z]+' %}{% if var == nil %}nil{% else %}non-nil{% endif %}"
       expect(agent.interpolated['test']).to eq("nil")
     end
   end
@@ -255,7 +259,8 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should support escaped characters' do
       agent.interpolation_context['something'] = "foo\\1\n\nfoo\\bar\n\nfoo\\baz"
-      agent.options['test'] = "{{ something | regex_replace: '\\\\(\\w{2,})', '\\1\\\\' | regex_replace: '\\n+', '\\n'  }}"
+      agent.options['test'] =
+        "{{ something | regex_replace: '\\\\(\\w{2,})', '\\1\\\\' | regex_replace: '\\n+', '\\n'  }}"
       expect(agent.interpolated['test']).to eq("foo\\1\nfoobar\\\nfoobaz\\")
     end
   end
@@ -265,21 +270,24 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should replace the first occurrence of a string using regex' do
       agent.interpolation_context['something'] = 'foobar zoobar'
-      agent.options['cleaned'] = '{% regex_replace_first "(?\S+)(?bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
+      agent.options['cleaned'] =
+        '{% regex_replace_first "(?\S+)(?bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
       expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
     end
 
     it 'should be able to take a pattern in a variable' do
       agent.interpolation_context['something'] = 'foobar zoobar'
       agent.interpolation_context['pattern'] = "(?\\S+)(?bar)"
-      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
+      agent.options['cleaned'] =
+        '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
       expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
     end
 
     it 'should define a variable named "match" in a "with" block' do
       agent.interpolation_context['something'] = 'foobar zoobar'
       agent.interpolation_context['pattern'] = "(?\\S+)(?bar)"
-      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ match.word | upcase }}{{ match["suffix"] }}{% endregex_replace_first %}'
+      agent.options['cleaned'] =
+        '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ match.word | upcase }}{{ match["suffix"] }}{% endregex_replace_first %}'
       expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
     end
   end
@@ -289,7 +297,8 @@ def @filter.to_xpath_roundtrip(string)
 
     it 'should replace the all occurrences of a string using regex' do
       agent.interpolation_context['something'] = 'foobar zoobar'
-      agent.options['cleaned'] = '{% regex_replace "(?\S+)(?bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace %}'
+      agent.options['cleaned'] =
+        '{% regex_replace "(?\S+)(?bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace %}'
       expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
     end
   end
@@ -304,9 +313,9 @@ def @filter.to_xpath_roundtrip(string)
     end
 
     it 'returns an object that was not modified in liquid' do
-      agent.interpolation_context['something'] = {'nested' => {'abc' => 'test'}}
+      agent.interpolation_context['something'] = { 'nested' => { 'abc' => 'test' } }
       agent.options['object'] = "{{something.nested | as_object}}"
-      expect(agent.interpolated['object']).to eq({"abc" => 'test'})
+      expect(agent.interpolated['object']).to eq({ "abc" => 'test' })
     end
 
     context 'as_json' do
@@ -315,19 +324,19 @@ def ensure_safety(obj)
       end
 
       it 'it converts "complex" objects' do
-        agent.interpolation_context['something'] = {'nested' => Service.new}
+        agent.interpolation_context['something'] = { 'nested' => Service.new }
         agent.options['object'] = "{{something | as_object}}"
-        expect(agent.interpolated['object']).to eq({'nested'=> ensure_safety(Service.new.as_json)})
+        expect(agent.interpolated['object']).to eq({ 'nested' => ensure_safety(Service.new.as_json) })
       end
 
-      it 'works with AgentDrops' do
+      it 'works with Agent::Drops' do
         agent.interpolation_context['something'] = agent
         agent.options['object'] = "{{something | as_object}}"
         expect(agent.interpolated['object']).to eq(ensure_safety(agent.to_liquid.as_json.stringify_keys))
       end
 
-      it 'works with EventDrops' do
-        event = Event.new(payload: {some: 'payload'}, agent: agent, created_at: Time.now)
+      it 'works with Event::Drops' do
+        event = Event.new(payload: { some: 'payload' }, agent:, created_at: Time.now)
         agent.interpolation_context['something'] = event
         agent.options['object'] = "{{something | as_object}}"
         expect(agent.interpolated['object']).to eq(ensure_safety(event.to_liquid.as_json.stringify_keys))
@@ -352,33 +361,33 @@ def ensure_safety(obj)
   describe 'rebase_hrefs' do
     let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
 
-    let(:fragment) { <
-  
  • - file1 -
  • -
  • - file2 -
  • -
  • - file3 -
  • - -HTML - - let(:replaced_fragment) { < -
  • - file1 -
  • -
  • - file2 -
  • -
  • - file3 -
  • - -HTML + let(:fragment) { <<~HTML } + + HTML + + let(:replaced_fragment) { <<~HTML } + + HTML it 'rebases relative URLs in a fragment' do agent.interpolation_context['content'] = fragment diff --git a/spec/models/agent_spec.rb b/spec/models/agent_spec.rb index a0622e63ae..de8756769b 100644 --- a/spec/models/agent_spec.rb +++ b/spec/models/agent_spec.rb @@ -967,7 +967,7 @@ def @agent.receive_webhook(params) end end -describe AgentDrop do +describe Agent::Drop do def interpolate(string, agent) agent.interpolate_string(string, "agent" => agent) end @@ -1032,9 +1032,9 @@ def interpolate(string, agent) end it 'should be created via Agent#to_liquid' do - expect(@wsa1.to_liquid.class).to be(AgentDrop) - expect(@wsa2.to_liquid.class).to be(AgentDrop) - expect(@efa.to_liquid.class).to be(AgentDrop) + expect(@wsa1.to_liquid.class).to be(Agent::Drop) + expect(@wsa2.to_liquid.class).to be(Agent::Drop) + expect(@efa.to_liquid.class).to be(Agent::Drop) end it 'should have .id, .type and .name' do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index fea103371c..7287fce8a5 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -23,7 +23,8 @@ lng: nil, radius: 0.0, speed: nil, - course: nil)) + course: nil + )) end it "returns a hash containing location information" do @@ -41,7 +42,8 @@ lng: 3.0, radius: 0.0, speed: 0.5, - course: 90.0)) + course: 90.0 + )) end end @@ -63,10 +65,10 @@ describe ".cleanup_expired!" do it "removes any Events whose expired_at date is non-null and in the past, updating Agent counter caches" do - half_hour_event = agents(:jane_weather_agent).create_event :expires_at => 20.minutes.from_now - one_hour_event = agents(:bob_weather_agent).create_event :expires_at => 1.hours.from_now - two_hour_event = agents(:jane_weather_agent).create_event :expires_at => 2.hours.from_now - three_hour_event = agents(:jane_weather_agent).create_event :expires_at => 3.hours.from_now + half_hour_event = agents(:jane_weather_agent).create_event expires_at: 20.minutes.from_now + one_hour_event = agents(:bob_weather_agent).create_event expires_at: 1.hours.from_now + two_hour_event = agents(:jane_weather_agent).create_event expires_at: 2.hours.from_now + three_hour_event = agents(:jane_weather_agent).create_event expires_at: 3.hours.from_now non_expiring_event = agents(:bob_weather_agent).create_event({}) initial_bob_count = agents(:bob_weather_agent).reload.events_count @@ -150,20 +152,20 @@ describe "when an event is created" do it "updates a counter cache on agent" do expect { - agents(:jane_weather_agent).events.create!(:user => users(:jane)) + agents(:jane_weather_agent).events.create!(user: users(:jane)) }.to change { agents(:jane_weather_agent).reload.events_count }.by(1) end it "updates last_event_at on agent" do expect { - agents(:jane_weather_agent).events.create!(:user => users(:jane)) + agents(:jane_weather_agent).events.create!(user: users(:jane)) }.to change { agents(:jane_weather_agent).reload.last_event_at } end end describe "when an event is updated" do it "does not touch the last_event_at on the agent" do - event = agents(:jane_weather_agent).events.create!(:user => users(:jane)) + event = agents(:jane_weather_agent).events.create!(user: users(:jane)) agents(:jane_weather_agent).update_attribute :last_event_at, 2.days.ago @@ -175,7 +177,7 @@ end end -describe EventDrop do +describe Event::Drop do def interpolate(string, event) event.agent.interpolate_string(string, event.to_liquid) end @@ -194,7 +196,7 @@ def interpolate(string, event) end it 'should be created via Agent#to_liquid' do - expect(@event.to_liquid.class).to be(EventDrop) + expect(@event.to_liquid.class).to be(Event::Drop) end it 'should have attributes of its payload' do From 7ffd0d04dd534a9430103207f9096a35d95f2059 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 20:34:39 +0900 Subject: [PATCH 18/32] Use the non-keyword parameter syntax --- app/concerns/web_request_concern.rb | 8 ++++---- spec/models/agents/twitter_stream_agent_spec.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/concerns/web_request_concern.rb b/app/concerns/web_request_concern.rb index 690939a845..82f7909bb4 100644 --- a/app/concerns/web_request_concern.rb +++ b/app/concerns/web_request_concern.rb @@ -15,11 +15,11 @@ def self.decode(val) end class CharacterEncoding < Faraday::Middleware - def initialize(app, force_encoding: nil, default_encoding: nil, unzip: nil) + def initialize(app, options = {}) super(app) - @force_encoding = force_encoding - @default_encoding = default_encoding - @unzip = unzip + @force_encoding = options[:force_encoding] + @default_encoding = options[:default_encoding] + @unzip = options[:unzip] end def call(env) diff --git a/spec/models/agents/twitter_stream_agent_spec.rb b/spec/models/agents/twitter_stream_agent_spec.rb index 8073629ac6..b614bb8487 100644 --- a/spec/models/agents/twitter_stream_agent_spec.rb +++ b/spec/models/agents/twitter_stream_agent_spec.rb @@ -189,7 +189,7 @@ it "yields received tweets" do expect(@worker).to receive(:stream!).with(['agent'], @agent).and_yield('status' => 'hello') - expect(@worker).to receive(:handle_status).with('status' => 'hello') + expect(@worker).to receive(:handle_status).with({ 'status' => 'hello' }) expect(Thread).to receive(:stop) @worker.run end From 1a9999c96b2732c15a7615961bb5d9259885a5d4 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 21:05:17 +0900 Subject: [PATCH 19/32] Update the http gem --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e72c772329..985ba9a112 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -236,7 +236,7 @@ GEM warden (~> 1.2.3) diff-lcs (1.5.0) docile (1.4.0) - domain_name (0.5.20170404) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) em-http-request (1.1.7) addressable (>= 2.3.4) @@ -385,14 +385,14 @@ GEM httparty (>= 0.7.3) mimemagic multipart-post - http (2.1.0) + http (2.2.2) addressable (~> 2.3) http-cookie (~> 1.0) http-form_data (~> 1.0.1) http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) + http-cookie (1.0.5) domain_name (~> 0.5) - http-form_data (1.0.1) + http-form_data (1.0.3) http_parser.rb (0.6.0) httparty (0.14.0) multi_xml (>= 0.5.2) @@ -750,7 +750,7 @@ GEM json (>= 1.8.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.4) + unf_ext (0.0.8.2) unicode-display_width (2.4.2) unicorn (6.1.0) kgio (~> 2.6) From 9c670a0aa6806741a202ae87244013f03f8047c6 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 21:45:07 +0900 Subject: [PATCH 20/32] Upgrade rr and rspec-mocks --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 985ba9a112..986405bdf5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -616,7 +616,7 @@ GEM retriable (3.1.2) rexml (3.2.5) rly (0.2.3) - rr (3.0.9) + rr (3.1.0) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -631,7 +631,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.3) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) From c6519a0801448040af1c56f2d6715dcf4958011a Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Mon, 17 Apr 2023 21:52:23 +0900 Subject: [PATCH 21/32] Use hover --- spec/features/create_an_agent_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/create_an_agent_spec.rb b/spec/features/create_an_agent_spec.rb index f5103a3379..4196755d07 100644 --- a/spec/features/create_an_agent_spec.rb +++ b/spec/features/create_an_agent_spec.rb @@ -7,7 +7,7 @@ it "creates an agent" do visit "/" - page.find("a", text: "Agents").trigger(:mouseover) + page.find("a", text: "Agents").hover click_on("New Agent") select_agent_type("Trigger Agent") @@ -43,7 +43,7 @@ it "creates an agent with a source and a receiver" do visit "/" - page.find("a", text: "Agents").trigger(:mouseover) + page.find("a", text: "Agents").hover click_on("New Agent") select_agent_type("Trigger Agent") @@ -64,7 +64,7 @@ it "creates an agent with a control target" do visit "/" - page.find("a", text: "Agents").trigger(:mouseover) + page.find("a", text: "Agents").hover click_on("New Agent") select_agent_type("Scheduler Agent") @@ -83,7 +83,7 @@ it "creates an agent with a controller" do visit "/" - page.find("a", text: "Agents").trigger(:mouseover) + page.find("a", text: "Agents").hover click_on("New Agent") select_agent_type("Weather Agent") @@ -103,7 +103,7 @@ it "creates an alert if a new agent with invalid json is submitted" do visit "/" - page.find("a", text: "Agents").trigger(:mouseover) + page.find("a", text: "Agents").hover click_on("New Agent") select_agent_type("Trigger Agent") From 9699362a4dee95026d20e4dac504f571f3c8289d Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Tue, 18 Apr 2023 02:13:57 +0900 Subject: [PATCH 22/32] Upgrade select2 and capybara-select-2 --- Gemfile | 6 ++---- Gemfile.lock | 9 ++++----- app/assets/javascripts/components/core.js | 12 ++++++------ .../javascripts/pages/agent-edit-page.js | 18 +++++++++--------- spec/support/feature_helpers.rb | 5 +++-- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index 5ef1f3c1e1..27e90ae86b 100644 --- a/Gemfile +++ b/Gemfile @@ -126,7 +126,7 @@ gem 'rails', '~> 6.1.7' gem 'rails-html-sanitizer', '~> 1.2' gem 'rufus-scheduler', '~> 3.4', require: false gem 'sass-rails', '>= 6.0' -gem 'select2-rails', '~> 3.5.4' +gem 'select2-rails' gem 'spectrum-rails' gem 'sprockets' gem 'typhoeus', '~> 1.3.1' @@ -158,9 +158,7 @@ group :development do group :test do gem 'capybara' - gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', - ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', - require: false + gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', require: false gem 'puma' gem 'rails-controller-testing' gem 'rr', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 986405bdf5..3a438bdc6a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,8 @@ GIT remote: https://github.com/Hirurg103/capybara_select2.git - revision: fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76 - ref: fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76 + revision: 48e2c458c2827cf2af2b0a287b30468fa8d761bf specs: - capybara-select-2 (0.3.2) + capybara-select-2 (0.5.1) GIT remote: https://github.com/albertsun/tumblr_client.git @@ -687,7 +686,7 @@ GEM sprockets-rails tilt sax-machine (1.3.2) - select2-rails (3.5.11) + select2-rails (4.0.13) selenium-webdriver (4.8.6) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) @@ -873,7 +872,7 @@ DEPENDENCIES rubocop-rspec rufus-scheduler (~> 3.4) sass-rails (>= 6.0) - select2-rails (~> 3.5.4) + select2-rails selenium-webdriver shoulda-matchers simplecov diff --git a/app/assets/javascripts/components/core.js b/app/assets/javascripts/components/core.js index 1cedbd21f9..7159077250 100644 --- a/app/assets/javascripts/components/core.js +++ b/app/assets/javascripts/components/core.js @@ -23,12 +23,12 @@ $(function () { $(".select2-linked-tags").select2({ width: "resolve", - formatSelection(obj) { - return `${Utils.escape( - obj.text - )}`; + templateSelection: ({ id, text, element }) => { + const a = document.createElement("a"); + a.href = `${element.closest("select").dataset.urlPrefix}/${id}/edit`; + a.onClick = "Utils.select2TagClickHandler(event, this)"; + a.appendChild(document.createTextNode(text)); + return a; }, }); diff --git a/app/assets/javascripts/pages/agent-edit-page.js b/app/assets/javascripts/pages/agent-edit-page.js index 494601546f..56edc7f164 100644 --- a/app/assets/javascripts/pages/agent-edit-page.js +++ b/app/assets/javascripts/pages/agent-edit-page.js @@ -77,15 +77,15 @@ $("select#agent_type").select2({ width: "resolve", formatResult: formatAgentForSelect, - escapeMarkup(m) { - return m; - }, - matcher(term, text, opt) { - const description = opt.attr("title"); - return ( - text.toUpperCase().indexOf(term.toUpperCase()) >= 0 || - description.toUpperCase().indexOf(term.toUpperCase()) >= 0 - ); + escapeMarkup: (m) => m, + matcher: (params, data) => { + const term = params.term; + if (term == null) return data; + const upperTerm = term.toUpperCase(); + return data.text.toUpperCase().indexOf(upperTerm) >= 0 || + data.title.toUpperCase().indexOf(upperTerm) >= 0 + ? data + : null; }, }); } else { diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 836586dfa9..f8a649c9d8 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -1,6 +1,7 @@ module FeatureHelpers - def select_agent_type(type) - select2(type, from: "Type") + def select_agent_type(search) + agent_name = search[/\A.*?Agent\b/] || search + select2(agent_name, search:, from: "Type") # Wait for all parts of the Agent form to load: expect(page).to have_css("div.function_buttons") # Options editor From 13a619c363f768b54ba0537589df0e26dfa076ae Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Thu, 20 Apr 2023 02:39:57 +0900 Subject: [PATCH 23/32] Adapt to select2 version 4 for :array options of form_configurable - Use the `