зеркало из
https://github.com/glebtv/telegram-bot.git
synced 2026-09-07 19:35:52 +03:00
Easily run UpdatesController's actions without updates
Этот коммит содержится в:
@@ -8,9 +8,11 @@ Style/AlignParameters:
|
|||||||
# EnforcedStyle:
|
# EnforcedStyle:
|
||||||
# - with_first_parameter
|
# - with_first_parameter
|
||||||
# - with_fixed_indentation
|
# - with_fixed_indentation
|
||||||
|
Style/AndOr: {EnforcedStyle: conditionals}
|
||||||
Style/ClosingParenthesisIndentation: {Enabled: false}
|
Style/ClosingParenthesisIndentation: {Enabled: false}
|
||||||
Style/Documentation: {Enabled: false}
|
Style/Documentation: {Enabled: false}
|
||||||
Style/DotPosition: {EnforcedStyle: trailing}
|
Style/DotPosition: {EnforcedStyle: trailing}
|
||||||
|
Style/FirstParameterIndentation: {EnforcedStyle: consistent}
|
||||||
Style/IfUnlessModifier: {Enabled: false}
|
Style/IfUnlessModifier: {Enabled: false}
|
||||||
Style/ModuleFunction: {Enabled: false}
|
Style/ModuleFunction: {Enabled: false}
|
||||||
Style/MultilineOperationIndentation: {EnforcedStyle: indented}
|
Style/MultilineOperationIndentation: {EnforcedStyle: indented}
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -158,6 +158,9 @@ config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.
|
|||||||
|
|
||||||
class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
||||||
include Telegram::Bot::UpdatesController::Session
|
include Telegram::Bot::UpdatesController::Session
|
||||||
|
# or just shortcut:
|
||||||
|
use_session!
|
||||||
|
|
||||||
# You can override global config
|
# You can override global config
|
||||||
self.session_store = :file_store
|
self.session_store = :file_store
|
||||||
|
|
||||||
@@ -219,6 +222,20 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
|||||||
end
|
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
|
### Routes
|
||||||
|
|
||||||
Use `telegram_webhooks` helper to add routes. It will create routes for bots
|
Use `telegram_webhooks` helper to add routes. It will create routes for bots
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ module Telegram
|
|||||||
end
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
"#<Telegram::Bot::Client##{object_id}(#{@username})>"
|
"#<#{self.class.name}##{object_id}(#{@username})>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,52 @@ require 'active_support/version'
|
|||||||
|
|
||||||
module Telegram
|
module Telegram
|
||||||
module Bot
|
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
|
class UpdatesController < AbstractController::Base
|
||||||
abstract!
|
abstract!
|
||||||
|
|
||||||
@@ -38,6 +84,7 @@ module Telegram
|
|||||||
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
|
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
# Initialize controller and process update.
|
||||||
def dispatch(*args)
|
def dispatch(*args)
|
||||||
new(*args).dispatch
|
new(*args).dispatch
|
||||||
end
|
end
|
||||||
@@ -68,18 +115,39 @@ module Telegram
|
|||||||
alias_method :command?, :is_command
|
alias_method :command?, :is_command
|
||||||
delegate :username, to: :bot, prefix: true, allow_nil: true
|
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)
|
def initialize(bot = nil, update = nil)
|
||||||
|
if update.is_a?(Hash) && (update.key?(:from) || update.key?(:chat))
|
||||||
|
options = update
|
||||||
|
update = nil
|
||||||
|
end
|
||||||
@_update = update
|
@_update = update
|
||||||
@_bot = bot
|
@_bot = bot
|
||||||
|
@_chat, @_from = options && options.values_at(:chat, :from)
|
||||||
|
|
||||||
|
payload_data = nil
|
||||||
update && PAYLOAD_TYPES.find do |type|
|
update && PAYLOAD_TYPES.find do |type|
|
||||||
item = update[type]
|
item = update[type]
|
||||||
next unless item
|
payload_data = [item, type] if item
|
||||||
@_payload = item
|
|
||||||
@_payload_type = type
|
|
||||||
end
|
end
|
||||||
|
@_payload, @_payload_type = payload_data
|
||||||
end
|
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
|
def dispatch
|
||||||
@_is_command, action, args = action_for_payload
|
@_is_command, action, args = action_for_payload
|
||||||
process(action, *args)
|
process(action, *args)
|
||||||
@@ -103,19 +171,32 @@ module Telegram
|
|||||||
def action_missing(*)
|
def action_missing(*)
|
||||||
end
|
end
|
||||||
|
|
||||||
%w(chat from).each do |field|
|
# Helper to call bot's `send_#{type}` method with already set `chat_id` and
|
||||||
define_method(field) { payload[field] }
|
# `reply_to_message_id`:
|
||||||
end
|
#
|
||||||
|
# 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)
|
def reply_with(type, params)
|
||||||
method = "send_#{type}"
|
method = "send_#{type}"
|
||||||
|
chat = self.chat
|
||||||
|
payload = self.payload
|
||||||
params = params.merge(
|
params = params.merge(
|
||||||
chat_id: chat['id'],
|
chat_id: (chat && chat['id'] or raise 'Can not reply_with when chat is not present'),
|
||||||
reply_to_message: payload['message_id'],
|
reply_to_message: payload && payload['message_id'],
|
||||||
)
|
)
|
||||||
bot.public_send(method, params)
|
bot.public_send(method, params)
|
||||||
end
|
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)
|
ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
|
|||||||
x.extend Telegram::Bot::UpdatesController::Testing
|
x.extend Telegram::Bot::UpdatesController::Testing
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
let(:update) { {payload_type => payload} }
|
let(:update) { build_update(payload_type, payload) }
|
||||||
let(:payload_type) { 'some_type' }
|
let(:payload_type) { :some_type }
|
||||||
let(:payload) { double(:payload) }
|
let(:payload) { double(:payload) }
|
||||||
let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
|
let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
|
||||||
let(:bot_name) { 'bot' }
|
let(:bot_name) { 'bot' }
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ module Telegram
|
|||||||
def session_store=(store)
|
def session_store=(store)
|
||||||
config.session_store = ActiveSupport::Cache.lookup_store(store)
|
config.session_store = ActiveSupport::Cache.lookup_store(store)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_session!
|
||||||
|
include Session
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
|
|||||||
|
|
||||||
describe '#message' do
|
describe '#message' do
|
||||||
subject { -> { dispatch } }
|
subject { -> { dispatch } }
|
||||||
let(:payload_type) { 'message' }
|
let(:payload_type) { :message }
|
||||||
let(:payload) { {'text' => text} }
|
let(:payload) { {'text' => text} }
|
||||||
let(:text) { 'asd qwe zxc' }
|
let(:text) { 'asd qwe zxc' }
|
||||||
|
|
||||||
|
|||||||
@@ -144,17 +144,19 @@ RSpec.describe Telegram::Bot::UpdatesController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#process_action' do
|
describe '#process' do
|
||||||
subject { -> { controller.process_action(:action) } }
|
subject { -> { controller.process(:action, *args) } }
|
||||||
|
let(:args) { [:arg1, :arg2] }
|
||||||
|
|
||||||
context 'when callbacks are defined' do
|
context 'when callbacks are defined' do
|
||||||
let(:controller_class) do
|
let(:controller_class) do
|
||||||
Class.new(described_class) do
|
Class.new(described_class) do
|
||||||
before_action :hook
|
before_action :hook, only: :action
|
||||||
attr_reader :acted, :hooked
|
attr_reader :acted, :hooked
|
||||||
|
|
||||||
def action
|
def action(*args)
|
||||||
@acted = true
|
@acted = true
|
||||||
|
args
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -167,6 +169,7 @@ RSpec.describe Telegram::Bot::UpdatesController do
|
|||||||
|
|
||||||
it { should change(controller, :hooked).to true }
|
it { should change(controller, :hooked).to true }
|
||||||
it { should change(controller, :acted).to true }
|
it { should change(controller, :acted).to true }
|
||||||
|
its(:call) { should eq args }
|
||||||
|
|
||||||
context 'when callback returns false' do
|
context 'when callback returns false' do
|
||||||
before do
|
before do
|
||||||
@@ -180,6 +183,102 @@ RSpec.describe Telegram::Bot::UpdatesController do
|
|||||||
|
|
||||||
it { should change(controller, :hooked).to true }
|
it { should change(controller, :hooked).to true }
|
||||||
it { should_not change(controller, :acted).from nil }
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user