cantino/huginn

View on GitHub
app/models/agents/user_location_agent.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'securerandom'

module Agents
  class UserLocationAgent < Agent
    cannot_be_scheduled!

    gem_dependency_check { defined?(Haversine) }

    description do
      <<~MD
        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.

        #{'## Include `haversine` in your Gemfile to use this Agent!' if dependencies_missing?}

        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.

        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).
      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' => '',
        'api_key' => '',
      }
    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)

      ['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:, location:)
        memory["last_location"] = payload
      end
    end
  end
end