java_script_agent.rb 6.9KB

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