@@ -0,0 +1,130 @@ |
||
| 1 |
+module Agents |
|
| 2 |
+ class BeeperAgent < Agent |
|
| 3 |
+ cannot_be_scheduled! |
|
| 4 |
+ cannot_create_events! |
|
| 5 |
+ |
|
| 6 |
+ description <<-MD |
|
| 7 |
+ Beeper agent sends messages to Beeper app on your mobile device via Push notifications. |
|
| 8 |
+ |
|
| 9 |
+ 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) |
|
| 10 |
+ |
|
| 11 |
+ 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 |
|
| 12 |
+ |
|
| 13 |
+ Also you have to provide a message `type` which has to be `message`, `image`, `event`, `location` or `task`. |
|
| 14 |
+ |
|
| 15 |
+ Depending on message type you have to provide additional fields: |
|
| 16 |
+ |
|
| 17 |
+ ##### Message |
|
| 18 |
+ * `text` – **required** |
|
| 19 |
+ |
|
| 20 |
+ ##### Image |
|
| 21 |
+ * `image` – **required** (Image URL or Base64-encoded image) |
|
| 22 |
+ * `text` – optional |
|
| 23 |
+ |
|
| 24 |
+ ##### Event |
|
| 25 |
+ * `text` – **required** |
|
| 26 |
+ * `start_time` – **required** (Corresponding to ISO 8601) |
|
| 27 |
+ * `end_time` – optional (Corresponding to ISO 8601) |
|
| 28 |
+ |
|
| 29 |
+ ##### Location |
|
| 30 |
+ * `latitude` – **required** |
|
| 31 |
+ * `longitude` – **required** |
|
| 32 |
+ * `text` – optional |
|
| 33 |
+ |
|
| 34 |
+ ##### Task |
|
| 35 |
+ * `text` – **required** |
|
| 36 |
+ |
|
| 37 |
+ You can see additional documentation at [Beeper website](https://beeper.io/docs) |
|
| 38 |
+ MD |
|
| 39 |
+ |
|
| 40 |
+ BASE_URL = 'https://api.beeper.io/api' |
|
| 41 |
+ |
|
| 42 |
+ TYPE_ATTRIBUTES = {
|
|
| 43 |
+ 'message' => %w(text), |
|
| 44 |
+ 'image' => %w(text image), |
|
| 45 |
+ 'event' => %w(text start_time end_time), |
|
| 46 |
+ 'location' => %w(text latitude longitude), |
|
| 47 |
+ 'task' => %w(text) |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ MESSAGE_TYPES = TYPE_ATTRIBUTES.keys |
|
| 51 |
+ |
|
| 52 |
+ TYPE_REQUIRED_ATTRIBUTES = {
|
|
| 53 |
+ 'message' => %w(text), |
|
| 54 |
+ 'image' => %w(image), |
|
| 55 |
+ 'event' => %w(text start_time), |
|
| 56 |
+ 'location' => %w(latitude longitude), |
|
| 57 |
+ 'task' => %w(text) |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ def default_options |
|
| 61 |
+ {
|
|
| 62 |
+ 'type' => 'message', |
|
| 63 |
+ 'app_id' => '', |
|
| 64 |
+ 'api_key' => '', |
|
| 65 |
+ 'sender_id' => '', |
|
| 66 |
+ 'phone' => '', |
|
| 67 |
+ 'text' => '{{title}}'
|
|
| 68 |
+ } |
|
| 69 |
+ end |
|
| 70 |
+ |
|
| 71 |
+ def validate_options |
|
| 72 |
+ %w(app_id api_key sender_id type).each do |attr| |
|
| 73 |
+ errors.add(:base, "you need to specify a #{attr}") if options[attr].blank?
|
|
| 74 |
+ end |
|
| 75 |
+ |
|
| 76 |
+ if options['type'].in?(MESSAGE_TYPES) |
|
| 77 |
+ required_attributes = TYPE_REQUIRED_ATTRIBUTES[options['type']] |
|
| 78 |
+ if required_attributes.any? { |attr| options[attr].blank? }
|
|
| 79 |
+ errors.add(:base, "you need to specify a #{required_attributes.join(', ')}")
|
|
| 80 |
+ end |
|
| 81 |
+ else |
|
| 82 |
+ errors.add(:base, 'you need to specify a valid message type') |
|
| 83 |
+ end |
|
| 84 |
+ |
|
| 85 |
+ unless options['group_id'].blank? ^ options['phone'].blank? |
|
| 86 |
+ errors.add(:base, 'you need to specify a phone or group_id') |
|
| 87 |
+ end |
|
| 88 |
+ end |
|
| 89 |
+ |
|
| 90 |
+ def working? |
|
| 91 |
+ received_event_without_error? && !recent_error_logs? |
|
| 92 |
+ end |
|
| 93 |
+ |
|
| 94 |
+ def receive(incoming_events) |
|
| 95 |
+ incoming_events.each do |event| |
|
| 96 |
+ send_message(event) |
|
| 97 |
+ end |
|
| 98 |
+ end |
|
| 99 |
+ |
|
| 100 |
+ def send_message(event) |
|
| 101 |
+ mo = interpolated(event) |
|
| 102 |
+ begin |
|
| 103 |
+ response = HTTParty.post(endpoint_for(mo['type']), body: payload_for(mo), headers: headers) |
|
| 104 |
+ response.code == 201 ? log(response.body) : error(response.body) |
|
| 105 |
+ rescue HTTParty::Error => e |
|
| 106 |
+ error(e.message) |
|
| 107 |
+ end |
|
| 108 |
+ end |
|
| 109 |
+ |
|
| 110 |
+ private |
|
| 111 |
+ |
|
| 112 |
+ def headers |
|
| 113 |
+ {
|
|
| 114 |
+ 'X-Beeper-Application-Id' => options['app_id'], |
|
| 115 |
+ 'X-Beeper-REST-API-Key' => options['api_key'], |
|
| 116 |
+ 'Content-Type' => 'application/json' |
|
| 117 |
+ } |
|
| 118 |
+ end |
|
| 119 |
+ |
|
| 120 |
+ def payload_for(mo) |
|
| 121 |
+ payload = mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json |
|
| 122 |
+ log(payload) |
|
| 123 |
+ payload |
|
| 124 |
+ end |
|
| 125 |
+ |
|
| 126 |
+ def endpoint_for(type) |
|
| 127 |
+ "#{BASE_URL}/#{type}s.json"
|
|
| 128 |
+ end |
|
| 129 |
+ end |
|
| 130 |
+end |
@@ -0,0 +1,145 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+describe Agents::BeeperAgent do |
|
| 5 |
+ let(:base_params) {
|
|
| 6 |
+ {
|
|
| 7 |
+ 'type' => 'message', |
|
| 8 |
+ 'app_id' => 'some-app-id', |
|
| 9 |
+ 'api_key' => 'some-api-key', |
|
| 10 |
+ 'sender_id' => 'sender-id', |
|
| 11 |
+ 'phone' => '+111111111111', |
|
| 12 |
+ 'text' => 'Some text' |
|
| 13 |
+ } |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ subject {
|
|
| 17 |
+ agent = described_class.new(name: 'beeper-agent', options: base_params) |
|
| 18 |
+ agent.user = users(:jane) |
|
| 19 |
+ agent.save! and return agent |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ context 'validation' do |
|
| 23 |
+ it 'valid' do |
|
| 24 |
+ expect(subject).to be_valid |
|
| 25 |
+ end |
|
| 26 |
+ |
|
| 27 |
+ [:type, :app_id, :api_key, :sender_id].each do |attr| |
|
| 28 |
+ it "invalid without #{attr}" do
|
|
| 29 |
+ subject.options[attr] = nil |
|
| 30 |
+ expect(subject).not_to be_valid |
|
| 31 |
+ end |
|
| 32 |
+ end |
|
| 33 |
+ |
|
| 34 |
+ it 'invalid with group_id and phone' do |
|
| 35 |
+ subject.options['group_id'] ='some-group-id' |
|
| 36 |
+ expect(subject).not_to be_valid |
|
| 37 |
+ end |
|
| 38 |
+ |
|
| 39 |
+ context '#message' do |
|
| 40 |
+ it 'requires text' do |
|
| 41 |
+ subject.options[:text] = nil |
|
| 42 |
+ expect(subject).not_to be_valid |
|
| 43 |
+ end |
|
| 44 |
+ end |
|
| 45 |
+ |
|
| 46 |
+ context '#image' do |
|
| 47 |
+ before(:each) do |
|
| 48 |
+ subject.options[:type] = 'image' |
|
| 49 |
+ end |
|
| 50 |
+ |
|
| 51 |
+ it 'invalid without image' do |
|
| 52 |
+ expect(subject).not_to be_valid |
|
| 53 |
+ end |
|
| 54 |
+ |
|
| 55 |
+ it 'valid with image' do |
|
| 56 |
+ subject.options[:image] = 'some-url' |
|
| 57 |
+ expect(subject).to be_valid |
|
| 58 |
+ end |
|
| 59 |
+ end |
|
| 60 |
+ |
|
| 61 |
+ context '#event' do |
|
| 62 |
+ before(:each) do |
|
| 63 |
+ subject.options[:type] = 'event' |
|
| 64 |
+ end |
|
| 65 |
+ |
|
| 66 |
+ it 'invalid without start_time' do |
|
| 67 |
+ expect(subject).not_to be_valid |
|
| 68 |
+ end |
|
| 69 |
+ |
|
| 70 |
+ it 'valid with start_time' do |
|
| 71 |
+ subject.options[:start_time] = Time.now |
|
| 72 |
+ expect(subject).to be_valid |
|
| 73 |
+ end |
|
| 74 |
+ end |
|
| 75 |
+ |
|
| 76 |
+ context '#location' do |
|
| 77 |
+ before(:each) do |
|
| 78 |
+ subject.options[:type] = 'location' |
|
| 79 |
+ end |
|
| 80 |
+ |
|
| 81 |
+ it 'invalid without latitude and longitude' do |
|
| 82 |
+ expect(subject).not_to be_valid |
|
| 83 |
+ end |
|
| 84 |
+ |
|
| 85 |
+ it 'valid with latitude and longitude' do |
|
| 86 |
+ subject.options[:latitude] = 15.0 |
|
| 87 |
+ subject.options[:longitude] = 16.0 |
|
| 88 |
+ expect(subject).to be_valid |
|
| 89 |
+ end |
|
| 90 |
+ end |
|
| 91 |
+ |
|
| 92 |
+ context '#task' do |
|
| 93 |
+ before(:each) do |
|
| 94 |
+ subject.options[:type] = 'task' |
|
| 95 |
+ end |
|
| 96 |
+ |
|
| 97 |
+ it 'valid with text' do |
|
| 98 |
+ expect(subject).to be_valid |
|
| 99 |
+ end |
|
| 100 |
+ end |
|
| 101 |
+ end |
|
| 102 |
+ |
|
| 103 |
+ context 'payload_for' do |
|
| 104 |
+ it 'removes unwanted attributes' do |
|
| 105 |
+ result = subject.send(:payload_for, {'type' => 'message', 'text' => 'text',
|
|
| 106 |
+ 'sender_id' => 'sender', 'phone' => '+1', 'random_attribute' => 'unwanted'}) |
|
| 107 |
+ expect(result).to eq('{"text":"text","sender_id":"sender","phone":"+1"}')
|
|
| 108 |
+ end |
|
| 109 |
+ end |
|
| 110 |
+ |
|
| 111 |
+ context 'headers' do |
|
| 112 |
+ it 'sets X-Beeper-Application-Id header with app_id' do |
|
| 113 |
+ expect(subject.send(:headers)['X-Beeper-Application-Id']).to eq(base_params['app_id']) |
|
| 114 |
+ end |
|
| 115 |
+ |
|
| 116 |
+ it 'sets X-Beeper-REST-API-Key header with api_key' do |
|
| 117 |
+ expect(subject.send(:headers)['X-Beeper-REST-API-Key']).to eq(base_params['api_key']) |
|
| 118 |
+ end |
|
| 119 |
+ |
|
| 120 |
+ it 'sets Content-Type' do |
|
| 121 |
+ expect(subject.send(:headers)['Content-Type']).to eq('application/json')
|
|
| 122 |
+ end |
|
| 123 |
+ end |
|
| 124 |
+ |
|
| 125 |
+ context 'endpoint_for' do |
|
| 126 |
+ it 'returns valid URL for message' do |
|
| 127 |
+ expect(subject.send(:endpoint_for, 'message')).to eq('https://api.beeper.io/api/messages.json')
|
|
| 128 |
+ end |
|
| 129 |
+ |
|
| 130 |
+ it 'returns valid URL for image' do |
|
| 131 |
+ expect(subject.send(:endpoint_for, 'image')).to eq('https://api.beeper.io/api/images.json')
|
|
| 132 |
+ end |
|
| 133 |
+ |
|
| 134 |
+ it 'returns valid URL for event' do |
|
| 135 |
+ expect(subject.send(:endpoint_for, 'event')).to eq('https://api.beeper.io/api/events.json')
|
|
| 136 |
+ end |
|
| 137 |
+ |
|
| 138 |
+ it 'returns valid URL for location' do |
|
| 139 |
+ expect(subject.send(:endpoint_for, 'location')).to eq('https://api.beeper.io/api/locations.json')
|
|
| 140 |
+ end |
|
| 141 |
+ it 'returns valid URL for task' do |
|
| 142 |
+ expect(subject.send(:endpoint_for, 'task')).to eq('https://api.beeper.io/api/tasks.json')
|
|
| 143 |
+ end |
|
| 144 |
+ end |
|
| 145 |
+end |