| @@ -86,7 +86,7 @@ gem 'sass-rails', '~> 5.0' | ||
| 86 | 86 | gem 'select2-rails', '~> 3.5.4' | 
| 87 | 87 | gem 'spectrum-rails' | 
| 88 | 88 | gem 'string-scrub' # for ruby <2.1 | 
| 89 | -gem 'therubyracer', '~> 0.12.1' | |
| 89 | +gem 'therubyracer', '~> 0.12.2' | |
| 90 | 90 | gem 'typhoeus', '~> 0.6.3' | 
| 91 | 91 | gem 'uglifier', '>= 1.3.0' | 
| 92 | 92 |  | 
| @@ -427,7 +427,7 @@ GEM | ||
| 427 | 427 | systemu (2.6.4) | 
| 428 | 428 | term-ansicolor (1.3.0) | 
| 429 | 429 | tins (~> 1.0) | 
| 430 | - therubyracer (0.12.1) | |
| 430 | + therubyracer (0.12.2) | |
| 431 | 431 | libv8 (~> 3.16.14.0) | 
| 432 | 432 | ref | 
| 433 | 433 | thor (0.19.1) | 
| @@ -565,7 +565,7 @@ DEPENDENCIES | ||
| 565 | 565 | spring (~> 1.3.0) | 
| 566 | 566 | spring-commands-rspec | 
| 567 | 567 | string-scrub | 
| 568 | - therubyracer (~> 0.12.1) | |
| 568 | + therubyracer (~> 0.12.2) | |
| 569 | 569 | tumblr_client | 
| 570 | 570 | twilio-ruby (~> 3.11.5) | 
| 571 | 571 | twitter (~> 5.14.0) | 
| @@ -0,0 +1,4 @@ | ||
| 1 | +#= require ace/ace | |
| 2 | +#= require ace/mode-javascript.js | |
| 3 | +#= require ace/mode-markdown.js | |
| 4 | +#= require ace/mode-coffee.js | 
| @@ -2,6 +2,7 @@ class @AgentEditPage | ||
| 2 | 2 | constructor: -> | 
| 3 | 3 |      $("#agent_source_ids").on "change", @showEventDescriptions | 
| 4 | 4 | @showCorrectRegionsOnStartup() | 
| 5 | +    $("form.agent-form").on "submit", => @updateFromEditors() | |
| 5 | 6 |  | 
| 6 | 7 |      $("#agent_name").each -> | 
| 7 | 8 | # Select the number suffix if this is a cloned agent. | 
| @@ -17,6 +18,7 @@ class @AgentEditPage | ||
| 17 | 18 | @handleTypeChange(true) | 
| 18 | 19 | else | 
| 19 | 20 | @enableDryRunButton() | 
| 21 | + @buildAce() | |
| 20 | 22 |  | 
| 21 | 23 | handleTypeChange: (firstTime) -> | 
| 22 | 24 |      $(".event-descriptions").html("").hide() | 
| @@ -61,6 +63,7 @@ class @AgentEditPage | ||
| 61 | 63 | window.jsonEditor = setupJsonEditor()[0] | 
| 62 | 64 |  | 
| 63 | 65 | @enableDryRunButton() | 
| 66 | + @buildAce() | |
| 64 | 67 |  | 
| 65 | 68 | window.initializeFormCompletable() | 
| 66 | 69 |  | 
| @@ -134,15 +137,45 @@ class @AgentEditPage | ||
| 134 | 137 | else | 
| 135 | 138 | @hideEventCreation() | 
| 136 | 139 |  | 
| 140 | + buildAce: -> | |
| 141 | +    $(".ace-editor").each -> | |
| 142 | +      unless $(this).data('initialized') | |
| 143 | +        $(this).data('initialized', true) | |
| 144 | +        $source = $($(this).data('source')).hide() | |
| 145 | + editor = ace.edit(this) | |
| 146 | +        $(this).data('ace-editor', editor) | |
| 147 | + session = editor.getSession() | |
| 148 | + session.setTabSize(2) | |
| 149 | + session.setUseSoftTabs(true) | |
| 150 | + session.setUseWrapMode(false) | |
| 151 | +        editor.setTheme("ace/theme/chrome") | |
| 152 | + | |
| 153 | + setSyntax = -> | |
| 154 | +          switch $("[name='agent[options][language]']").val() | |
| 155 | +            when 'JavaScript' then session.setMode("ace/mode/javascript") | |
| 156 | +            when 'CoffeeScript' then session.setMode("ace/mode/coffee") | |
| 157 | +            else session.setMode("ace/mode/text") | |
| 158 | + | |
| 159 | +        $("[name='agent[options][language]']").on 'change', setSyntax | |
| 160 | + setSyntax() | |
| 161 | + | |
| 162 | + session.setValue($source.val()) | |
| 163 | + | |
| 164 | + updateFromEditors: -> | |
| 165 | +    $(".ace-editor").each -> | |
| 166 | +      $source = $($(this).data('source')) | |
| 167 | +      $source.val($(this).data('ace-editor').getSession().getValue()) | |
| 168 | + | |
| 137 | 169 | enableDryRunButton: -> | 
| 138 | 170 |      $(".agent-dry-run-button").prop('disabled', false).off().on "click", @invokeDryRun | 
| 139 | 171 |  | 
| 140 | 172 | disableDryRunButton: -> | 
| 141 | 173 |      $(".agent-dry-run-button").prop('disabled', true) | 
| 142 | 174 |  | 
| 143 | - invokeDryRun: (e) -> | |
| 175 | + invokeDryRun: (e) => | |
| 144 | 176 | e.preventDefault() | 
| 145 | - Utils.handleDryRunButton(this) | |
| 177 | + @updateFromEditors() | |
| 178 | + Utils.handleDryRunButton(e.target) | |
| 146 | 179 |  | 
| 147 | 180 | $ -> | 
| 148 | 181 | Utils.registerPage(AgentEditPage, forPathsMatching: /^agents/) | 
| @@ -0,0 +1,26 @@ | ||
| 1 | +class @UserCredentialPage | |
| 2 | + constructor: -> | |
| 3 | +    editor = ace.edit("ace-credential-value") | |
| 4 | + editor.getSession().setTabSize(2) | |
| 5 | + editor.getSession().setUseSoftTabs(true) | |
| 6 | + editor.getSession().setUseWrapMode(false) | |
| 7 | +    editor.setTheme("ace/theme/chrome") | |
| 8 | + | |
| 9 | + setMode = -> | |
| 10 | +      mode = $("#user_credential_mode").val() | |
| 11 | + if mode == 'java_script' | |
| 12 | +        editor.getSession().setMode("ace/mode/javascript") | |
| 13 | + else | |
| 14 | +        editor.getSession().setMode("ace/mode/text") | |
| 15 | + | |
| 16 | + setMode() | |
| 17 | +    $("#user_credential_mode").on 'change', setMode | |
| 18 | + | |
| 19 | +    $textarea = $('#user_credential_credential_value').hide() | |
| 20 | + editor.getSession().setValue($textarea.val()) | |
| 21 | + | |
| 22 | +    $textarea.closest('form').on 'submit', -> | |
| 23 | + $textarea.val(editor.getSession().getValue()) | |
| 24 | + | |
| 25 | +$ -> | |
| 26 | + Utils.registerPage(UserCredentialPage, forPathsMatching: /^user_credentials\/\d+/) | 
| @@ -1,29 +0,0 @@ | ||
| 1 | -#= require ace/ace | |
| 2 | -#= require ace/mode-javascript.js | |
| 3 | -#= require ace/mode-markdown.js | |
| 4 | -#= require_self | |
| 5 | - | |
| 6 | -# This is not included in the core application.js bundle. | |
| 7 | - | |
| 8 | -$ -> | |
| 9 | -  editor = ace.edit("ace-credential-value") | |
| 10 | - editor.getSession().setTabSize(2) | |
| 11 | - editor.getSession().setUseSoftTabs(true) | |
| 12 | - editor.getSession().setUseWrapMode(false) | |
| 13 | -  editor.setTheme("ace/theme/chrome") | |
| 14 | - | |
| 15 | - setMode = -> | |
| 16 | -    mode = $("#user_credential_mode").val() | |
| 17 | - if mode == 'java_script' | |
| 18 | -      editor.getSession().setMode("ace/mode/javascript") | |
| 19 | - else | |
| 20 | -      editor.getSession().setMode("ace/mode/text") | |
| 21 | - | |
| 22 | - setMode() | |
| 23 | -  $("#user_credential_mode").on 'change', setMode | |
| 24 | - | |
| 25 | -  $textarea = $('#user_credential_credential_value').hide() | |
| 26 | - editor.getSession().setValue($textarea.val()) | |
| 27 | - | |
| 28 | -  $textarea.closest('form').on 'submit', -> | |
| 29 | - $textarea.val(editor.getSession().getValue()) | 
| @@ -179,12 +179,18 @@ span.not-applicable:after { | ||
| 179 | 179 | cursor: pointer; | 
| 180 | 180 | } | 
| 181 | 181 |  | 
| 182 | -// Credentials | |
| 182 | +// Credentials and Ace Editor | |
| 183 | 183 |  | 
| 184 | 184 |  #ace-credential-value { | 
| 185 | 185 | position: relative; | 
| 186 | 186 | width: 940px; | 
| 187 | - height: 400px; | |
| 187 | + height: 300px; | |
| 188 | +} | |
| 189 | + | |
| 190 | +.ace-editor { | |
| 191 | + position: relative; | |
| 192 | + width: 550px; | |
| 193 | + height: 300px; | |
| 188 | 194 | } | 
| 189 | 195 |  | 
| 190 | 196 | // Disabled | 
| @@ -60,6 +60,7 @@ module DryRunnable | ||
| 60 | 60 | def create_event(event_hash) | 
| 61 | 61 | if can_create_events? | 
| 62 | 62 | @dry_run_results[:events] << event_hash[:payload] | 
| 63 | +        events.build({ user: user, expires_at: new_event_expiration_date }.merge(event_hash)) | |
| 63 | 64 | else | 
| 64 | 65 | error "This Agent cannot create events!" | 
| 65 | 66 | end | 
| @@ -32,7 +32,7 @@ module FormConfigurable | ||
| 32 | 32 | options = args.extract_options!.reverse_merge(roles: [], type: :string) | 
| 33 | 33 |  | 
| 34 | 34 |        if args.all? { |arg| arg.is_a?(Symbol) } | 
| 35 | - options.assert_valid_keys([:type, :roles, :values]) | |
| 35 | + options.assert_valid_keys([:type, :roles, :values, :ace]) | |
| 36 | 36 | end | 
| 37 | 37 |  | 
| 38 | 38 | if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array)) | 
| @@ -86,6 +86,12 @@ module ApplicationHelper | ||
| 86 | 86 |      ].join.html_safe, class: "label label-default label-service service-#{service.provider}" | 
| 87 | 87 | end | 
| 88 | 88 |  | 
| 89 | + def load_ace_editor! | |
| 90 | + unless content_for?(:ace_editor_script) | |
| 91 | +      content_for :ace_editor_script, javascript_include_tag('ace') | |
| 92 | + end | |
| 93 | + end | |
| 94 | + | |
| 89 | 95 | def highlighted?(id) | 
| 90 | 96 | @highlighted_ranges ||= | 
| 91 | 97 | case value = params[:hl].presence | 
| @@ -3,6 +3,10 @@ require 'cgi' | ||
| 3 | 3 |  | 
| 4 | 4 | module Agents | 
| 5 | 5 | class JavaScriptAgent < Agent | 
| 6 | + include FormConfigurable | |
| 7 | + | |
| 8 | + can_dry_run! | |
| 9 | + | |
| 6 | 10 | default_schedule "never" | 
| 7 | 11 |  | 
| 8 | 12 | description <<-MD | 
| @@ -25,6 +29,11 @@ module Agents | ||
| 25 | 29 | * `this.unescapeHtml(htmlToUnescape)` | 
| 26 | 30 | MD | 
| 27 | 31 |  | 
| 32 | + form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript] | |
| 33 | + form_configurable :code, type: :text, ace: true | |
| 34 | + form_configurable :expected_receive_period_in_days | |
| 35 | + form_configurable :expected_update_period_in_days | |
| 36 | + | |
| 28 | 37 | def validate_options | 
| 29 | 38 | cred_name = credential_referenced_by_code | 
| 30 | 39 | if cred_name | 
| @@ -32,6 +41,10 @@ module Agents | ||
| 32 | 41 | else | 
| 33 | 42 | errors.add(:base, "The 'code' option is required") unless options['code'].present? | 
| 34 | 43 | end | 
| 44 | + | |
| 45 | + if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript]) | |
| 46 | + errors.add(:base, "The 'language' must be JavaScript or CoffeeScript") | |
| 47 | + end | |
| 35 | 48 | end | 
| 36 | 49 |  | 
| 37 | 50 | def working? | 
| @@ -69,7 +82,7 @@ module Agents | ||
| 69 | 82 |              this.memory('callCount', callCount + 1); | 
| 70 | 83 | } | 
| 71 | 84 | }; | 
| 72 | - | |
| 85 | + | |
| 73 | 86 |          Agent.receive = function() { | 
| 74 | 87 | var events = this.incomingEvents(); | 
| 75 | 88 |            for(var i = 0; i < events.length; i++) { | 
| @@ -79,9 +92,10 @@ module Agents | ||
| 79 | 92 | JS | 
| 80 | 93 |  | 
| 81 | 94 |        { | 
| 82 | - "code" => js_code.gsub(/[\n\r\t]/, '').strip, | |
| 83 | - 'expected_receive_period_in_days' => "2", | |
| 84 | - 'expected_update_period_in_days' => "2" | |
| 95 | + 'code' => Utils.unindent(js_code), | |
| 96 | + 'language' => 'JavaScript', | |
| 97 | + 'expected_receive_period_in_days' => '2', | |
| 98 | + 'expected_update_period_in_days' => '2' | |
| 85 | 99 | } | 
| 86 | 100 | end | 
| 87 | 101 |  | 
| @@ -107,7 +121,11 @@ module Agents | ||
| 107 | 121 |        context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) } | 
| 108 | 122 |        context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) } | 
| 109 | 123 |  | 
| 110 | - context.eval(code) | |
| 124 | + if (options['language'] || '').downcase == 'coffeescript' | |
| 125 | + context.eval(CoffeeScript.compile code) | |
| 126 | + else | |
| 127 | + context.eval(code) | |
| 128 | + end | |
| 111 | 129 |        context.eval("Agent.#{js_function}();") | 
| 112 | 130 | end | 
| 113 | 131 |  | 
| @@ -121,7 +139,7 @@ module Agents | ||
| 121 | 139 | end | 
| 122 | 140 |  | 
| 123 | 141 | def credential_referenced_by_code | 
| 124 | - interpolated['code'] =~ /\Acredential:(.*)\Z/ && $1 | |
| 142 | + (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1 | |
| 125 | 143 | end | 
| 126 | 144 |  | 
| 127 | 145 | def setup_javascript | 
| @@ -379,7 +379,7 @@ module Agents | ||
| 379 | 379 | end | 
| 380 | 380 | end | 
| 381 | 381 |  | 
| 382 | - # Wraps Faraday::Utilsa::Headers | |
| 382 | + # Wraps Faraday::Utils::Headers | |
| 383 | 383 | class HeaderDrop < LiquidDroppable::Drop | 
| 384 | 384 | def before_method(name) | 
| 385 | 385 |          @object[name.tr('_', '-')] | 
| @@ -20,7 +20,12 @@ class FormConfigurableAgentPresenter < Decorator | ||
| 20 | 20 |  | 
| 21 | 21 | case data[:type] | 
| 22 | 22 | when :text | 
| 23 | -      @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3) | |
| 23 | + @view.content_tag 'div' do | |
| 24 | +        @view.concat @view.text_area_tag("agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3)) | |
| 25 | + if data[:ace].present? | |
| 26 | +          @view.concat @view.content_tag('div', '', class: 'ace-editor', data: { source: "[name='agent[options][#{attribute}]']" }) | |
| 27 | + end | |
| 28 | + end | |
| 24 | 29 | when :boolean | 
| 25 | 30 | @view.content_tag 'div' do | 
| 26 | 31 |          @view.concat(@view.content_tag('label', class: 'radio-inline') do | 
| @@ -1,3 +1,5 @@ | ||
| 1 | +<% load_ace_editor! %> | |
| 2 | + | |
| 1 | 3 | <% if @agent.errors.any? %> | 
| 2 | 4 | <div class="row well model-errors"> | 
| 3 | 5 | <h2><%= pluralize(@agent.errors.count, "error") %> prohibited this Agent from being saved:</h2> | 
| @@ -8,9 +10,10 @@ | ||
| 8 | 10 | <% end %> | 
| 9 | 11 |  | 
| 10 | 12 | <%= form_for(@agent, | 
| 11 | - :as => :agent, | |
| 12 | - :url => @agent.new_record? ? agents_path : agent_path(@agent), | |
| 13 | - :method => @agent.new_record? ? "POST" : "PUT") do |f| %> | |
| 13 | + as: :agent, | |
| 14 | + url: @agent.new_record? ? agents_path : agent_path(@agent), | |
| 15 | + method: @agent.new_record? ? "POST" : "PUT", | |
| 16 | +             html: { class: 'agent-form' }) do |f| %> | |
| 14 | 17 |  | 
| 15 | 18 | <div class="row"> | 
| 16 | 19 | <div class="col-md-6"> | 
| @@ -9,6 +9,7 @@ | ||
| 9 | 9 | <%= stylesheet_link_tag "application", :media => "all" %> | 
| 10 | 10 | <%= javascript_include_tag "application" %> | 
| 11 | 11 | <%= csrf_meta_tags %> | 
| 12 | + <%= yield(:ace_editor_script) %> | |
| 12 | 13 | <%= yield(:head) %> | 
| 13 | 14 | </head> | 
| 14 | 15 | <body> | 
| @@ -1,3 +1,5 @@ | ||
| 1 | +<% load_ace_editor! %> | |
| 2 | + | |
| 1 | 3 | <%= form_for(@user_credential, :method => @user_credential.new_record? ? "POST" : "PUT") do |f| %> | 
| 2 | 4 | <% if @user_credential.errors.any? %> | 
| 3 | 5 | <div class="row well"> | 
| @@ -40,5 +42,3 @@ | ||
| 40 | 42 | </div> | 
| 41 | 43 | </div> | 
| 42 | 44 | <% end %> | 
| 43 | - | |
| 44 | -<%= javascript_include_tag "user_credentials" %> | 
| @@ -2,6 +2,8 @@ require 'thread' | ||
| 2 | 2 | require 'huginn_scheduler' | 
| 3 | 3 | require 'twitter_stream' | 
| 4 | 4 |  | 
| 5 | +Rails.configuration.cache_classes = true | |
| 6 | + | |
| 5 | 7 | STDOUT.sync = true | 
| 6 | 8 | STDERR.sync = true | 
| 7 | 9 |  | 
| @@ -63,7 +63,7 @@ Huginn::Application.configure do | ||
| 63 | 63 | end | 
| 64 | 64 |  | 
| 65 | 65 | # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) | 
| 66 | - config.assets.precompile += %w( diagram.js graphing.js map_marker.js user_credentials.js ) | |
| 66 | + config.assets.precompile += %w( diagram.js graphing.js map_marker.js ace.js ) | |
| 67 | 67 |  | 
| 68 | 68 | # Ignore bad email addresses and do not raise email delivery errors. | 
| 69 | 69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. | 
| @@ -23,6 +23,21 @@ describe Agents::JavaScriptAgent do | ||
| 23 | 23 | expect(@agent).not_to be_valid | 
| 24 | 24 | end | 
| 25 | 25 |  | 
| 26 | + it "checks for a valid 'language', but allows nil" do | |
| 27 | + expect(@agent).to be_valid | |
| 28 | + @agent.options['language'] = '' | |
| 29 | + expect(@agent).to be_valid | |
| 30 | +      @agent.options.delete('language') | |
| 31 | + expect(@agent).to be_valid | |
| 32 | + @agent.options['language'] = 'foo' | |
| 33 | + expect(@agent).not_to be_valid | |
| 34 | + | |
| 35 | + %w[javascript JavaScript coffeescript CoffeeScript].each do |valid_language| | |
| 36 | + @agent.options['language'] = valid_language | |
| 37 | + expect(@agent).to be_valid | |
| 38 | + end | |
| 39 | + end | |
| 40 | + | |
| 26 | 41 | it "accepts a credential, but it must exist" do | 
| 27 | 42 | expect(@agent).to be_valid | 
| 28 | 43 | @agent.options['code'] = 'credential:foo' | 
| @@ -74,11 +89,10 @@ describe Agents::JavaScriptAgent do | ||
| 74 | 89 |        }.to change { Event.count }.by(2) | 
| 75 | 90 | end | 
| 76 | 91 |  | 
| 77 | - | |
| 78 | 92 | describe "using credentials as code" do | 
| 79 | 93 | before do | 
| 80 | 94 |          @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };' | 
| 81 | - @agent.options['code'] = 'credential:code-foo' | |
| 95 | + @agent.options['code'] = "credential:code-foo\n\n" | |
| 82 | 96 | @agent.save! | 
| 83 | 97 | end | 
| 84 | 98 |  | 
| @@ -238,5 +252,17 @@ describe Agents::JavaScriptAgent do | ||
| 238 | 252 |          }.not_to change { Event.count } | 
| 239 | 253 | end | 
| 240 | 254 | end | 
| 255 | + | |
| 256 | + describe "using CoffeeScript" do | |
| 257 | + it "will accept a 'language' of 'CoffeeScript'" do | |
| 258 | +        @agent.options['code'] = 'Agent.check = -> this.log("hello from coffeescript")' | |
| 259 | + @agent.options['language'] = 'CoffeeScript' | |
| 260 | + @agent.save! | |
| 261 | +        expect { | |
| 262 | + @agent.check | |
| 263 | + }.not_to raise_error | |
| 264 | +        expect(AgentLog.last.message).to eq("hello from coffeescript") | |
| 265 | + end | |
| 266 | + end | |
| 241 | 267 | end | 
| 242 | 268 | end |