From 225290b67754619719a837494e56e013ca2bca67 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 11 Mar 2016 13:03:13 +0600 Subject: [PATCH] Easily run UpdatesController's actions without updates --- .rubocop.yml | 2 + README.md | 17 +++ lib/telegram/bot/client.rb | 2 +- lib/telegram/bot/updates_controller.rb | 99 ++++++++++++++-- .../bot/updates_controller/rspec_helpers.rb | 4 +- .../bot/updates_controller/session.rb | 4 + .../message_context_spec.rb | 2 +- spec/telegram/bot/updates_controller_spec.rb | 107 +++++++++++++++++- 8 files changed, 220 insertions(+), 17 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8c48293..bcb8e00 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,9 +8,11 @@ Style/AlignParameters: # EnforcedStyle: # - with_first_parameter # - with_fixed_indentation +Style/AndOr: {EnforcedStyle: conditionals} Style/ClosingParenthesisIndentation: {Enabled: false} Style/Documentation: {Enabled: false} Style/DotPosition: {EnforcedStyle: trailing} +Style/FirstParameterIndentation: {EnforcedStyle: consistent} Style/IfUnlessModifier: {Enabled: false} Style/ModuleFunction: {Enabled: false} Style/MultilineOperationIndentation: {EnforcedStyle: indented} diff --git a/README.md b/README.md index cf0a0df..acdde1c 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,9 @@ config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1. class Telegram::WebhookController < Telegram::Bot::UpdatesController include Telegram::Bot::UpdatesController::Session + # or just shortcut: + use_session! + # You can override global config self.session_store = :file_store @@ -219,6 +222,20 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController end ``` +To process update run: + +```ruby +ControllerClass.dispatch(bot, update) +``` + +There is also ability to run action without update: + +```ruby +# Most likely you'll want to pass :from and :chat +controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat) +controller.process(:help, *args) +``` + ### Routes Use `telegram_webhooks` helper to add routes. It will create routes for bots diff --git a/lib/telegram/bot/client.rb b/lib/telegram/bot/client.rb index 41d7b65..c2193ef 100644 --- a/lib/telegram/bot/client.rb +++ b/lib/telegram/bot/client.rb @@ -104,7 +104,7 @@ module Telegram end def inspect - "#" + "#<#{self.class.name}##{object_id}(#{@username})>" end end end diff --git a/lib/telegram/bot/updates_controller.rb b/lib/telegram/bot/updates_controller.rb index 9f745af..ebfdb0c 100644 --- a/lib/telegram/bot/updates_controller.rb +++ b/lib/telegram/bot/updates_controller.rb @@ -4,6 +4,52 @@ require 'active_support/version' module Telegram module Bot + # Base class to create update processors. With callbacks, session and helpers. + # + # Define public methods for each command and they will be called when + # update has this command. Message is automatically parsed and + # words are passed as method arguments. Be sure to use default values and + # splat arguments in every action method to not get errors, when user + # sends command without necessary args / with extra args. + # + # def start(token = nil, *) + # if token + # # ... + # else + # # ... + # end + # end + # + # def help(*) + # reply_with :message, text: + # end + # + # To process plain text messages (without commands) or other updates just + # define public method with name of payload type. They will receive payload + # as an argument. + # + # def message(message) + # reply_with :message, text: "Echo: #{message['text']}" + # end + # + # def inline_query(query) + # answer_inline_query results_for_query(query), is_personal: true + # end + # + # # To process conflicting commands (`/message args`) just use `on_` prefix: + # def on_message(*args) + # # ... + # end + # + # To process update run: + # + # ControllerClass.dispatch(bot, update) + # + # There is also ability to run action without update: + # + # ControllerClass.new(bot, from: telegram_user, chat: telegram_chat). + # process(:help, *args) + # class UpdatesController < AbstractController::Base abstract! @@ -38,6 +84,7 @@ module Telegram CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)") class << self + # Initialize controller and process update. def dispatch(*args) new(*args).dispatch end @@ -68,18 +115,39 @@ module Telegram alias_method :command?, :is_command delegate :username, to: :bot, prefix: true, allow_nil: true + # Second argument can be either update object with hash access & string + # keys or Hash with `:from` or `:chat` to override this values and assume + # that update is nil. def initialize(bot = nil, update = nil) + if update.is_a?(Hash) && (update.key?(:from) || update.key?(:chat)) + options = update + update = nil + end @_update = update @_bot = bot + @_chat, @_from = options && options.values_at(:chat, :from) + payload_data = nil update && PAYLOAD_TYPES.find do |type| item = update[type] - next unless item - @_payload = item - @_payload_type = type + payload_data = [item, type] if item end + @_payload, @_payload_type = payload_data end + # Accessor to `'chat'` field of payload. Can be overriden with `chat` option + # for #initialize. + def chat + @_chat || payload && payload['chat'] + end + + # Accessor to `'from'` field of payload. Can be overriden with `from` option + # for #initialize. + def from + @_from || payload && payload['from'] + end + + # Processes current update. def dispatch @_is_command, action, args = action_for_payload process(action, *args) @@ -103,19 +171,32 @@ module Telegram def action_missing(*) end - %w(chat from).each do |field| - define_method(field) { payload[field] } - end - + # Helper to call bot's `send_#{type}` method with already set `chat_id` and + # `reply_to_message_id`: + # + # reply_with :message, text: 'Hello!' + # reply_with :message, text: '__Hello!__', parse_mode: :Markdown + # reply_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!" def reply_with(type, params) method = "send_#{type}" + chat = self.chat + payload = self.payload params = params.merge( - chat_id: chat['id'], - reply_to_message: payload['message_id'], + chat_id: (chat && chat['id'] or raise 'Can not reply_with when chat is not present'), + reply_to_message: payload && payload['message_id'], ) bot.public_send(method, params) end + # Same as reply_with, but for inline queries. + def answer_inline_query(results, params = {}) + params = params.merge( + inline_query_id: payload['id'], + results: results, + ) + bot.answer_inline_query(params) + end + ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self) end end diff --git a/lib/telegram/bot/updates_controller/rspec_helpers.rb b/lib/telegram/bot/updates_controller/rspec_helpers.rb index e2ffeea..1387119 100644 --- a/lib/telegram/bot/updates_controller/rspec_helpers.rb +++ b/lib/telegram/bot/updates_controller/rspec_helpers.rb @@ -7,8 +7,8 @@ RSpec.shared_context 'telegram/bot/updates_controller' do x.extend Telegram::Bot::UpdatesController::Testing end end - let(:update) { {payload_type => payload} } - let(:payload_type) { 'some_type' } + let(:update) { build_update(payload_type, payload) } + let(:payload_type) { :some_type } let(:payload) { double(:payload) } let(:bot) { Telegram::Bot::ClientStub.new(bot_name) } let(:bot_name) { 'bot' } diff --git a/lib/telegram/bot/updates_controller/session.rb b/lib/telegram/bot/updates_controller/session.rb index 47139d3..9bc0268 100644 --- a/lib/telegram/bot/updates_controller/session.rb +++ b/lib/telegram/bot/updates_controller/session.rb @@ -65,6 +65,10 @@ module Telegram def session_store=(store) config.session_store = ActiveSupport::Cache.lookup_store(store) end + + def use_session! + include Session + end end end end diff --git a/spec/telegram/bot/updates_controller/message_context_spec.rb b/spec/telegram/bot/updates_controller/message_context_spec.rb index 1fad69a..38ac962 100644 --- a/spec/telegram/bot/updates_controller/message_context_spec.rb +++ b/spec/telegram/bot/updates_controller/message_context_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do describe '#message' do subject { -> { dispatch } } - let(:payload_type) { 'message' } + let(:payload_type) { :message } let(:payload) { {'text' => text} } let(:text) { 'asd qwe zxc' } diff --git a/spec/telegram/bot/updates_controller_spec.rb b/spec/telegram/bot/updates_controller_spec.rb index eb8a3d2..e136975 100644 --- a/spec/telegram/bot/updates_controller_spec.rb +++ b/spec/telegram/bot/updates_controller_spec.rb @@ -144,17 +144,19 @@ RSpec.describe Telegram::Bot::UpdatesController do end end - describe '#process_action' do - subject { -> { controller.process_action(:action) } } + describe '#process' do + subject { -> { controller.process(:action, *args) } } + let(:args) { [:arg1, :arg2] } context 'when callbacks are defined' do let(:controller_class) do Class.new(described_class) do - before_action :hook + before_action :hook, only: :action attr_reader :acted, :hooked - def action + def action(*args) @acted = true + args end private @@ -167,6 +169,7 @@ RSpec.describe Telegram::Bot::UpdatesController do it { should change(controller, :hooked).to true } it { should change(controller, :acted).to true } + its(:call) { should eq args } context 'when callback returns false' do before do @@ -180,6 +183,102 @@ RSpec.describe Telegram::Bot::UpdatesController do it { should change(controller, :hooked).to true } it { should_not change(controller, :acted).from nil } + its(:call) { should eq false } + end + end + + context 'when initialized without update' do + let(:controller) { controller_class.new(bot, from: from, chat: chat) } + let(:from) { {'id' => 'user_id'} } + let(:chat) { {'id' => 'chat_id'} } + let(:controller_class) do + Class.new(described_class) do + def action(*args) + [from, chat, args] + end + end + end + + its(:call) { should eq [from, chat, args] } + end + end + + describe '#initialize' do + subject { controller } + let(:payload_type) { 'message' } + let(:payload) { deep_stringify(chat: chat, from: from) } + let(:chat) { double(:chat) } + let(:from) { double(:from) } + + def self.with_reinitialize(&block) + instance_eval(&block) + context 'when re-initialized' do + let(:controller) do + described_class.new(double(:other_bot), build_update(:message, + text: 'original message', + from: double(:original_from), + chat: double(:original_chat), + )).tap { |x| x.send(:initialize, bot, update) } + end + instance_eval(&block) + end + end + + context 'when update is given' do + with_reinitialize do + its(:bot) { should eq bot } + its(:update) { should eq update } + its(:payload) { should eq payload } + its(:payload_type) { should eq payload_type } + its(:from) { should eq from } + its(:chat) { should eq chat } + end + end + + context 'when options hash is given' do + let(:update) { {from: from, chat: chat} } + with_reinitialize do + its(:bot) { should eq bot } + its(:update) { should eq nil } + its(:payload) { should eq nil } + its(:payload_type) { should eq nil } + its(:from) { should eq from } + its(:chat) { should eq chat } + end + end + end + + describe '#reply_with' do + subject { controller.reply_with type, params } + let(:params) { {arg: 1, 'other_arg' => 2} } + let(:type) { :photo } + let(:result) { double(:result) } + let(:payload_type) { :message } + let(:payload) { {message_id: double(:message_id)} } + let(:chat) { {'id' => double(:chat_id)} } + + it 'sets chat_id & reply_to_message' do + expect(controller).to receive(:chat) { chat } + expect(bot).to receive("send_#{type}").with(params.merge( + chat_id: chat['id'], + reply_to_message: payload[:message_id], + )) { result } + should eq result + end + + context 'when chat is missing' do + let(:payload_type) { :some_type } + it { expect { subject }.to raise_error(/chat/) } + end + + context 'when update is not set' do + let(:update) { {chat: chat} } + it 'sets chat_id & reply_to_message' do + expect(bot).to receive("send_#{type}").with(params.merge( + chat_id: chat['id'], + reply_to_message: nil, + )) { result } + should eq result end end end