From 8a14a57e00716c12d060a036240cbfda789ab010 Mon Sep 17 00:00:00 2001 From: Andrew Cantino Date: Sun, 27 Nov 2016 15:30:50 -0500 Subject: [PATCH] Beeper.io is no more (#1808) * Beeper.io is no more * Avoid event propagation or scheduling of missing agents * Update undefined_agents.html.erb with a link to the wiki --- app/models/agent.rb | 8 +- app/models/agents/beeper_agent.rb | 129 ---------------- .../application/undefined_agents.html.erb | 8 +- spec/models/agent_spec.rb | 33 +++- spec/models/agents/beeper_agent_spec.rb | 145 ------------------ 5 files changed, 41 insertions(+), 282 deletions(-) delete mode 100644 app/models/agents/beeper_agent.rb delete mode 100644 spec/models/agents/beeper_agent_spec.rb diff --git a/app/models/agent.rb b/app/models/agent.rb index b8d32ee9..66672d28 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -370,7 +370,7 @@ class Agent < ActiveRecord::Base def receive!(options={}) Agent.transaction do scope = Agent. - select("agents.id AS receiver_agent_id, events.id AS event_id"). + select("agents.id AS receiver_agent_id, sources.type AS source_agent_type, agents.type AS receiver_agent_type, events.id AS event_id"). joins("JOIN links ON (links.receiver_id = agents.id)"). joins("JOIN agents AS sources ON (links.source_id = sources.id)"). joins("JOIN events ON (events.agent_id = sources.id AND events.id > links.event_id_at_creation)"). @@ -379,10 +379,11 @@ class Agent < ActiveRecord::Base scope = scope.where("agents.id in (?)", options[:only_receivers]) end - sql = scope.to_sql() + sql = scope.to_sql agents_to_events = {} - Agent.connection.select_rows(sql).each do |receiver_agent_id, event_id| + Agent.connection.select_rows(sql).each do |receiver_agent_id, source_agent_type, receiver_agent_type, event_id| + next unless const_defined?(source_agent_type) && const_defined?(receiver_agent_type) agents_to_events[receiver_agent_id.to_i] ||= [] agents_to_events[receiver_agent_id.to_i] << event_id end @@ -417,6 +418,7 @@ class Agent < ActiveRecord::Base return if schedule == 'never' types = where(:schedule => schedule).group(:type).pluck(:type) types.each do |type| + next unless const_defined?(type) type.constantize.bulk_check(schedule) end end diff --git a/app/models/agents/beeper_agent.rb b/app/models/agents/beeper_agent.rb deleted file mode 100644 index 4cd5a41e..00000000 --- a/app/models/agents/beeper_agent.rb +++ /dev/null @@ -1,129 +0,0 @@ -module Agents - class BeeperAgent < Agent - cannot_be_scheduled! - cannot_create_events! - no_bulk_receive! - - 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) - error(response.body) if response.code != 201 - 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) - mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json - end - - def endpoint_for(type) - "#{BASE_URL}/#{type}s.json" - end - end -end \ No newline at end of file diff --git a/app/views/application/undefined_agents.html.erb b/app/views/application/undefined_agents.html.erb index 3912af7d..229f3e19 100644 --- a/app/views/application/undefined_agents.html.erb +++ b/app/views/application/undefined_agents.html.erb @@ -19,20 +19,20 @@

- The issue most probably occurred because of one or more of the following reasons: + This issue probably occurred for one or more of the following reasons:


- You can fix the issue by adding the Agent(s) back to the application codebase by + You can fix this issue by:


diff --git a/spec/models/agent_spec.rb b/spec/models/agent_spec.rb index fa98b45b..7a67a587 100644 --- a/spec/models/agent_spec.rb +++ b/spec/models/agent_spec.rb @@ -74,6 +74,13 @@ describe Agent do Agent.run_schedule("midnight") end + it "ignores unknown types" do + Agent.where(id: agents(:bob_weather_agent).id).update_all type: 'UnknownTypeAgent' + mock(Agents::WeatherAgent).bulk_check("midnight").once + mock(Agents::WebsiteAgent).bulk_check("midnight").once + Agent.run_schedule("midnight") + end + it "only runs agents with the given schedule" do do_not_allow(Agents::WebsiteAgent).async_check Agent.run_schedule("blah") @@ -283,13 +290,37 @@ describe Agent do Agent.receive! end - it "should not propogate to disabled Agents" do + it "should not propagate to disabled Agents" do Agent.async_check(agents(:bob_weather_agent).id) agents(:bob_rain_notifier_agent).update_attribute :disabled, true mock(Agent).async_receive(agents(:bob_rain_notifier_agent).id, anything).times(0) Agent.receive! end + it "should not propagate to Agents with unknown types" do + Agent.async_check(agents(:jane_weather_agent).id) + Agent.async_check(agents(:bob_weather_agent).id) + + Agent.where(id: agents(:bob_rain_notifier_agent).id).update_all type: 'UnknownTypeAgent' + + mock(Agent).async_receive(agents(:bob_rain_notifier_agent).id, anything).times(0) + mock(Agent).async_receive(agents(:jane_rain_notifier_agent).id, anything).times(1) + + Agent.receive! + end + + it "should not propagate from Agents with unknown types" do + Agent.async_check(agents(:jane_weather_agent).id) + Agent.async_check(agents(:bob_weather_agent).id) + + Agent.where(id: agents(:bob_weather_agent).id).update_all type: 'UnknownTypeAgent' + + mock(Agent).async_receive(agents(:bob_rain_notifier_agent).id, anything).times(0) + mock(Agent).async_receive(agents(:jane_rain_notifier_agent).id, anything).times(1) + + Agent.receive! + end + it "should log exceptions" do mock.any_instance_of(Agents::TriggerAgent).receive(anything).once { raise "foo" diff --git a/spec/models/agents/beeper_agent_spec.rb b/spec/models/agents/beeper_agent_spec.rb deleted file mode 100644 index e2c2aba5..00000000 --- a/spec/models/agents/beeper_agent_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'rails_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