user_location_agent.rb 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. require 'securerandom'
  2. module Agents
  3. class UserLocationAgent < Agent
  4. cannot_be_scheduled!
  5. gem_dependency_check { defined?(Haversine) }
  6. description do
  7. <<~MD
  8. The User Location Agent creates events based on WebHook POSTS that contain a `latitude` and `longitude`. You can use the [POSTLocation](https://github.com/cantino/post_location) or [PostGPS](https://github.com/chriseidhof/PostGPS) iOS app to post your location to `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options.
  9. #{'## Include `haversine` in your Gemfile to use this Agent!' if dependencies_missing?}
  10. If you want to only keep more precise locations, set `max_accuracy` to the upper bound, in meters. The default name for this field is `accuracy`, but you can change this by setting a value for `accuracy_field`.
  11. If you want to require a certain distance traveled, set `min_distance` to the minimum distance, in meters. Note that GPS readings and the measurement itself aren't exact, so don't rely on this for precision filtering.
  12. To view the locations on a map, set `api_key` to your [Google Maps JavaScript API key](https://developers.google.com/maps/documentation/javascript/get-api-key#key).
  13. MD
  14. end
  15. event_description <<~MD
  16. Assuming you're using the iOS application, events look like this:
  17. {
  18. "latitude": "37.12345",
  19. "longitude": "-122.12345",
  20. "timestamp": "123456789.0",
  21. "altitude": "22.0",
  22. "horizontal_accuracy": "5.0",
  23. "vertical_accuracy": "3.0",
  24. "speed": "0.52595",
  25. "course": "72.0703",
  26. "device_token": "..."
  27. }
  28. MD
  29. def working?
  30. event_created_within?(2) && !recent_error_logs?
  31. end
  32. def default_options
  33. {
  34. 'secret' => SecureRandom.hex(7),
  35. 'max_accuracy' => '',
  36. 'min_distance' => '',
  37. 'api_key' => '',
  38. }
  39. end
  40. def validate_options
  41. errors.add(:base,
  42. "secret is required and must be longer than 4 characters") unless options['secret'].present? && options['secret'].length > 4
  43. end
  44. def receive(incoming_events)
  45. incoming_events.each do |event|
  46. interpolate_with(event) do
  47. handle_payload event.payload
  48. end
  49. end
  50. end
  51. def receive_web_request(params, method, format)
  52. params = params.symbolize_keys
  53. if method != 'post'
  54. return ['Not Found', 404]
  55. end
  56. if interpolated['secret'] != params[:secret]
  57. return ['Not Authorized', 401]
  58. end
  59. handle_payload params.except(:secret)
  60. ['ok', 200]
  61. end
  62. private
  63. def handle_payload(payload)
  64. location = Location.new(payload)
  65. accuracy_field = interpolated[:accuracy_field].presence || "accuracy"
  66. def accurate_enough?(payload, accuracy_field)
  67. !interpolated[:max_accuracy].present? || !payload[accuracy_field] || payload[accuracy_field].to_i < interpolated[:max_accuracy].to_i
  68. end
  69. def far_enough?(payload)
  70. if memory['last_location'].present?
  71. travel = Haversine.distance(
  72. memory['last_location']['latitude'].to_i,
  73. memory['last_location']['longitude'].to_i,
  74. payload['latitude'].to_i,
  75. payload['longitude'].to_i
  76. ).to_meters
  77. !interpolated[:min_distance].present? || travel > interpolated[:min_distance].to_i
  78. else # for the first run, before "last_location" exists
  79. true
  80. end
  81. end
  82. if location.present? && accurate_enough?(payload, accuracy_field) && far_enough?(payload)
  83. if interpolated[:max_accuracy].present? && !payload[accuracy_field].present?
  84. log "Accuracy field missing; all locations will be kept"
  85. end
  86. create_event(payload:, location:)
  87. memory["last_location"] = payload
  88. end
  89. end
  90. end
  91. end