diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4a1307da6f..4a55f7a504 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"
@@ -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 e057571b3e..56675a7ed5 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'
+gem 'net-ftp-list' # FtpsiteAgent
+gem 'rturk', '~> 2.12.1' # HumanTaskAgent
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 'ace-rails-ap'
+gem 'bootsnap', require: false
gem 'bootstrap-kaminari-views', '~> 0.0.3'
gem 'bundler', '>= 1.5.0'
gem 'coffee-rails', '~> 5'
@@ -97,17 +99,18 @@ 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'
gem 'font-awesome-sass', '~> 4.7.0'
-gem 'foreman', '~> 0.63.0'
+gem 'foreman', '~> 0.87.2'
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,15 @@ 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 'select2-rails'
gem 'spectrum-rails'
-gem 'execjs'
+gem 'sprockets'
+gem 'terser'
gem 'typhoeus', '~> 1.3.1'
gem 'uglifier', '~> 2.7.2'
-gem 'bootsnap', require: false
group :development do
gem 'better_errors'
@@ -137,38 +139,41 @@ 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 'poltergeist'
- gem 'pry-rails'
- gem 'pry-byebug'
+ gem 'capybara'
+ gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', require: false
+ gem 'puma'
+ 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 '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
@@ -178,18 +183,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 ca81053d41..56692e6522 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
@@ -67,15 +66,15 @@ GIT
GIT
remote: https://github.com/sferik/twitter.git
- revision: d11707edf4abd13f7ada0eef57fc1eaa1062d75b
+ revision: 5a49cb6b6c84ccc8f2f980c42d3e9f852033735a
specs:
- twitter (5.15.0)
+ twitter (8.0.0)
addressable (~> 2.3)
- buftok (~> 0.2.0)
+ buftok (~> 0.3.0)
equalizer (~> 0.0.11)
- http (~> 2.0)
- http-form_data (~> 1.0)
- http_parser.rb (~> 0.6.0)
+ http (~> 5.1)
+ http-form_data (~> 2.3)
+ llhttp-ffi (~> 0.4.0)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
@@ -91,7 +90,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,10 +150,11 @@ 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)
+ ast (2.4.2)
aws-eventstream (1.2.0)
aws-partitions (1.547.0)
aws-sdk-core (3.125.2)
@@ -184,9 +184,8 @@ GEM
bootstrap-kaminari-views (0.0.5)
kaminari (>= 0.13)
rails (>= 3.1)
- buftok (0.2.0)
+ buftok (0.3.0)
builder (3.2.4)
- byebug (11.1.3)
capistrano (3.16.0)
airbrussh (>= 1.0.0)
i18n
@@ -197,17 +196,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)
@@ -216,14 +213,8 @@ 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)
- 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)
@@ -244,7 +235,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)
@@ -269,7 +260,7 @@ GEM
evernote-thrift
oauth (>= 0.4.1)
execjs (2.8.1)
- faraday (0.17.4)
+ faraday (0.17.6)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
@@ -277,15 +268,16 @@ GEM
loofah (>= 2.3.1)
sax-machine (>= 1.0)
ffi (1.15.5)
+ ffi-compiler (1.0.1)
+ ffi (>= 1.0.0)
+ rake
font-awesome-sass (4.7.0)
sass (>= 3.2)
forecast_io (2.0.1)
faraday
hashie
multi_json
- foreman (0.63.0)
- dotenv (>= 0.7)
- thor (>= 0.13.6)
+ foreman (0.87.2)
formatador (1.1.0)
fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8)
@@ -393,14 +385,14 @@ GEM
httparty (>= 0.7.3)
mimemagic
multipart-post
- http (2.1.0)
- addressable (~> 2.3)
+ http (5.1.1)
+ addressable (~> 2.8)
http-cookie (~> 1.0)
- http-form_data (~> 1.0.1)
- http_parser.rb (~> 0.6.0)
- http-cookie (1.0.3)
+ http-form_data (~> 2.2)
+ llhttp-ffi (~> 0.4.0)
+ http-cookie (1.0.5)
domain_name (~> 0.5)
- http-form_data (1.0.1)
+ http-form_data (2.3.0)
http_parser.rb (0.6.0)
httparty (0.14.0)
multi_xml (>= 0.5.2)
@@ -417,7 +409,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)
@@ -449,22 +441,24 @@ 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)
+ llhttp-ffi (0.4.0)
+ ffi-compiler (~> 1.0)
+ rake (~> 13.0)
loofah (2.20.0)
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
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)
@@ -480,15 +474,18 @@ 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)
multi_xml (0.6.0)
- multipart-post (2.1.1)
+ multipart-post (2.3.0)
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
@@ -551,28 +548,23 @@ 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)
- 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)
- 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)
+ 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)
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)
@@ -589,10 +581,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)
@@ -604,6 +596,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 +604,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)
@@ -625,7 +619,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)
@@ -640,7 +634,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)
@@ -656,8 +650,28 @@ GEM
erector
nokogiri
rest-client
- ruby-growl (4.1)
- uuid (~> 2.3, >= 2.3.5)
+ 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-progressbar (1.13.0)
+ rubyzip (2.3.2)
rufus-scheduler (3.8.1)
fugit (~> 1.1, >= 1.1.6)
sass (3.7.4)
@@ -676,7 +690,11 @@ 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)
+ websocket (~> 1.0)
shellany (0.0.1)
shoulda-matchers (4.0.1)
activesupport (>= 4.2.0)
@@ -686,11 +704,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)
@@ -700,9 +720,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)
@@ -710,16 +730,14 @@ GEM
sshkit (1.21.2)
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)
+ terser (1.1.14)
+ execjs (>= 0.3.0, < 3)
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)
@@ -737,13 +755,12 @@ 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)
raindrops (~> 0.7)
- uuid (2.3.7)
- macaddr (~> 1.0)
- vcr (3.0.3)
+ vcr (6.1.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
@@ -751,16 +768,17 @@ 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)
xmpp4r (0.5.6)
- xpath (3.0.0)
+ xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.7)
@@ -772,7 +790,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
- ace-rails-ap (~> 2.0.1)
+ ace-rails-ap
aws-sdk-s3 (~> 1)
better_errors
binding_of_caller
@@ -782,11 +800,9 @@ DEPENDENCIES
capistrano
capistrano-bundler
capistrano-rails
- capybara (~> 2.18)
- capybara-screenshot
+ capybara
capybara-select-2!
coffee-rails (~> 5)
- coveralls
daemons (~> 1.1.9)
delayed_job
delayed_job_active_record
@@ -804,7 +820,7 @@ DEPENDENCIES
ffi (>= 1.9.4)
font-awesome-sass (~> 4.7.0)
forecast_io (~> 2.0.0)
- foreman (~> 0.63.0)
+ foreman (~> 0.87.2)
geokit (~> 1.13)
geokit-rails (~> 2.3)
gmail_xoauth
@@ -834,7 +850,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!
@@ -843,9 +860,7 @@ DEPENDENCIES
omniauth-tumblr
omniauth-twitter
pg (~> 1.1)
- poltergeist
- pry-byebug
- pry-rails
+ puma
rack-livereload
rails (~> 6.1.7)
rails-controller-testing
@@ -858,17 +873,23 @@ DEPENDENCIES
rspec-mocks
rspec-rails
rturk (~> 2.12.1)
- ruby-growl (~> 4.1.0)
+ rubocop
+ rubocop-performance
+ rubocop-rspec
rufus-scheduler (~> 3.4)
sass-rails (>= 6.0)
- select2-rails (~> 3.5.4)
+ select2-rails
+ selenium-webdriver
shoulda-matchers
+ simplecov
+ simplecov-lcov (~> 0.8.0)
slack-notifier (~> 1.0.0)
spectrum-rails
spring
spring-commands-rspec
spring-watcher-listen
- sprockets (~> 3.7.2)
+ sprockets
+ terser
tumblr_client!
twilio-ruby (~> 5.62.0)
twitter!
@@ -880,12 +901,12 @@ DEPENDENCIES
unicorn
vcr
web-console (>= 3.3.0)
- webmock (~> 3.5.1)
+ webmock
weibo_2!
xmpp4r (~> 0.5.6)
RUBY VERSION
- ruby 2.7.6p219
+ ruby 3.2.2p53
BUNDLED WITH
2.4.12
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
new file mode 100644
index 0000000000..b16e53d6d5
--- /dev/null
+++ b/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= 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..7159077250
--- /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",
+ 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;
+ },
+ });
+
+ // 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..e38ee66d1a
--- /dev/null
+++ b/app/assets/javascripts/components/form_configurable.js
@@ -0,0 +1,95 @@
+$(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 () {
+ $("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");
+ },
+ error: (data) => {
+ form_group.addClass("has-feedback").addClass("has-error");
+ form_group.find("span").addClass("hidden");
+ form_group.find(".glyphicon-remove").removeClass("hidden");
+ },
+ });
+ }
+ );
+
+ $("input[role~=validatable], select[role~=validatable]").trigger("change");
+
+ $.each($("select[role~=completable]"), (i, select) => {
+ const $select = $(select);
+ const value = $select.data("value");
+
+ const setValue = (value) => {
+ if (
+ $select
+ .find("option")
+ .toArray()
+ .some((option) => option.value == value)
+ ) {
+ $select.val(value).trigger("change");
+ } else {
+ $select
+ .append(new Option(value, value, true, true))
+ .trigger("change");
+ }
+ };
+
+ if ($select.data("cacheResponse")) {
+ const loadData = (data) => {
+ $select.select2({ data: data, tags: true });
+ setValue(value);
+ };
+
+ $.ajax("/agents/complete", {
+ type: "POST",
+ data: getFormData(select),
+ success: (data) => loadData(data),
+ error: (data) =>
+ loadData([{ id: undefined, text: "Error loading data." }]),
+ });
+ } else {
+ $select.select2({
+ ajax: {
+ url: "/agents/complete",
+ type: "POST",
+ data: (params) => getFormData(select),
+ processResults: (data) => ({ results: data }),
+ },
+ tags: true,
+ });
+ setValue(value);
+ }
+ });
+
+ $("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") {
+ input.removeClass("hidden");
+ } else {
+ input.val($(e.currentTarget).val());
+ 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..56edc7f164
--- /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) => 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 {
+ 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/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
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/liquid_droppable.rb b/app/concerns/liquid_droppable.rb
index bad29253d5..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
@@ -23,21 +18,9 @@ def each
def as_json
return {} unless defined?(self.class::METHODS)
- Hash[self.class::METHODS.map { |m| [m, send(m).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)
+ self.class::METHODS.to_h { |m| [m, send(m).as_json] }
+ end
end
class MatchDataDrop < Drop
diff --git a/app/concerns/twitter_concern.rb b/app/concerns/twitter_concern.rb
index c0c78b4897..54261c6a1f 100644
--- a/app/concerns/twitter_concern.rb
+++ b/app/concerns/twitter_concern.rb
@@ -85,19 +85,10 @@ def format_tweet(tweet)
text.gsub!(RE_HTML_ENTITIES, HTML_ENTITIES)
expanded_text.gsub!(RE_HTML_ENTITIES, HTML_ENTITIES)
- if attrs[:text]
- {
- **attrs,
- text: text,
- expanded_text: expanded_text,
- }
- else
- {
- **attrs,
- full_text: text,
- expanded_text: expanded_text,
- }
- end
+ attrs[:text] &&= text
+ attrs[:full_text] &&= text
+
+ attrs.update(expanded_text:)
end
module_function :format_tweet
diff --git a/app/concerns/web_request_concern.rb b/app/concerns/web_request_concern.rb
index ead57151a2..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)
@@ -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/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/app/helpers/jobs_helper.rb b/app/helpers/jobs_helper.rb
index 305e38f172..8a23c59691 100644
--- a/app/helpers/jobs_helper.rb
+++ b/app/helpers/jobs_helper.rb
@@ -1,5 +1,4 @@
module JobsHelper
-
def status(job)
case
when job.failed_at
@@ -24,7 +23,7 @@ def relative_distance_of_time_in_words(time)
#
# Can return nil, or an instance of Agent.
def agent_from_job(job)
- data = YAML.load(job.handler.to_s).try(:job_data)
+ data = YAML.unsafe_load(job.handler.to_s).try(:job_data)
case data['job_class']
when 'AgentCheckJob', 'AgentReceiveJob'
Agent.find_by_id(data['arguments'][0])
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..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
@@ -325,7 +324,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 +453,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)
@@ -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/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..aae045fc8a 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
@@ -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/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..fdda960f84 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,22 @@ 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 )
+ 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 ed63dcf187..9905ce6534 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?
@@ -53,8 +54,8 @@ def check
def ls(dir_to_watch)
dropbox.ls(dir_to_watch)
- .select { |entry| entry.respond_to?(:rev) }
- .map { |file| { 'path' => file.path, 'rev' => file.rev, 'modified' => file.server_modified } }
+ .select { |entry| entry.respond_to?(:rev) }
+ .map { |file| { 'path' => file.path, 'rev' => file.rev, 'modified' => file.server_modified } }
end
def previous_contents
@@ -69,7 +70,8 @@ def remember(contents)
class DropboxDirDiff
def initialize(previous, current)
- @previous, @current = [previous || [], current || []]
+ @previous = previous || []
+ @current = current || []
end
def empty?
@@ -101,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..98f4d3f9ad 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:
@@ -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.
@@ -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
deleted file mode 100644
index 66cd9bd20f..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: subject,
- message: 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/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 %}
+
+ {{ event.title }}
+ Click here to see
+
+ {% endfor %}
+
+ 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 4bf5f32498..63a7548431 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)
expanded_path = File.expand_path(mo['path'])
@@ -153,7 +156,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
@@ -173,7 +177,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)
@@ -182,9 +186,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/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..7e827cb3d4 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
@@ -98,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,
@@ -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..0148ad9908 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,43 +96,52 @@ 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
-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/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..6d40d63fe1 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,30 @@ 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})
+ when :array
+ @view.select_tag "agent[options][#{attribute}]", nil,
+ html_options.deep_merge(class: 'form-control',
+ data: { value:, cache_response: data[:cache_response] != false })
+ when :string
+ @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/config/environments/production.rb b/config/environments/production.rb
index f2475bb530..290d52f824 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -30,18 +30,18 @@
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
if ENV["RAILS_LOG_TO_STDOUT"].present? ||
- 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['ON_HEROKU'] ||
+ ENV['HEROKU_POSTGRESQL_ROSE_URL'] ||
+ ENV['HEROKU_POSTGRESQL_GOLD_URL'] ||
+ File.read(File.join(File.dirname(__FILE__), '../../Procfile')) =~ /intended for Heroku/
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Compress JavaScripts and CSS
- config.assets.js_compressor = :uglifier
- config.assets.css_compressor = :sass
+ config.assets.js_compressor = :terser
+ config.assets.css_compressor = :scss
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
@@ -69,7 +69,7 @@
config.log_level = :info
# Prepend all log lines with the following tags.
- config.log_tags = [ :request_id ]
+ config.log_tags = [:request_id]
# Use a different cache store in production.
config.cache_store = :memory_store
@@ -86,7 +86,7 @@
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
- config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] }
+ config.action_mailer.default_url_options = { host: ENV['DOMAIN'] }
config.action_mailer.asset_host = ENV['DOMAIN']
if ENV['ASSET_HOST'].present?
config.action_mailer.asset_host = ENV['ASSET_HOST']
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
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/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/doc/manual/installation.md b/doc/manual/installation.md
index a55fba05f8..a392d46e9e 100644
--- a/doc/manual/installation.md
+++ b/doc/manual/installation.md
@@ -46,7 +46,7 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
- sudo apt-get install -y runit build-essential git zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs graphviz jq shared-mime-info
+ sudo apt-get install -y runit build-essential git zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate pkg-config cmake nodejs graphviz jq shared-mime-info
### Debian Stretch
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/multi-process/scripts/standalone-packages b/docker/multi-process/scripts/standalone-packages
index cb9dc1f542..120397c024 100755
--- a/docker/multi-process/scripts/standalone-packages
+++ b/docker/multi-process/scripts/standalone-packages
@@ -1,9 +1,18 @@
+set -e
+
export DEBIAN_FRONTEND=noninteractive
+
apt-get update
-apt-get install -y python2.7 python-docutils mysql-server \
- supervisor python-pip && \
+apt-get install -y gnupg
+
+echo "deb http://repo.mysql.com/apt/ubuntu/ bionic mysql-5.7" > /etc/apt/sources.list.d/mysql.list
+apt-key adv --keyserver pgp.mit.edu --recv-keys 3A79BD29
+apt-get update
+
+apt-get install -y --allow-downgrades python3-pip mysql-server supervisor \
+ mysql-server=5.7.42-1ubuntu18.04 mysql-client=5.7.42-1ubuntu18.04 libmysqlclient-dev=5.7.42-1ubuntu18.04 && \
apt-get -y clean
-pip install supervisor-stdout
+pip install git+https://github.com/coderanger/supervisor-stdout
rm -rf /var/lib/apt/lists/*
rm -rf /usr/share/doc/
rm -rf /usr/share/man/
@@ -14,10 +23,13 @@ mkdir -p /var/log/supervisor /var/log/mysql
chgrp -R 0 /etc/supervisor /var/lib/mysql /var/log/supervisor /var/log/mysql
chmod -R g=u /etc/supervisor /var/lib/mysql /var/log/supervisor /var/log/mysql
sed -r -i /etc/mysql/mysql.conf.d/mysqld.cnf \
- -e 's/^ *user *.+/user=1001/' \
-e 's#/var/run/mysqld/mysqld.sock#/app/tmp/sockets/mysqld.sock#' \
-e 's#/var/run/mysqld/mysqld.pid#/app/tmp/pids/mysqld.pid#'
-sed -r -i /etc/mysql/debian.cnf \
- -e 's#/var/run/mysqld/mysqld.sock#/app/tmp/sockets/mysqld.sock#'
-cp /etc/mysql/debian.cnf /etc/mysql/mysql.conf.d/client.cnf
-chmod 644 /etc/mysql/mysql.conf.d/client.cnf
+echo "user=1001" >> /etc/mysql/mysql.conf.d/mysqld.cnf
+cat >> /etc/mysql/conf.d/mysql.cnf << EOF
+[client]
+socket = /app/tmp/sockets/mysqld.sock
+[mysql_upgrade]
+socket = /app/tmp/sockets/mysqld.sock
+find /etc/mysql/
+EOF
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
diff --git a/lib/location.rb b/lib/location.rb
index e6db847659..7eb45b4f2c 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?
@@ -101,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
- def liquid_method_missing(key)
- if KEYS.include?(key)
- @object.__send__(key)
+ 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)
+ end
end
end
end
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
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/concerns/twitter_concern_spec.rb b/spec/concerns/twitter_concern_spec.rb
index d6eb98c8ec..fe705177f3 100644
--- a/spec/concerns/twitter_concern_spec.rb
+++ b/spec/concerns/twitter_concern_spec.rb
@@ -107,6 +107,7 @@ class TestTwitterAgent < Agent
context "when a Twitter::Tweet object is given" do
let(:input) { Twitter::Tweet.new(tweet_hash) }
+ let(:expected) { super().then { |attrs| attrs.update(text: attrs[:full_text]) } }
it "formats a tweet" do
expect(subject).to eq(expected)
end
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..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")
@@ -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..de8756769b 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
@@ -960,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
@@ -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
@@ -1022,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
@@ -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/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
diff --git a/spec/models/agents/shell_command_agent_spec.rb b/spec/models/agents/shell_command_agent_spec.rb
index 0b82a44fb8..800f2ad314 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',
}
@@ -60,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
@@ -74,13 +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, 'echo $BUNDLE_GEMFILE', nil, unbundle: true) { orig_run_command.(@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
+ 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)
+ }
+ 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
@@ -93,7 +99,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 +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
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
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
diff --git a/spec/presenters/form_configurable_agent_presenter_spec.rb b/spec/presenters/form_configurable_agent_presenter_spec.rb
index 6f7e39a584..64ece21ed3 100644
--- a/spec/presenters/form_configurable_agent_presenter_spec.rb
+++ b/spec/presenters/form_configurable_agent_presenter_spec.rb
@@ -12,30 +12,61 @@ class FormConfigurableAgentPresenterAgent < Agent
end
before(:all) do
- @presenter = FormConfigurableAgentPresenter.new(FormConfigurableAgentPresenterAgent.new, ActionController::Base.new.view_context)
+ @presenter = FormConfigurableAgentPresenter.new(FormConfigurableAgentPresenterAgent.new,
+ ActionController::Base.new.view_context)
end
it "works for the type :string" do
expect(@presenter.option_field_for(:string)).to(
- have_tag('input', with: {:'data-attribute' => 'string', role: 'validatable form-configurable', type: 'text', name: 'agent[options][string]'})
+ have_tag(
+ 'input',
+ with: {
+ 'data-attribute': 'string',
+ role: 'validatable form-configurable',
+ type: 'text',
+ name: 'agent[options][string]'
+ }
+ )
)
end
it "works for the type :text" do
expect(@presenter.option_field_for(:text)).to(
- have_tag('textarea', with: {:'data-attribute' => 'text', role: 'completable form-configurable', name: 'agent[options][text]'})
+ have_tag(
+ 'textarea',
+ with: {
+ 'data-attribute': 'text',
+ role: 'completable form-configurable',
+ name: 'agent[options][text]'
+ }
+ )
)
end
it "works for the type :boolean" do
expect(@presenter.option_field_for(:boolean)).to(
- have_tag('input', with: {:'data-attribute' => 'boolean', role: 'form-configurable', name: 'agent[options][boolean_radio]', type: 'radio'})
+ have_tag(
+ 'input',
+ with: {
+ 'data-attribute': 'boolean',
+ role: 'form-configurable',
+ name: 'agent[options][boolean_radio]',
+ type: 'radio'
+ }
+ )
)
end
it "works for the type :array" do
expect(@presenter.option_field_for(:array)).to(
- have_tag('input', with: {:'data-attribute' => 'array', role: 'completable form-configurable', type: 'text', name: 'agent[options][array]'})
+ have_tag(
+ 'select',
+ with: {
+ 'data-attribute': 'array',
+ role: 'completable form-configurable',
+ name: 'agent[options][array]'
+ }
+ )
)
end
-end
\ No newline at end of file
+end
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'
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