java_script_agent.rb 7.8 KB

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