java_script_agent.rb 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. This 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.options()`
  18. * `this.options(key)`
  19. * `this.log(message)`
  20. * `this.error(message)`
  21. * `this.escapeHtml(htmlToEscape)`
  22. * `this.unescapeHtml(htmlToUnescape)`
  23. MD
  24. form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
  25. form_configurable :code, type: :text, ace: true
  26. form_configurable :expected_receive_period_in_days
  27. form_configurable :expected_update_period_in_days
  28. def validate_options
  29. cred_name = credential_referenced_by_code
  30. if cred_name
  31. errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
  32. else
  33. errors.add(:base, "The 'code' option is required") unless options['code'].present?
  34. end
  35. if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
  36. errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
  37. end
  38. end
  39. def working?
  40. return false if recent_error_logs?
  41. if interpolated['expected_update_period_in_days'].present?
  42. return false unless event_created_within?(interpolated['expected_update_period_in_days'])
  43. end
  44. if interpolated['expected_receive_period_in_days'].present?
  45. return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
  46. end
  47. true
  48. end
  49. def check
  50. log_errors do
  51. execute_js("check")
  52. end
  53. end
  54. def receive(incoming_events)
  55. log_errors do
  56. execute_js("receive", incoming_events)
  57. end
  58. end
  59. def default_options
  60. js_code = <<-JS
  61. Agent.check = function() {
  62. if (this.options('make_event')) {
  63. this.createEvent({ 'message': 'I made an event!' });
  64. var callCount = this.memory('callCount') || 0;
  65. this.memory('callCount', callCount + 1);
  66. }
  67. };
  68. Agent.receive = function() {
  69. var events = this.incomingEvents();
  70. for(var i = 0; i < events.length; i++) {
  71. this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
  72. }
  73. }
  74. JS
  75. {
  76. 'code' => Utils.unindent(js_code),
  77. 'language' => 'JavaScript',
  78. 'expected_receive_period_in_days' => '2',
  79. 'expected_update_period_in_days' => '2'
  80. }
  81. end
  82. private
  83. def execute_js(js_function, incoming_events = [])
  84. js_function = js_function == "check" ? "check" : "receive"
  85. context = V8::Context.new
  86. context.eval(setup_javascript)
  87. context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json }
  88. context["getIncomingEvents"] = lambda { |a| incoming_events.to_json }
  89. context["getOptions"] = lambda { |a, x| interpolated.to_json }
  90. context["doLog"] = lambda { |a, x| log x }
  91. context["doError"] = lambda { |a, x| error x }
  92. context["getMemory"] = lambda do |a, x, y|
  93. if x && y
  94. memory[x] = clean_nans(y)
  95. else
  96. memory.to_json
  97. end
  98. end
  99. context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
  100. context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
  101. if (options['language'] || '').downcase == 'coffeescript'
  102. context.eval(CoffeeScript.compile code)
  103. else
  104. context.eval(code)
  105. end
  106. context.eval("Agent.#{js_function}();")
  107. end
  108. def code
  109. cred = credential_referenced_by_code
  110. if cred
  111. credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
  112. else
  113. interpolated['code']
  114. end
  115. end
  116. def credential_referenced_by_code
  117. (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
  118. end
  119. def setup_javascript
  120. <<-JS
  121. function Agent() {};
  122. Agent.createEvent = function(opts) {
  123. return JSON.parse(doCreateEvent(JSON.stringify(opts)));
  124. }
  125. Agent.incomingEvents = function() {
  126. return JSON.parse(getIncomingEvents());
  127. }
  128. Agent.memory = function(key, value) {
  129. if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
  130. getMemory(key, value);
  131. } else if (typeof(key) !== "undefined") {
  132. return JSON.parse(getMemory())[key];
  133. } else {
  134. return JSON.parse(getMemory());
  135. }
  136. }
  137. Agent.options = function(key) {
  138. if (typeof(key) !== "undefined") {
  139. return JSON.parse(getOptions())[key];
  140. } else {
  141. return JSON.parse(getOptions());
  142. }
  143. }
  144. Agent.log = function(message) {
  145. doLog(message);
  146. }
  147. Agent.error = function(message) {
  148. doError(message);
  149. }
  150. Agent.escapeHtml = function(html) {
  151. return escapeHtml(html);
  152. }
  153. Agent.unescapeHtml = function(html) {
  154. return unescapeHtml(html);
  155. }
  156. Agent.check = function(){};
  157. Agent.receive = function(){};
  158. JS
  159. end
  160. def log_errors
  161. begin
  162. yield
  163. rescue V8::Error => e
  164. error "JavaScript error: #{e.message}"
  165. end
  166. end
  167. def clean_nans(input)
  168. if input.is_a?(Array)
  169. input.map {|v| clean_nans(v) }
  170. elsif input.is_a?(Hash)
  171. input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
  172. elsif input.is_a?(Float) && input.nan?
  173. 'NaN'
  174. else
  175. input
  176. end
  177. end
  178. end
  179. end