event_formatting_agent.rb 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. module Agents
  2. class EventFormattingAgent < Agent
  3. cannot_be_scheduled!
  4. description <<-MD
  5. An Event Formatting Agent allows you to format incoming Events, adding new fields as needed.
  6. For example, here is a possible Event:
  7. {
  8. "high": {
  9. "celsius": "18",
  10. "fahreinheit": "64"
  11. },
  12. "date": {
  13. "epoch": "1357959600",
  14. "pretty": "10:00 PM EST on January 11, 2013"
  15. },
  16. "conditions": "Rain showers",
  17. "data": "This is some data"
  18. }
  19. You may want to send this event to another Agent, for example a Twilio Agent, which expects a `message` key.
  20. You can use an Event Formatting Agent's `instructions` setting to do this in the following way:
  21. "instructions": {
  22. "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.",
  23. "subject": "{{data}}",
  24. "created_at": "{{created_at}}"
  25. }
  26. Names here like `conditions`, `high` and `data` refer to the corresponding values in the Event hash.
  27. The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`.
  28. The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}.
  29. Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
  30. Events generated by this possible Event Formatting Agent will look like:
  31. {
  32. "message": "Today's conditions look like Rain showers with a high temperature of 18 degrees Celsius.",
  33. "subject": "This is some data"
  34. }
  35. In `matchers` setting you can perform regular expression matching against contents of events and expand the match data for use in `instructions` setting. Here is an example:
  36. {
  37. "matchers": [
  38. {
  39. "path": "{{date.pretty}}",
  40. "regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
  41. "to": "pretty_date",
  42. }
  43. ]
  44. }
  45. This virtually merges the following hash into the original event hash:
  46. "pretty_date": {
  47. "time": "10:00 PM EST",
  48. "0": "10:00 PM EST on January 11, 2013"
  49. "1": "10:00 PM EST",
  50. }
  51. So you can use it in `instructions` like this:
  52. "instructions": {
  53. "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.",
  54. "subject": "{{data}}"
  55. }
  56. If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
  57. To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so:
  58. {
  59. "message": "A peak was on Twitter in {{group_by}}. Search: https://twitter.com/search?q={{group_by | uri_escape}}"
  60. }
  61. MD
  62. event_description "User defined"
  63. after_save :clear_matchers
  64. def validate_options
  65. errors.add(:base, "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present?
  66. validate_matchers
  67. end
  68. def default_options
  69. {
  70. 'instructions' => {
  71. 'message' => "You received a text {{text}} from {{fields.from}}",
  72. 'agent' => "{{agent.type}}",
  73. 'some_other_field' => "Looks like the weather is going to be {{fields.weather}}",
  74. 'created_at' => "{{created_at}}"
  75. },
  76. 'matchers' => [],
  77. 'mode' => "clean",
  78. }
  79. end
  80. def working?
  81. !recent_error_logs?
  82. end
  83. def receive(incoming_events)
  84. incoming_events.each do |event|
  85. payload = perform_matching(event.payload)
  86. opts = interpolated(event.to_liquid(payload))
  87. formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {}
  88. formatted_event.merge! opts['instructions']
  89. create_event :payload => formatted_event
  90. end
  91. end
  92. private
  93. def validate_matchers
  94. matchers = options['matchers'] or return
  95. unless matchers.is_a?(Array)
  96. errors.add(:base, "matchers must be an array if present")
  97. return
  98. end
  99. matchers.each do |matcher|
  100. unless matcher.is_a?(Hash)
  101. errors.add(:base, "each matcher must be a hash")
  102. next
  103. end
  104. regexp, path, to = matcher.values_at(*%w[regexp path to])
  105. if regexp.present?
  106. begin
  107. Regexp.new(regexp)
  108. rescue
  109. errors.add(:base, "bad regexp found in matchers: #{regexp}")
  110. end
  111. else
  112. errors.add(:base, "regexp is mandatory for a matcher and must be a string")
  113. end
  114. errors.add(:base, "path is mandatory for a matcher and must be a string") if !path.present?
  115. errors.add(:base, "to must be a string if present in a matcher") if to.present? && !to.is_a?(String)
  116. end
  117. end
  118. def perform_matching(payload)
  119. matchers.inject(payload.dup) { |hash, matcher|
  120. matcher[hash]
  121. }
  122. end
  123. def matchers
  124. @matchers ||=
  125. if matchers = options['matchers']
  126. matchers.map { |matcher|
  127. regexp, path, to = matcher.values_at(*%w[regexp path to])
  128. re = Regexp.new(regexp)
  129. proc { |hash|
  130. mhash = {}
  131. value = interpolate_string(path, hash)
  132. if value.is_a?(String) && (m = re.match(value))
  133. m.to_a.each_with_index { |s, i|
  134. mhash[i.to_s] = s
  135. }
  136. m.names.each do |name|
  137. mhash[name] = m[name]
  138. end if m.respond_to?(:names)
  139. end
  140. if to
  141. case value = hash[to]
  142. when Hash
  143. value.update(mhash)
  144. else
  145. hash[to] = mhash
  146. end
  147. else
  148. hash.update(mhash)
  149. end
  150. hash
  151. }
  152. }
  153. else
  154. []
  155. end
  156. end
  157. def clear_matchers
  158. @matchers = nil
  159. end
  160. end
  161. end