java_script_agent.rb 7.2KB

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