require 'securerandom' module Agents class UserLocationAgent < Agent cannot_be_scheduled! gem_dependency_check { defined?(Haversine) } description do <<-MD #{'## Include `haversine` in your Gemfile to use this Agent!' if dependencies_missing?} The UserLocationAgent 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. Your POST path will be `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options. 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`. 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. MD end event_description <<-MD Assuming you're using the iOS application, events look like this: { "latitude": "37.12345", "longitude": "-122.12345", "timestamp": "123456789.0", "altitude": "22.0", "horizontal_accuracy": "5.0", "vertical_accuracy": "3.0", "speed": "0.52595", "course": "72.0703", "device_token": "..." } MD def working? event_created_within?(2) && !recent_error_logs? end def default_options { 'secret' => SecureRandom.hex(7), 'max_accuracy' => '', 'min_distance' => '', } end def validate_options errors.add(:base, "secret is required and must be longer than 4 characters") unless options['secret'].present? && options['secret'].length > 4 end def receive(incoming_events) incoming_events.each do |event| interpolate_with(event) do handle_payload event.payload end end end def receive_web_request(params, method, format) params = params.symbolize_keys if method != 'post' return ['Not Found', 404] end if interpolated['secret'] != params[:secret] return ['Not Authorized', 401] end handle_payload params.except(:secret) return ['ok', 200] end private def handle_payload(payload) location = Location.new(payload) accuracy_field = interpolated[:accuracy_field].presence || "accuracy" def accurate_enough?(payload, accuracy_field) !interpolated[:max_accuracy].present? || !payload[accuracy_field] || payload[accuracy_field].to_i < interpolated[:max_accuracy].to_i end def far_enough?(payload) if memory['last_location'].present? travel = Haversine.distance(memory['last_location']['latitude'].to_i, memory['last_location']['longitude'].to_i, payload['latitude'].to_i, payload['longitude'].to_i).to_meters !interpolated[:min_distance].present? || travel > interpolated[:min_distance].to_i else # for the first run, before "last_location" exists true end end if location.present? && accurate_enough?(payload, accuracy_field) && far_enough?(payload) if interpolated[:max_accuracy].present? && !payload[accuracy_field].present? log "Accuracy field missing; all locations will be kept" end create_event payload: payload, location: location memory["last_location"] = payload end end end end