java_script_agent.rb 5.7KB

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