| @@ -35,7 +35,7 @@ gem 'hipchat', '~> 1.2.0' # HipchatAgent | ||
| 35 | 35 | gem 'xmpp4r', '~> 0.5.6' # JabberAgent | 
| 36 | 36 | gem 'mqtt' # MQTTAgent | 
| 37 | 37 | gem 'slack-notifier', '~> 1.0.0' # SlackAgent | 
| 38 | -gem 'hypdf', '~> 1.0.7' # PDFInfoAgent | |
| 38 | +gem 'hypdf', '~> 1.0.10' # PDFInfoAgent | |
| 39 | 39 |  | 
| 40 | 40 | # Weibo Agents | 
| 41 | 41 | gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master' | 
| @@ -100,6 +100,7 @@ gem 'foreman', '~> 0.63.0' | ||
| 100 | 100 | gem 'geokit', '~> 1.8.4' | 
| 101 | 101 | gem 'geokit-rails', '~> 2.0.1' | 
| 102 | 102 | gem 'httparty', '~> 0.13' | 
| 103 | +gem 'httmultiparty', '~> 0.3.16' | |
| 103 | 104 | gem 'jquery-rails', '~> 3.1.3' | 
| 104 | 105 | gem 'json', '~> 1.8.1' | 
| 105 | 106 | gem 'jsonpath', '~> 0.5.6' | 
| @@ -267,19 +267,21 @@ GEM | ||
| 267 | 267 | hipchat (1.2.0) | 
| 268 | 268 | httparty | 
| 269 | 269 | hpricot (0.8.6) | 
| 270 | - httmultiparty (0.3.10) | |
| 270 | + httmultiparty (0.3.16) | |
| 271 | 271 | httparty (>= 0.7.3) | 
| 272 | + mimemagic | |
| 272 | 273 | multipart-post | 
| 273 | 274 | http (0.6.4) | 
| 274 | 275 | http_parser.rb (~> 0.6.0) | 
| 275 | 276 | http-cookie (1.0.2) | 
| 276 | 277 | domain_name (~> 0.5) | 
| 277 | 278 | http_parser.rb (0.6.0) | 
| 278 | - httparty (0.13.1) | |
| 279 | + httparty (0.13.7) | |
| 279 | 280 | json (~> 1.8) | 
| 280 | 281 | multi_xml (>= 0.5.2) | 
| 281 | - hypdf (1.0.7) | |
| 282 | - httmultiparty (= 0.3.10) | |
| 282 | + hypdf (1.0.10) | |
| 283 | + httmultiparty (~> 0.3) | |
| 284 | + httparty (~> 0.13) | |
| 283 | 285 | i18n (0.7.0) | 
| 284 | 286 | jmespath (1.1.3) | 
| 285 | 287 | jquery-rails (3.1.3) | 
| @@ -318,6 +320,7 @@ GEM | ||
| 318 | 320 | thread_safe (~> 0.3, >= 0.3.1) | 
| 319 | 321 | method_source (0.8.2) | 
| 320 | 322 | mime-types (2.99.1) | 
| 323 | + mimemagic (0.3.1) | |
| 321 | 324 | mini_magick (4.2.3) | 
| 322 | 325 | mini_portile2 (2.0.0) | 
| 323 | 326 | minitest (5.8.4) | 
| @@ -616,8 +619,9 @@ DEPENDENCIES | ||
| 616 | 619 | guard-rspec (~> 4.6.4) | 
| 617 | 620 | haversine | 
| 618 | 621 | hipchat (~> 1.2.0) | 
| 622 | + httmultiparty (~> 0.3.16) | |
| 619 | 623 | httparty (~> 0.13) | 
| 620 | - hypdf (~> 1.0.7) | |
| 624 | + hypdf (~> 1.0.10) | |
| 621 | 625 | jquery-rails (~> 3.1.3) | 
| 622 | 626 | json (~> 1.8.1) | 
| 623 | 627 | jsonpath (~> 0.5.6) | 
| @@ -0,0 +1,96 @@ | ||
| 1 | +require 'httmultiparty' | |
| 2 | +require 'open-uri' | |
| 3 | +require 'tempfile' | |
| 4 | + | |
| 5 | +module Agents | |
| 6 | + class TelegramAgent < Agent | |
| 7 | + cannot_be_scheduled! | |
| 8 | + cannot_create_events! | |
| 9 | + no_bulk_receive! | |
| 10 | + | |
| 11 | + description <<-MD | |
| 12 | + The Telegram Agent receives and collects events and sends them via [Telegram](https://telegram.org/). | |
| 13 | + | |
| 14 | + 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. | |
| 15 | + | |
| 16 | + The value of `text` key is sent as a plain text message. | |
| 17 | + The value of `photo`, `audio`, `document` and `video` keys should be a url whose contents will be sent to you. | |
| 18 | + | |
| 19 | + **Setup** | |
| 20 | + | |
| 21 | + 1. Obtain an `auth_token` by [creating a new bot](https://telegram.me/botfather). | |
| 22 | + 2. Send a private message to your bot by visiting https://telegram.me/YourHuginnBot | |
| 23 | + 3. Obtain your private `chat_id` from the recently started conversation by visiting https://api.telegram.org/bot<auth_token>/getUpdates | |
| 24 | + MD | |
| 25 | + | |
| 26 | + def default_options | |
| 27 | +      { | |
| 28 | + auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', | |
| 29 | + chat_id: 'xxxxxxxx' | |
| 30 | + } | |
| 31 | + end | |
| 32 | + | |
| 33 | + def validate_options | |
| 34 | + errors.add(:base, 'auth_token is required') unless options['auth_token'].present? | |
| 35 | + errors.add(:base, 'chat_id is required') unless options['chat_id'].present? | |
| 36 | + end | |
| 37 | + | |
| 38 | + def working? | |
| 39 | + received_event_without_error? && !recent_error_logs? | |
| 40 | + end | |
| 41 | + | |
| 42 | + def receive(incoming_events) | |
| 43 | + incoming_events.each do |event| | |
| 44 | + receive_event event | |
| 45 | + end | |
| 46 | + end | |
| 47 | + | |
| 48 | + private | |
| 49 | + | |
| 50 | +    TELEGRAM_ACTIONS = { | |
| 51 | + text: :sendMessage, | |
| 52 | + photo: :sendPhoto, | |
| 53 | + audio: :sendAudio, | |
| 54 | + document: :sendDocument, | |
| 55 | + video: :sendVideo | |
| 56 | + }.freeze | |
| 57 | + | |
| 58 | + def telegram_bot_uri(method) | |
| 59 | +      "https://api.telegram.org/bot#{interpolated['auth_token']}/#{method}" | |
| 60 | + end | |
| 61 | + | |
| 62 | + def receive_event(event) | |
| 63 | + TELEGRAM_ACTIONS.each do |field, method| | |
| 64 | + payload = load_field event, field | |
| 65 | + next unless payload | |
| 66 | + send_telegram_message method, field => payload | |
| 67 | + unlink_file payload if payload.is_a? Tempfile | |
| 68 | + end | |
| 69 | + end | |
| 70 | + | |
| 71 | + def send_telegram_message(method, params) | |
| 72 | + params[:chat_id] = interpolated['chat_id'] | |
| 73 | + HTTMultiParty.post telegram_bot_uri(method), query: params | |
| 74 | + end | |
| 75 | + | |
| 76 | + def load_field(event, field) | |
| 77 | + payload = event.payload[field] | |
| 78 | + return false unless payload.present? | |
| 79 | + return payload if field == :text | |
| 80 | + load_file payload | |
| 81 | + end | |
| 82 | + | |
| 83 | + def load_file(url) | |
| 84 | + file = Tempfile.new [File.basename(url), File.extname(url)] | |
| 85 | + file.binmode | |
| 86 | + file.write open(url).read | |
| 87 | + file.rewind | |
| 88 | + file | |
| 89 | + end | |
| 90 | + | |
| 91 | + def unlink_file(file) | |
| 92 | + file.close | |
| 93 | + file.unlink | |
| 94 | + end | |
| 95 | + end | |
| 96 | +end | 
| @@ -0,0 +1,102 @@ | ||
| 1 | +require 'rails_helper' | |
| 2 | + | |
| 3 | +describe Agents::TelegramAgent do | |
| 4 | + before do | |
| 5 | +    default_options = { | |
| 6 | + auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', | |
| 7 | + chat_id: 'xxxxxxxx' | |
| 8 | + } | |
| 9 | + @checker = Agents::TelegramAgent.new name: 'Telegram Tester', options: default_options | |
| 10 | + @checker.user = users(:bob) | |
| 11 | + @checker.save! | |
| 12 | + | |
| 13 | + @sent_messages = [] | |
| 14 | + stub_methods | |
| 15 | + end | |
| 16 | + | |
| 17 | + def stub_methods | |
| 18 | + stub.any_instance_of(Agents::TelegramAgent).send_telegram_message do |method, params| | |
| 19 | +      @sent_messages << { method => params } | |
| 20 | + end | |
| 21 | + | |
| 22 | + stub.any_instance_of(Agents::TelegramAgent).load_file do |_url| | |
| 23 | + :stubbed_file | |
| 24 | + end | |
| 25 | + end | |
| 26 | + | |
| 27 | + def event_with_payload(payload) | |
| 28 | + event = Event.new | |
| 29 | + event.agent = agents(:bob_weather_agent) | |
| 30 | + event.payload = payload | |
| 31 | + event.save! | |
| 32 | + event | |
| 33 | + end | |
| 34 | + | |
| 35 | + describe 'validation' do | |
| 36 | + before do | |
| 37 | + expect(@checker).to be_valid | |
| 38 | + end | |
| 39 | + | |
| 40 | + it 'should validate presence of of auth_token' do | |
| 41 | + @checker.options[:auth_token] = '' | |
| 42 | + expect(@checker).not_to be_valid | |
| 43 | + end | |
| 44 | + | |
| 45 | + it 'should validate presence of of chat_id' do | |
| 46 | + @checker.options[:chat_id] = '' | |
| 47 | + expect(@checker).not_to be_valid | |
| 48 | + end | |
| 49 | + end | |
| 50 | + | |
| 51 | + describe '#receive' do | |
| 52 | + it 'processes multiple events properly' do | |
| 53 | + event_0 = event_with_payload text: 'Looks like its going to rain' | |
| 54 | + event_1 = event_with_payload text: 'Another text message' | |
| 55 | + @checker.receive [event_0, event_1] | |
| 56 | + | |
| 57 | + expect(@sent_messages).to eq([ | |
| 58 | +        { sendMessage: { text: 'Looks like its going to rain' } }, | |
| 59 | +        { sendMessage: { text: 'Another text message' } } | |
| 60 | + ]) | |
| 61 | + end | |
| 62 | + | |
| 63 | + it 'accepts photo key and uses :send_photo to send the file' do | |
| 64 | + event = event_with_payload photo: 'https://example.com/image.png' | |
| 65 | + @checker.receive [event] | |
| 66 | + | |
| 67 | +      expect(@sent_messages).to eq([{ sendPhoto: { photo: :stubbed_file } }]) | |
| 68 | + end | |
| 69 | + | |
| 70 | + it 'accepts audio key and uses :send_audio to send the file' do | |
| 71 | + event = event_with_payload audio: 'https://example.com/sound.mp3' | |
| 72 | + @checker.receive [event] | |
| 73 | + | |
| 74 | +      expect(@sent_messages).to eq([{ sendAudio: { audio: :stubbed_file } }]) | |
| 75 | + end | |
| 76 | + | |
| 77 | + it 'accepts document key and uses :send_document to send the file' do | |
| 78 | + event = event_with_payload document: 'https://example.com/document.pdf' | |
| 79 | + @checker.receive [event] | |
| 80 | + | |
| 81 | +      expect(@sent_messages).to eq([{ sendDocument: { document: :stubbed_file } }]) | |
| 82 | + end | |
| 83 | + | |
| 84 | + it 'accepts video key and uses :send_video to send the file' do | |
| 85 | + event = event_with_payload video: 'https://example.com/video.avi' | |
| 86 | + @checker.receive [event] | |
| 87 | + | |
| 88 | +      expect(@sent_messages).to eq([{ sendVideo: { video: :stubbed_file } }]) | |
| 89 | + end | |
| 90 | + end | |
| 91 | + | |
| 92 | + describe '#working?' do | |
| 93 | + it 'is not working without having received an event' do | |
| 94 | + expect(@checker).not_to be_working | |
| 95 | + end | |
| 96 | + | |
| 97 | + it 'is working after receiving an event without error' do | |
| 98 | + @checker.last_receive_at = Time.now | |
| 99 | + expect(@checker).to be_working | |
| 100 | + end | |
| 101 | + end | |
| 102 | +end |