java_script_agent.rb 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. require 'date'
  2. require 'cgi'
  3. module Agents
  4. class JavaScriptAgent < Agent
  5. include FormConfigurable
  6. can_dry_run!
  7. default_schedule "never"
  8. description <<-MD
  9. 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!
  10. You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
  11. You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent in the JavaScript environment:
  12. * `this.createEvent(payload)`
  13. * `this.incomingEvents()` (the returned event objects will each have a `payload` property)
  14. * `this.memory()`
  15. * `this.memory(key)`
  16. * `this.memory(keyToSet, valueToSet)`
  17. * `this.setMemory(object)` (replaces the Agent's memory with the provided object)
  18. * `this.deleteKey(key)` (deletes a key from memory and returns the value)
  19. * `this.credential(name)`
  20. * `this.credential(name, valueToSet)`
  21. * `this.options()`
  22. * `this.options(key)`
  23. * `this.log(message)`
  24. * `this.error(message)`
  25. * `this.escapeHtml(htmlToEscape)`
  26. * `this.unescapeHtml(htmlToUnescape)`
  27. MD
  28. form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
  29. form_configurable :code, type: :text, ace: true
  30. form_configurable :expected_receive_period_in_days
  31. form_configurable :expected_update_period_in_days
  32. def validate_options
  33. cred_name = credential_referenced_by_code
  34. if cred_name
  35. errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
  36. else
  37. errors.add(:base, "The 'code' option is required") unless options['code'].present?
  38. end
  39. if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
  40. errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
  41. end
  42. end
  43. def working?
  44. return false if recent_error_logs?
  45. if interpolated['expected_update_period_in_days'].present?
  46. return false unless event_created_within?(interpolated['expected_update_period_in_days'])
  47. end
  48. if interpolated['expected_receive_period_in_days'].present?
  49. return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
  50. end
  51. true
  52. end
  53. def check
  54. log_errors do
  55. execute_js("check")
  56. end
  57. end
  58. def receive(incoming_events)
  59. log_errors do
  60. execute_js("receive", incoming_events)
  61. end
  62. end
  63. def default_options
  64. js_code = <<-JS
  65. Agent.check = function() {
  66. if (this.options('make_event')) {
  67. this.createEvent({ 'message': 'I made an event!' });
  68. var callCount = this.memory('callCount') || 0;
  69. this.memory('callCount', callCount + 1);
  70. }
  71. };
  72. Agent.receive = function() {
  73. var events = this.incomingEvents();
  74. for(var i = 0; i < events.length; i++) {
  75. this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
  76. }
  77. }
  78. JS
  79. {
  80. 'code' => Utils.unindent(js_code),
  81. 'language' => 'JavaScript',
  82. 'expected_receive_period_in_days' => '2',
  83. 'expected_update_period_in_days' => '2'
  84. }
  85. end
  86. private
  87. def execute_js(js_function, incoming_events = [])
  88. js_function = js_function == "check" ? "check" : "receive"
  89. context = V8::Context.new
  90. context.eval(setup_javascript)
  91. context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json }
  92. context["getIncomingEvents"] = lambda { |a| incoming_events.to_json }
  93. context["getOptions"] = lambda { |a, x| interpolated.to_json }
  94. context["doLog"] = lambda { |a, x| log x }
  95. context["doError"] = lambda { |a, x| error x }
  96. context["getMemory"] = lambda { |a| memory.to_json }
  97. context["setMemoryKey"] = lambda do |a, x, y|
  98. memory[x] = clean_nans(y)
  99. end
  100. context["setMemory"] = lambda do |a, x|
  101. memory.replace(clean_nans(x))
  102. end
  103. context["deleteKey"] = lambda { |a, x| memory.delete(x).to_json }
  104. context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
  105. context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
  106. context['getCredential'] = lambda { |a, k| credential(k); }
  107. context['setCredential'] = lambda { |a, k, v| set_credential(k, v) }
  108. if (options['language'] || '').downcase == 'coffeescript'
  109. context.eval(CoffeeScript.compile code)
  110. else
  111. context.eval(code)
  112. end
  113. context.eval("Agent.#{js_function}();")
  114. end
  115. def code
  116. cred = credential_referenced_by_code
  117. if cred
  118. credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
  119. else
  120. interpolated['code']
  121. end
  122. end
  123. def credential_referenced_by_code
  124. (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
  125. end
  126. def set_credential(name, value)
  127. c = user.user_credentials.find_or_initialize_by(credential_name: name)
  128. c.credential_value = value
  129. c.save!
  130. end
  131. def setup_javascript
  132. <<-JS
  133. function Agent() {};
  134. Agent.createEvent = function(opts) {
  135. return JSON.parse(doCreateEvent(JSON.stringify(opts)));
  136. }
  137. Agent.incomingEvents = function() {
  138. return JSON.parse(getIncomingEvents());
  139. }
  140. Agent.memory = function(key, value) {
  141. if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
  142. setMemoryKey(key, value);
  143. } else if (typeof(key) !== "undefined") {
  144. return JSON.parse(getMemory())[key];
  145. } else {
  146. return JSON.parse(getMemory());
  147. }
  148. }
  149. Agent.setMemory = function(obj) {
  150. setMemory(obj);
  151. }
  152. Agent.credential = function(name, value) {
  153. if (typeof(value) !== "undefined") {
  154. setCredential(name, value);
  155. } else {
  156. return getCredential(name);
  157. }
  158. }
  159. Agent.options = function(key) {
  160. if (typeof(key) !== "undefined") {
  161. return JSON.parse(getOptions())[key];
  162. } else {
  163. return JSON.parse(getOptions());
  164. }
  165. }
  166. Agent.log = function(message) {
  167. doLog(message);
  168. }
  169. Agent.error = function(message) {
  170. doError(message);
  171. }
  172. Agent.deleteKey = function(key) {
  173. return JSON.parse(deleteKey(key));
  174. }
  175. Agent.escapeHtml = function(html) {
  176. return escapeHtml(html);
  177. }
  178. Agent.unescapeHtml = function(html) {
  179. return unescapeHtml(html);
  180. }
  181. Agent.check = function(){};
  182. Agent.receive = function(){};
  183. JS
  184. end
  185. def log_errors
  186. begin
  187. yield
  188. rescue V8::Error => e
  189. error "JavaScript error: #{e.message}"
  190. end
  191. end
  192. def clean_nans(input)
  193. if input.is_a?(V8::Array)
  194. input.map {|v| clean_nans(v) }
  195. elsif input.is_a?(V8::Object)
  196. input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
  197. elsif input.is_a?(Float) && input.nan?
  198. 'NaN'
  199. else
  200. input
  201. end
  202. end
  203. end
  204. end