trigger_agent.rb 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. module Agents
  2. class TriggerAgent < Agent
  3. cannot_be_scheduled!
  4. VALID_COMPARISON_TYPES = %w[regex !regex field<value field<=value field==value field!=value field>=value field>value]
  5. description <<-MD
  6. The Trigger Agent will watch for a specific value in an Event payload.
  7. The `rules` array contains hashes of `path`, `value`, and `type`. The `path` value is a dotted path through a hash in [JSONPaths](http://goessner.net/articles/JsonPath/) syntax.
  8. The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`. Note that regex patterns are matched case insensitively. If you want case sensitive matching, prefix your pattern with `(?-i)`.
  9. The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches.
  10. By default, all rules must match for the Agent to trigger. You can switch this so that only one rule must match by
  11. setting `must_match` to `1`.
  12. The resulting Event will have a payload message of `message`. You can use liquid templating in the `message, have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) for details.
  13. Set `keep_event` to `true` if you'd like to re-emit the incoming event, optionally merged with 'message' when provided.
  14. Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent.
  15. MD
  16. event_description <<-MD
  17. Events look like this:
  18. { "message": "Your message" }
  19. MD
  20. def validate_options
  21. unless options['expected_receive_period_in_days'].present? && options['rules'].present? &&
  22. options['rules'].all? { |rule| rule['type'].present? && VALID_COMPARISON_TYPES.include?(rule['type']) && rule['value'].present? && rule['path'].present? }
  23. errors.add(:base, "expected_receive_period_in_days, message, and rules, with a type, value, and path for every rule, are required")
  24. end
  25. errors.add(:base, "message is required unless 'keep_event' is 'true'") unless options['message'].present? || keep_event?
  26. errors.add(:base, "keep_event, when present, must be 'true' or 'false'") unless options['keep_event'].blank? || %w[true false].include?(options['keep_event'])
  27. if options['must_match'].present?
  28. if options['must_match'].to_i < 1
  29. errors.add(:base, "If used, the 'must_match' option must be a positive integer")
  30. elsif options['must_match'].to_i > options['rules'].length
  31. errors.add(:base, "If used, the 'must_match' option must be equal to or less than the number of rules")
  32. end
  33. end
  34. end
  35. def default_options
  36. {
  37. 'expected_receive_period_in_days' => "2",
  38. 'keep_event' => 'false',
  39. 'rules' => [{
  40. 'type' => "regex",
  41. 'value' => "foo\\d+bar",
  42. 'path' => "topkey.subkey.subkey.goal",
  43. }],
  44. 'message' => "Looks like your pattern matched in '{{value}}'!"
  45. }
  46. end
  47. def working?
  48. last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
  49. end
  50. def receive(incoming_events)
  51. incoming_events.each do |event|
  52. opts = interpolated(event)
  53. match_results = opts['rules'].map do |rule|
  54. value_at_path = Utils.value_at(event['payload'], rule['path'])
  55. rule_values = rule['value']
  56. rule_values = [rule_values] unless rule_values.is_a?(Array)
  57. rule_values.any? do |rule_value|
  58. case rule['type']
  59. when "regex"
  60. value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE)
  61. when "!regex"
  62. value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE)
  63. when "field>value"
  64. value_at_path.to_f > rule_value.to_f
  65. when "field>=value"
  66. value_at_path.to_f >= rule_value.to_f
  67. when "field<value"
  68. value_at_path.to_f < rule_value.to_f
  69. when "field<=value"
  70. value_at_path.to_f <= rule_value.to_f
  71. when "field==value"
  72. value_at_path.to_s == rule_value.to_s
  73. when "field!=value"
  74. value_at_path.to_s != rule_value.to_s
  75. else
  76. raise "Invalid type of #{rule['type']} in TriggerAgent##{id}"
  77. end
  78. end
  79. end
  80. if matches?(match_results)
  81. if keep_event?
  82. payload = event.payload.dup
  83. payload['message'] = opts['message'] if opts['message'].present?
  84. else
  85. payload = { 'message' => opts['message'] }
  86. end
  87. create_event :payload => payload
  88. end
  89. end
  90. end
  91. def matches?(matches)
  92. if options['must_match'].present?
  93. matches.select { |match| match }.length >= options['must_match'].to_i
  94. else
  95. matches.all?
  96. end
  97. end
  98. def keep_event?
  99. boolify(interpolated['keep_event'])
  100. end
  101. end
  102. end