liquid_output_agent.rb 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. module Agents
  2. class LiquidOutputAgent < Agent
  3. include WebRequestConcern
  4. include FormConfigurable
  5. cannot_be_scheduled!
  6. cannot_create_events!
  7. DATE_UNITS = %w[second seconds minute minutes hour hours day days week weeks month months year years]
  8. description do
  9. <<-MD
  10. The Liquid Output Agent outputs events through a Liquid template you provide. Use it to create a HTML page, or a json feed, or anything else that can be rendered as a string from your stream of Huginn data.
  11. This Agent will output data at:
  12. `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id: user_id, secret: ':secret', format: :any_extension)}`
  13. where `:secret` is the secret specified in your options. You can use any extension you wish.
  14. Options:
  15. * `secret` - A token that the requestor must provide for light-weight authentication.
  16. * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
  17. * `content` - The content to display when someone requests this page.
  18. * `mime_type` - The mime type to use when someone requests this page.
  19. * `mode` - The behavior that determines what data is passed to the Liquid template.
  20. * `event_limit` - A limit applied to the events passed to a template when in "Last X events" mode. Can be a count like "1", or an amount of time like "1 day" or "5 minutes".
  21. # Liquid Templating
  22. The content you provide will be run as a Liquid template. The data from the last event received will be used when processing the Liquid template.
  23. To learn more about Liquid templates, go here: [http://liquidmarkup.org](http://liquidmarkup.org "Liquid Templating")
  24. # Modes
  25. ### Merge events
  26. The data for incoming events will be merged. So if two events come in like this:
  27. ```
  28. { 'a' => 'b', 'c' => 'd'}
  29. { 'a' => 'bb', 'e' => 'f'}
  30. ```
  31. The final result will be:
  32. ```
  33. { 'a' => 'bb', 'c' => 'd', 'e' => 'f'}
  34. ```
  35. This merged version will be passed to the Liquid template.
  36. ### Last event in
  37. The data from the last event will be passed to the template.
  38. ### Last X events
  39. All of the events received by this agent will be passed to the template
  40. as the ```events``` array.
  41. The number of events can be controlled via the ```event_limit``` option.
  42. If ```event_limit``` is an integer X, the last X events will be passed
  43. to the template. If ```event_limit``` is an integer with a unit of
  44. measure like "1 day" or "5 minutes" or "9 years", a date filter will
  45. be applied to the events passed to the template. If no ```event_limit```
  46. is provided, then all of the events for the agent will be passed to
  47. the template.
  48. For performance, the maximum ```event_limit``` allowed is 1000.
  49. MD
  50. end
  51. def default_options
  52. content = <<EOF
  53. When you use the "Last event in" or "Merge events" option, you can use variables from the last event received, like this:
  54. Name: {{name}}
  55. Url: {{url}}
  56. If you use the "Last X Events" mode, a set of events will be passed to your Liquid template. You can use them like this:
  57. <table class="table">
  58. {% for event in events %}
  59. <tr>
  60. <td>{{ event.title }}</td>
  61. <td><a href="{{ event.url }}">Click here to see</a></td>
  62. </tr>
  63. {% endfor %}
  64. </table>
  65. EOF
  66. {
  67. "secret" => "a-secret-key",
  68. "expected_receive_period_in_days" => 2,
  69. "mime_type" => 'text/html',
  70. "mode" => 'Last event in',
  71. "event_limit" => '',
  72. "content" => content,
  73. }
  74. end
  75. form_configurable :secret
  76. form_configurable :expected_receive_period_in_days
  77. form_configurable :content, type: :text
  78. form_configurable :mime_type
  79. form_configurable :mode, type: :array, values: [ 'Last event in', 'Merge events', 'Last X events']
  80. form_configurable :event_limit
  81. def working?
  82. last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
  83. end
  84. def validate_options
  85. if options['secret'].present?
  86. case options['secret']
  87. when %r{[/.]}
  88. errors.add(:base, "secret may not contain a slash or dot")
  89. when String
  90. else
  91. errors.add(:base, "secret must be a string")
  92. end
  93. else
  94. errors.add(:base, "Please specify one secret for 'authenticating' incoming feed requests")
  95. end
  96. unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
  97. errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
  98. end
  99. if options['event_limit'].present?
  100. if((Integer(options['event_limit']) rescue false) == false)
  101. errors.add(:base, "Event limit must be an integer that is less than 1001.")
  102. elsif (options['event_limit'].to_i > 1000)
  103. errors.add(:base, "For performance reasons, you cannot have an event limit greater than 1000.")
  104. end
  105. else
  106. end
  107. end
  108. def receive(incoming_events)
  109. return unless ['merge events', 'last event in'].include?(mode)
  110. memory['last_event'] ||= {}
  111. incoming_events.each do |event|
  112. case mode
  113. when 'merge events'
  114. memory['last_event'] = memory['last_event'].merge(event.payload)
  115. else
  116. memory['last_event'] = event.payload
  117. end
  118. end
  119. end
  120. def receive_web_request(params, method, format)
  121. valid_authentication?(params) ? [liquified_content, 200, mime_type]
  122. : [unauthorized_content(format), 401]
  123. end
  124. private
  125. def mode
  126. options['mode'].to_s.downcase
  127. end
  128. def unauthorized_content(format)
  129. format =~ /json/ ? { error: "Not Authorized" }
  130. : "Not Authorized"
  131. end
  132. def valid_authentication?(params)
  133. interpolated['secret'] == params['secret']
  134. end
  135. def mime_type
  136. options['mime_type'].presence || 'text/html'
  137. end
  138. def liquified_content
  139. template = Liquid::Template.parse(options['content'] || "")
  140. template.render(data_for_liquid_template)
  141. end
  142. def data_for_liquid_template
  143. case mode
  144. when 'last x events'
  145. events = received_events
  146. events = events.where('events.created_at > ?', date_limit) if date_limit
  147. events = events.limit count_limit
  148. events = events.to_a.map { |x| x.payload }
  149. { 'events' => events }
  150. else
  151. memory['last_event'] || {}
  152. end
  153. end
  154. def count_limit
  155. limit = Integer(options['event_limit']) rescue 1000
  156. limit <= 1000 ? limit : 1000
  157. end
  158. def date_limit
  159. return nil unless options['event_limit'].to_s.include?(' ')
  160. value, unit = options['event_limit'].split(' ')
  161. value = Integer(value) rescue nil
  162. return nil unless value
  163. unit = unit.to_s.downcase
  164. return nil unless DATE_UNITS.include?(unit)
  165. value.send(unit.to_sym).ago
  166. end
  167. end
  168. end