Ivan Buiko преди 9 години
родител
ревизия
534e8ab348
променени са 2 файла, в които са добавени 275 реда и са изтрити 0 реда
  1. 130 0
      app/models/agents/beeper_agent.rb
  2. 145 0
      spec/models/agents/beeper_agent_spec.rb

+ 130 - 0
app/models/agents/beeper_agent.rb

@@ -0,0 +1,130 @@
+module Agents
+  class BeeperAgent < Agent
+    cannot_be_scheduled!
+    cannot_create_events!
+
+    description <<-MD
+      Beeper agent sends messages to Beeper app on your mobile device via Push notifications.
+
+      You need a Beeper Application ID (`app_id`), Beeper REST API Key (`api_key`) and Beeper Sender ID (`sender_id`) [https://beeper.io](https://beeper.io)
+
+      You have to provide phone number (`phone`) of the recipient which have a mobile device with Beeper installed, or a `group_id` – Beeper Group ID
+
+      Also you have to provide a message `type` which has to be `message`, `image`, `event`, `location` or `task`.
+
+      Depending on message type you have to provide additional fields:
+
+      ##### Message
+      * `text` – **required**
+
+      ##### Image
+      * `image` – **required** (Image URL or Base64-encoded image)
+      * `text` – optional
+
+      ##### Event
+      * `text` – **required**
+      * `start_time` – **required** (Corresponding to ISO 8601)
+      * `end_time` – optional (Corresponding to ISO 8601)
+
+      ##### Location
+      * `latitude` – **required**
+      * `longitude` – **required**
+      * `text` – optional
+
+      ##### Task
+      * `text` – **required**
+
+      You can see additional documentation at [Beeper website](https://beeper.io/docs)
+    MD
+
+    BASE_URL = 'https://api.beeper.io/api'
+
+    TYPE_ATTRIBUTES = {
+      'message'  => %w(text),
+      'image'    => %w(text image),
+      'event'    => %w(text start_time end_time),
+      'location' => %w(text latitude longitude),
+      'task'     => %w(text)
+    }
+
+    MESSAGE_TYPES = TYPE_ATTRIBUTES.keys
+
+    TYPE_REQUIRED_ATTRIBUTES = {
+      'message'  => %w(text),
+      'image'    => %w(image),
+      'event'    => %w(text start_time),
+      'location' => %w(latitude longitude),
+      'task'     => %w(text)
+    }
+
+    def default_options
+      {
+        'type'      => 'message',
+        'app_id'    => '',
+        'api_key'   => '',
+        'sender_id' => '',
+        'phone'     => '',
+        'text'      => '{{title}}'
+      }
+    end
+
+    def validate_options
+      %w(app_id api_key sender_id type).each do |attr|
+        errors.add(:base, "you need to specify a #{attr}") if options[attr].blank?
+      end
+
+      if options['type'].in?(MESSAGE_TYPES)
+        required_attributes = TYPE_REQUIRED_ATTRIBUTES[options['type']]
+        if required_attributes.any? { |attr| options[attr].blank? }
+          errors.add(:base, "you need to specify a #{required_attributes.join(', ')}")
+        end
+      else
+        errors.add(:base, 'you need to specify a valid message type')
+      end
+
+      unless options['group_id'].blank? ^ options['phone'].blank?
+        errors.add(:base, 'you need to specify a phone or group_id')
+      end
+    end
+
+    def working?
+      received_event_without_error? && !recent_error_logs?
+    end
+
+    def receive(incoming_events)
+      incoming_events.each do |event|
+        send_message(event)
+      end
+    end
+
+    def send_message(event)
+      mo = interpolated(event)
+      begin
+        response = HTTParty.post(endpoint_for(mo['type']), body: payload_for(mo), headers: headers)
+        response.code == 201 ? log(response.body) : error(response.body)
+      rescue HTTParty::Error => e
+        error(e.message)
+      end
+    end
+
+    private
+
+    def headers
+      {
+        'X-Beeper-Application-Id' => options['app_id'],
+        'X-Beeper-REST-API-Key'   => options['api_key'],
+        'Content-Type' => 'application/json'
+      }
+    end
+
+    def payload_for(mo)
+      payload = mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json
+      log(payload)
+      payload
+    end
+
+    def endpoint_for(type)
+      "#{BASE_URL}/#{type}s.json"
+    end
+  end
+end

+ 145 - 0
spec/models/agents/beeper_agent_spec.rb

@@ -0,0 +1,145 @@
+require 'spec_helper'
+
+
+describe Agents::BeeperAgent do
+  let(:base_params) {
+    {
+      'type'      => 'message',
+      'app_id'    => 'some-app-id',
+      'api_key'   => 'some-api-key',
+      'sender_id' => 'sender-id',
+      'phone'     => '+111111111111',
+      'text'      => 'Some text'
+    }
+  }
+
+  subject {
+    agent = described_class.new(name: 'beeper-agent', options: base_params)
+    agent.user = users(:jane)
+    agent.save! and return agent
+  }
+
+  context 'validation' do
+    it 'valid' do
+      expect(subject).to be_valid
+    end
+
+    [:type, :app_id, :api_key, :sender_id].each do |attr|
+      it "invalid without #{attr}" do
+        subject.options[attr] = nil
+        expect(subject).not_to be_valid
+      end
+    end
+
+    it 'invalid with group_id and phone' do
+      subject.options['group_id'] ='some-group-id'
+      expect(subject).not_to be_valid
+    end
+
+    context '#message' do
+      it 'requires text' do
+        subject.options[:text] = nil
+        expect(subject).not_to be_valid
+      end
+    end
+
+    context '#image' do
+      before(:each) do
+        subject.options[:type] = 'image'
+      end
+
+      it 'invalid without image' do
+        expect(subject).not_to be_valid
+      end
+
+      it 'valid with image' do
+        subject.options[:image] = 'some-url'
+        expect(subject).to be_valid
+      end
+    end
+
+    context '#event' do
+      before(:each) do
+        subject.options[:type] = 'event'
+      end
+
+      it 'invalid without start_time' do
+        expect(subject).not_to be_valid
+      end
+
+      it 'valid with start_time' do
+        subject.options[:start_time] = Time.now
+        expect(subject).to be_valid
+      end
+    end
+
+    context '#location' do
+      before(:each) do
+        subject.options[:type] = 'location'
+      end
+
+      it 'invalid without latitude and longitude' do
+        expect(subject).not_to be_valid
+      end
+
+      it 'valid with latitude and longitude' do
+        subject.options[:latitude] = 15.0
+        subject.options[:longitude] = 16.0
+        expect(subject).to be_valid
+      end
+    end
+
+    context '#task' do
+      before(:each) do
+        subject.options[:type] = 'task'
+      end
+
+      it 'valid with text' do
+        expect(subject).to be_valid
+      end
+    end
+  end
+
+  context 'payload_for' do
+    it 'removes unwanted attributes' do
+      result = subject.send(:payload_for, {'type' => 'message', 'text' => 'text',
+        'sender_id' => 'sender', 'phone' => '+1', 'random_attribute' => 'unwanted'})
+      expect(result).to eq('{"text":"text","sender_id":"sender","phone":"+1"}')
+    end
+  end
+
+  context 'headers' do
+    it 'sets X-Beeper-Application-Id header with app_id' do
+      expect(subject.send(:headers)['X-Beeper-Application-Id']).to eq(base_params['app_id'])
+    end
+
+    it 'sets X-Beeper-REST-API-Key header with api_key' do
+      expect(subject.send(:headers)['X-Beeper-REST-API-Key']).to eq(base_params['api_key'])
+    end
+
+    it 'sets Content-Type' do
+      expect(subject.send(:headers)['Content-Type']).to eq('application/json')
+    end
+  end
+
+  context 'endpoint_for' do
+    it 'returns valid URL for message' do
+      expect(subject.send(:endpoint_for, 'message')).to eq('https://api.beeper.io/api/messages.json')
+    end
+
+    it 'returns valid URL for image' do
+      expect(subject.send(:endpoint_for, 'image')).to eq('https://api.beeper.io/api/images.json')
+    end
+
+    it 'returns valid URL for event' do
+      expect(subject.send(:endpoint_for, 'event')).to eq('https://api.beeper.io/api/events.json')
+    end
+
+    it 'returns valid URL for location' do
+      expect(subject.send(:endpoint_for, 'location')).to eq('https://api.beeper.io/api/locations.json')
+    end
+    it 'returns valid URL for task' do
+      expect(subject.send(:endpoint_for, 'task')).to eq('https://api.beeper.io/api/tasks.json')
+    end
+  end
+end