123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- require 'date'
- require 'cgi'
- module Agents
- class JavaScriptAgent < Agent
- include FormConfigurable
- can_dry_run!
- default_schedule "never"
- gem_dependency_check { defined?(MiniRacer) }
- 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?}
- You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
- You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent in the JavaScript environment:
- * `this.createEvent(payload)`
- * `this.incomingEvents()` (the returned event objects will each have a `payload` property)
- * `this.memory()`
- * `this.memory(key)`
- * `this.memory(keyToSet, valueToSet)`
- * `this.setMemory(object)` (replaces the Agent's memory with the provided object)
- * `this.deleteKey(key)` (deletes a key from memory and returns the value)
- * `this.credential(name)`
- * `this.credential(name, valueToSet)`
- * `this.options()`
- * `this.options(key)`
- * `this.log(message)`
- * `this.error(message)`
- * `this.kvs` (whose properties are variables provided by KeyValueStoreAgents)
- * `this.escapeHtml(htmlToEscape)`
- * `this.unescapeHtml(htmlToUnescape)`
- MD
- form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
- form_configurable :code, type: :text, ace: true
- form_configurable :expected_receive_period_in_days
- form_configurable :expected_update_period_in_days
- 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?
- else
- errors.add(:base, "The 'code' option is required") unless options['code'].present?
- end
- if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
- errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
- end
- end
- def working?
- return false if recent_error_logs?
- if interpolated['expected_update_period_in_days'].present?
- return false unless event_created_within?(interpolated['expected_update_period_in_days'])
- end
- 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
- true
- end
- def check
- log_errors do
- execute_js("check")
- end
- end
- def receive(incoming_events)
- log_errors do
- execute_js("receive", incoming_events)
- end
- end
- def default_options
- js_code = <<-JS
- Agent.check = function() {
- if (this.options('make_event')) {
- this.createEvent({ 'message': 'I made an event!' });
- var callCount = this.memory('callCount') || 0;
- this.memory('callCount', callCount + 1);
- }
- };
- Agent.receive = function() {
- var events = this.incomingEvents();
- for(var i = 0; i < events.length; i++) {
- this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
- }
- }
- JS
- {
- 'code' => Utils.unindent(js_code),
- 'language' => 'JavaScript',
- 'expected_receive_period_in_days' => '2',
- 'expected_update_period_in_days' => '2'
- }
- end
- private
- def execute_js(js_function, incoming_events = [])
- js_function = js_function == "check" ? "check" : "receive"
- 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("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("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) })
- kvs = Agents::KeyValueStoreAgent.merge(controllers).find_each.to_h { |kvs|
- [kvs.options[:variable], kvs.memory.as_json]
- }
- context.attach("getKeyValueStores", -> { kvs })
- context.eval("Object.defineProperty(Agent, 'kvs', { get: getKeyValueStores })")
- if (options['language'] || '').downcase == 'coffeescript'
- context.eval(CoffeeScript.compile(code))
- else
- context.eval(code)
- end
- context.eval("Agent.#{js_function}();")
- end
- def code
- cred = credential_referenced_by_code
- if cred
- credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
- else
- interpolated['code']
- end
- end
- def credential_referenced_by_code
- (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
- end
- def set_credential(name, value)
- c = user.user_credentials.find_or_initialize_by(credential_name: name)
- c.credential_value = value
- c.save!
- end
- def setup_javascript
- <<-JS
- function Agent() {};
- Agent.createEvent = function(opts) {
- return JSON.parse(doCreateEvent(JSON.stringify(opts)));
- }
- Agent.incomingEvents = function() {
- return JSON.parse(getIncomingEvents());
- }
- Agent.memory = function(key, value) {
- if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
- setMemoryKey(key, value);
- } else if (typeof(key) !== "undefined") {
- return JSON.parse(getMemory())[key];
- } else {
- return JSON.parse(getMemory());
- }
- }
- Agent.setMemory = function(obj) {
- setMemory(obj);
- }
- Agent.credential = function(name, value) {
- if (typeof(value) !== "undefined") {
- setCredential(name, value);
- } else {
- return getCredential(name);
- }
- }
- Agent.options = function(key) {
- if (typeof(key) !== "undefined") {
- return JSON.parse(getOptions())[key];
- } else {
- return JSON.parse(getOptions());
- }
- }
- Agent.log = function(message) {
- doLog(message);
- }
- Agent.error = function(message) {
- doError(message);
- }
- Agent.deleteKey = function(key) {
- return JSON.parse(deleteKey(key));
- }
- Agent.escapeHtml = function(html) {
- return escapeHtml(html);
- }
- Agent.unescapeHtml = function(html) {
- return unescapeHtml(html);
- }
- Agent.check = function(){};
- Agent.receive = function(){};
- JS
- end
- def log_errors
- yield
- rescue MiniRacer::Error => e
- error "JavaScript error: #{e.message}"
- end
- def clean_nans(input)
- 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
- end
- end
- end
|