|
require 'date'
require 'cgi'
module Agents
class JavaScriptAgent < Agent
include FormConfigurable
can_dry_run!
default_schedule "never"
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!
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.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 = V8::Context.new
context.eval(setup_javascript)
context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json }
context["getIncomingEvents"] = lambda { |a| incoming_events.to_json }
context["getOptions"] = lambda { |a, x| interpolated.to_json }
context["doLog"] = lambda { |a, x| log x }
context["doError"] = lambda { |a, x| error x }
context["getMemory"] = lambda { |a| memory.to_json }
context["setMemoryKey"] = lambda do |a, x, y|
memory[x] = clean_nans(y)
end
context["setMemory"] = lambda do |a, x|
memory.replace(clean_nans(x))
end
context["deleteKey"] = lambda { |a, x| memory.delete(x).to_json }
context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
context['getCredential'] = lambda { |a, k| credential(k); }
context['setCredential'] = lambda { |a, k, v| set_credential(k, v) }
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
begin
yield
rescue V8::Error => e
error "JavaScript error: #{e.message}"
end
end
def clean_nans(input)
if input.is_a?(V8::Array)
input.map {|v| clean_nans(v) }
elsif input.is_a?(V8::Object)
input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
elsif input.is_a?(Float) && input.nan?
'NaN'
else
input
end
end
end
end
|