From adc1dee4fb9660678af73cf6b803308591cdd9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20K=C3=B6nig?= Date: Sun, 27 Mar 2016 15:56:31 +0200 Subject: [PATCH] implement basic telegram agent --- Gemfile | 23 ++--- Gemfile.lock | 26 ++++++ app/models/agents/telegram_agent.rb | 92 +++++++++++++++++++ spec/models/agents/telegram_agent_spec.rb | 102 ++++++++++++++++++++++ 4 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 app/models/agents/telegram_agent.rb create mode 100644 spec/models/agents/telegram_agent_spec.rb diff --git a/Gemfile b/Gemfile index 4065a39c..a5e5f01a 100644 --- a/Gemfile +++ b/Gemfile @@ -25,17 +25,18 @@ end # Optional libraries. To conserve RAM, comment out any that you don't need, # then run `bundle` and commit the updated Gemfile and Gemfile.lock. -gem 'twilio-ruby', '~> 3.11.5' # TwilioAgent -gem 'ruby-growl', '~> 4.1.0' # GrowlAgent -gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent -gem 'wunderground', '~> 1.2.0' # WeatherAgent -gem 'forecast_io', '~> 2.0.0' # WeatherAgent -gem 'rturk', '~> 2.12.1' # HumanTaskAgent -gem 'hipchat', '~> 1.2.0' # HipchatAgent -gem 'xmpp4r', '~> 0.5.6' # JabberAgent -gem 'mqtt' # MQTTAgent -gem 'slack-notifier', '~> 1.0.0' # SlackAgent -gem 'hypdf', '~> 1.0.7' # PDFInfoAgent +gem 'twilio-ruby', '~> 3.11.5' # TwilioAgent +gem 'ruby-growl', '~> 4.1.0' # GrowlAgent +gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent +gem 'wunderground', '~> 1.2.0' # WeatherAgent +gem 'forecast_io', '~> 2.0.0' # WeatherAgent +gem 'rturk', '~> 2.12.1' # HumanTaskAgent +gem 'hipchat', '~> 1.2.0' # HipchatAgent +gem 'xmpp4r', '~> 0.5.6' # JabberAgent +gem 'mqtt' # MQTTAgent +gem 'slack-notifier', '~> 1.0.0' # SlackAgent +gem 'hypdf', '~> 1.0.7' # PDFInfoAgent +gem 'telegram-bot-ruby', '~> 0.4.1' # TelegramAgent # Weibo Agents gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master' diff --git a/Gemfile.lock b/Gemfile.lock index 239fa74c..b2df0366 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -112,6 +112,10 @@ GEM multi_json (>= 1.0.0) aws-sdk-core (2.2.15) jmespath (~> 1.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) bcrypt (3.1.10) better_errors (1.1.0) coderay (>= 1.0.0) @@ -146,6 +150,8 @@ GEM chronic (0.10.2) cliver (0.3.2) coderay (1.1.0) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) coffee-rails (4.1.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.1.x) @@ -170,6 +176,8 @@ GEM activesupport (>= 3.0, < 5.0) delorean (2.1.0) chronic + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) devise (3.5.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -225,6 +233,8 @@ GEM dotenv (>= 0.7) thor (>= 0.13.6) formatador (0.2.5) + gene_pool (1.4.1) + thread_safe geokit (1.8.5) multi_json (>= 1.3.2) geokit-rails (2.0.1) @@ -281,6 +291,7 @@ GEM hypdf (1.0.7) httmultiparty (= 0.3.10) i18n (0.7.0) + ice_nine (0.11.2) jmespath (1.1.3) jquery-rails (3.1.3) railties (>= 3.0, < 5.0) @@ -371,6 +382,11 @@ GEM multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.5.0) + persistent_http (1.0.6) + gene_pool (>= 1.3) + persistent_httparty (0.1.2) + httparty (~> 0.9) + persistent_http (< 2) pg (0.18.3) poltergeist (1.8.1) capybara (~> 2.1) @@ -515,6 +531,10 @@ GEM net-ssh (>= 2.8.0) string-scrub (0.0.5) systemu (2.6.4) + telegram-bot-ruby (0.4.1) + httmultiparty + persistent_httparty + virtus term-ansicolor (1.3.0) tins (~> 1.0) therubyracer (0.12.2) @@ -559,6 +579,11 @@ GEM macaddr (~> 1.0) uuidtools (2.1.5) vcr (2.9.2) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) warden (1.2.4) rack (>= 1.0) webmock (1.17.4) @@ -664,6 +689,7 @@ DEPENDENCIES spring (~> 1.6.3) spring-commands-rspec (~> 1.0.4) string-scrub + telegram-bot-ruby (~> 0.4.1) therubyracer (~> 0.12.2) tumblr_client! twilio-ruby (~> 3.11.5) diff --git a/app/models/agents/telegram_agent.rb b/app/models/agents/telegram_agent.rb new file mode 100644 index 00000000..972a4a9c --- /dev/null +++ b/app/models/agents/telegram_agent.rb @@ -0,0 +1,92 @@ +require 'telegram/bot' +require 'open-uri' +require 'tempfile' + +module Agents + class TelegramAgent < Agent + cannot_be_scheduled! + cannot_create_events! + no_bulk_receive! + + gem_dependency_check { defined?(Telegram) } + + description <<-MD + #{'# Include `telegram-bot-ruby` in your Gemfile to use this Agent!' if dependencies_missing?} + + The Telegram Agent receives and collects events and sends them via [Telegram](https://telegram.org/). + + It is assumed that events have either a `text`, `photo`, `audio`, `document` or `video` key. You can use the EventFormattingAgent if your event does not provide these keys. + + The value of `text` key is sent as a plain text message. + The value of `photo`, `audio`, `document` and `video` keys should be an url which contents are sent to you according to the type. + + **Setup** + + 1. obtain an `auth_token` by [creating a new bot](https://telegram.me/botfather). + 2. [send a private message to your bot](https://telegram.me/YourHuginnBot) + 3. obtain your private `chat_id` [from the recently started conversation](https://api.telegram.org/bot/getUpdates) + MD + + def default_options + { + auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + chat_id: 'xxxxxxxx' + } + end + + def validate_options + errors.add(:base, 'auth_token is required') unless options['auth_token'].present? + errors.add(:base, 'chat_id is required') unless options['chat_id'].present? + end + + def working? + received_event_without_error? && !recent_error_logs? + end + + def receive(incoming_events) + incoming_events.each do |event| + receive_event event + end + end + + private + + TELEGRAM_FIELDS = { + text: :send_message, + photo: :send_photo, + audio: :send_audio, + document: :send_document, + video: :send_video + }.freeze + + def receive_event(event) + TELEGRAM_FIELDS.each do |field, method| + payload = load_field event, field + next unless payload + send_telegram_message method, field => payload + end + end + + def send_telegram_message(method, params) + params[:chat_id] = interpolated['chat_id'] + Telegram::Bot::Client.run interpolated['auth_token'] do |bot| + bot.api.send method, params + end + end + + def load_field(event, field) + payload = event.payload[field] + return false unless payload.present? + return payload if field == :text + load_file payload + end + + def load_file(url) + file = Tempfile.new [File.basename(url), File.extname(url)] + file.binmode + file.write open(url).read + file.rewind + file + end + end +end diff --git a/spec/models/agents/telegram_agent_spec.rb b/spec/models/agents/telegram_agent_spec.rb new file mode 100644 index 00000000..f8ea4ee6 --- /dev/null +++ b/spec/models/agents/telegram_agent_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +describe Agents::TelegramAgent do + before do + default_options = { + auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + chat_id: 'xxxxxxxx' + } + @checker = Agents::TelegramAgent.new name: 'Telegram Tester', options: default_options + @checker.user = users(:bob) + @checker.save! + + @sent_messages = [] + stub_methods + end + + def stub_methods + stub.any_instance_of(Agents::TelegramAgent).send_telegram_message do |method, params| + @sent_messages << { method => params } + end + + stub.any_instance_of(Agents::TelegramAgent).load_file do |_url| + :stubbed_file + end + end + + def event_with_payload(payload) + event = Event.new + event.agent = agents(:bob_weather_agent) + event.payload = payload + event.save! + event + end + + describe 'validation' do + before do + expect(@checker).to be_valid + end + + it 'should validate presence of of auth_token' do + @checker.options[:auth_token] = '' + expect(@checker).not_to be_valid + end + + it 'should validate presence of of chat_id' do + @checker.options[:chat_id] = '' + expect(@checker).not_to be_valid + end + end + + describe '#receive' do + it 'processes multiple events properly' do + event_0 = event_with_payload text: 'Looks like its going to rain' + event_1 = event_with_payload text: 'Another text message' + @checker.receive [event_0, event_1] + + expect(@sent_messages).to eq([ + { send_message: { text: 'Looks like its going to rain' } }, + { send_message: { text: 'Another text message' } } + ]) + end + + it 'accepts photo key and uses :send_photo to send the file' do + event = event_with_payload photo: 'https://example.com/image.png' + @checker.receive [event] + + expect(@sent_messages).to eq([{ send_photo: { photo: :stubbed_file } }]) + end + + it 'accepts audio key and uses :send_audio to send the file' do + event = event_with_payload audio: 'https://example.com/sound.mp3' + @checker.receive [event] + + expect(@sent_messages).to eq([{ send_audio: { audio: :stubbed_file } }]) + end + + it 'accepts document key and uses :send_document to send the file' do + event = event_with_payload document: 'https://example.com/document.pdf' + @checker.receive [event] + + expect(@sent_messages).to eq([{ send_document: { document: :stubbed_file } }]) + end + + it 'accepts video key and uses :send_video to send the file' do + event = event_with_payload video: 'https://example.com/video.avi' + @checker.receive [event] + + expect(@sent_messages).to eq([{ send_video: { video: :stubbed_file } }]) + end + end + + describe '#working?' do + it 'is not working without having received an event' do + expect(@checker).not_to be_working + end + + it 'is working after receiving an event without error' do + @checker.last_receive_at = Time.now + expect(@checker).to be_working + end + end +end