зеркало из
https://github.com/glebtv/telegram-bot.git
synced 2026-08-28 15:26:18 +03:00
Сравнить коммиты
10 Коммитов
v0.9.0.alp
...
v0.9.0
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
ec1d5bad7d | ||
|
|
194fc78202 | ||
|
|
ececfa660e | ||
|
|
9e67122465 | ||
|
|
caee9809d0 | ||
|
|
4b511f51a7 | ||
|
|
d2d6202fa7 | ||
|
|
dd38cfea93 | ||
|
|
3cb6e930b8 | ||
|
|
e42a293804 |
@@ -1,6 +1,12 @@
|
||||
# Unreleased
|
||||
# 0.9.0
|
||||
|
||||
- Async API requests.
|
||||
- One more description for StaleChat error.
|
||||
- edit_message_* methods.
|
||||
- API methods from 2016-10-03 update
|
||||
- Fix typo in module name: CallbackQueyContext -> CallbackQueryContext.
|
||||
- Take `chat` from `message` for callback queries
|
||||
- RSpec matchers.
|
||||
|
||||
# 0.8.0
|
||||
|
||||
|
||||
40
README.md
40
README.md
@@ -17,7 +17,8 @@ Package contains:
|
||||
- Middleware and routes helpers for production env.
|
||||
- Poller with automatic source-reloader for development env.
|
||||
- Rake tasks to update webhook urls.
|
||||
- Async requests for Telegram and/or Botan API. Let the queue adapter handle errors!
|
||||
- __[Async mode](#async-mode)__ for Telegram and/or Botan API.
|
||||
Let the queue adapter handle network errors!
|
||||
|
||||
Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app)
|
||||
with session, keyboards and inline queries.
|
||||
@@ -132,6 +133,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
||||
# do_smth_with(data)
|
||||
|
||||
# There are `chat` & `from` shortcut methods.
|
||||
# For callback queries `chat` if taken from `message` when it's available.
|
||||
response = from ? "Hello #{from['username']}!" : 'Hi there!'
|
||||
# There is `respond_with` helper to set `chat_id` from received message:
|
||||
respond_with :message, text: response
|
||||
@@ -246,7 +248,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
||||
end
|
||||
```
|
||||
|
||||
You can use `CallbackQueyContext` in the similar way to split `#callback_query` into
|
||||
You can use `CallbackQueryContext` in the similar way to split `#callback_query` into
|
||||
several specific methods. It doesn't require session support, and takes context from
|
||||
data. If data has a prefix with colon like this `my_ctx:smth...` it'll call
|
||||
`my_ctx_callback_query('smth...')` when there is such action method. Otherwise
|
||||
@@ -334,6 +336,23 @@ There are also some helpers for controller tests.
|
||||
Check out `telegram/bot/updates_controller/rspec_helpers` and
|
||||
`telegram/bot/updates_controller/testing`.
|
||||
|
||||
Built-in RSpec matchers will help you to write tests fast:
|
||||
|
||||
```ruby
|
||||
include Telegram::Bot::RSpec::ClientMatchers # no need if you already use controller herlpers
|
||||
|
||||
expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option)
|
||||
expect(&process_update).
|
||||
to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
|
||||
|
||||
# controller specs are even simplier:
|
||||
describe '#start' do
|
||||
subject { -> { dispatch_message '/start' } }
|
||||
it { should respond_with_message(/Hello/) }
|
||||
end
|
||||
# See sample app for more examples.
|
||||
```
|
||||
|
||||
### Deploying
|
||||
|
||||
Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots.
|
||||
@@ -384,11 +403,28 @@ you can implement your own worker class to handle such requests. This allows:
|
||||
- Handle and retry network and other errors with queue adapter.
|
||||
- ???
|
||||
|
||||
Instead of performing request instantly client serializes it, pushes to queue,
|
||||
and immediately return control back. The job is then fetched with a worker
|
||||
and real API request is performed. And this all is absolutely transparent for the app.
|
||||
|
||||
To enable this mode add `async: true` to bot's and botan's config.
|
||||
For more information and custom configuration check out
|
||||
[docs](http://www.rubydoc.info/github/telegram-bot-rb/telegram-bot/master/Telegram/Bot/Async) or
|
||||
[source](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/async.rb).
|
||||
|
||||
If you want async mode, but don't want to setup queue, know that Rails 5 are shipped
|
||||
with Async adapter by default, and there is
|
||||
[Sucker Punch](https://github.com/brandonhilkert/sucker_punch) for Rails 4.
|
||||
|
||||
Be aware of some limitations:
|
||||
|
||||
- Client will not return API response.
|
||||
- Sending files is not available in async mode [now],
|
||||
because them can not be serialized.
|
||||
|
||||
To disable async mode for the block of code use `bot.async(false) { bot.send_photo }`.
|
||||
Yes, it's threadsafe too.
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies.
|
||||
|
||||
@@ -34,6 +34,7 @@ module Telegram
|
||||
autoload :DebugClient, 'telegram/bot/debug_client'
|
||||
autoload :Initializers, 'telegram/bot/initializers'
|
||||
autoload :Middleware, 'telegram/bot/middleware'
|
||||
autoload :RSpec, 'telegram/bot/rspec'
|
||||
autoload :UpdatesController, 'telegram/bot/updates_controller'
|
||||
autoload :UpdatesPoller, 'telegram/bot/updates_poller'
|
||||
end
|
||||
|
||||
@@ -63,21 +63,27 @@ module Telegram
|
||||
%w(
|
||||
answerCallbackQuery
|
||||
answerInlineQuery
|
||||
editMessageCaption
|
||||
editMessageReplyMarkup
|
||||
editMessageText
|
||||
forwardMessage
|
||||
getChat
|
||||
getChatAdministrators
|
||||
getChatMember
|
||||
getChatMembersCount
|
||||
getFile
|
||||
getGameHighScores
|
||||
getMe
|
||||
getUpdates
|
||||
getUserProfilePhotos
|
||||
getWebhookInfo
|
||||
kickChatMember
|
||||
leaveChat
|
||||
sendAudio
|
||||
sendChatAction
|
||||
sendContact
|
||||
sendDocument
|
||||
sendGame
|
||||
sendLocation
|
||||
sendMessage
|
||||
sendPhoto
|
||||
@@ -85,6 +91,7 @@ module Telegram
|
||||
sendVenue
|
||||
sendVideo
|
||||
sendVoice
|
||||
setGameScore
|
||||
setWebhook
|
||||
unbanChatMember
|
||||
).each do |method|
|
||||
|
||||
@@ -43,7 +43,7 @@ module Telegram
|
||||
@requests = Hash.new { |h, k| h[k] = [] }
|
||||
end
|
||||
|
||||
def request(action, body)
|
||||
def request(action, body = {})
|
||||
requests[action.to_sym] << body
|
||||
end
|
||||
end
|
||||
|
||||
7
lib/telegram/bot/rspec.rb
Обычный файл
7
lib/telegram/bot/rspec.rb
Обычный файл
@@ -0,0 +1,7 @@
|
||||
module Telegram
|
||||
module Bot
|
||||
module RSpec
|
||||
autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers'
|
||||
end
|
||||
end
|
||||
end
|
||||
151
lib/telegram/bot/rspec/client_matchers.rb
Обычный файл
151
lib/telegram/bot/rspec/client_matchers.rb
Обычный файл
@@ -0,0 +1,151 @@
|
||||
module Telegram
|
||||
module Bot
|
||||
module RSpec
|
||||
# Proxy that uses RSpec::Mocks::ArgListMatcher when it's available.
|
||||
# Otherwise just performs `#==` match.
|
||||
#
|
||||
# Also allows to check argumets with custom block.
|
||||
class ArgListMatcher
|
||||
attr_reader :expected, :expected_proc
|
||||
|
||||
def initialize(*args, &block)
|
||||
@expected_proc = block if block_given?
|
||||
@expected =
|
||||
if mocks_matcher?
|
||||
::RSpec::Mocks::ArgumentListMatcher.new(*args)
|
||||
else
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
def args_match?(*actual)
|
||||
if expected_proc
|
||||
expected_proc[*actual]
|
||||
true
|
||||
elsif mocks_matcher?
|
||||
expected.args_match?(*actual)
|
||||
else
|
||||
expected == actual
|
||||
end
|
||||
end
|
||||
|
||||
def args
|
||||
mocks_matcher? ? expected.args : expected
|
||||
end
|
||||
|
||||
def mocks_matcher?
|
||||
defined?(::RSpec::Mocks::ArgumentListMatcher)
|
||||
end
|
||||
|
||||
def to_s
|
||||
if mocks_matcher?
|
||||
expected.expected_args.inspect
|
||||
elsif expected_proc
|
||||
'(proc matcher)'
|
||||
else
|
||||
expected.inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Matchers to test requests to Telegram API.
|
||||
#
|
||||
# Complex matchers requires `rspec-mocks` to be installed.
|
||||
module ClientMatchers
|
||||
class MakeTelegramRequest < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
||||
EXPECTATION_TYPES = {
|
||||
exactly: :==,
|
||||
at_most: :>=,
|
||||
at_least: :<=,
|
||||
}.freeze
|
||||
|
||||
attr_reader :performed_requests, :description
|
||||
|
||||
def initialize(bot, action, description: nil)
|
||||
@bot = bot
|
||||
@action = action
|
||||
@description = description || "make #{action} telegram request"
|
||||
exactly(1)
|
||||
end
|
||||
|
||||
def matches?(proc) # rubocop:disable AbcSize
|
||||
raise ArgumentError, 'matcher only supports block expectations' unless proc.is_a?(Proc)
|
||||
original_requests_count = bot.requests[action].count
|
||||
proc.call
|
||||
@performed_requests = bot.requests[action].drop(original_requests_count)
|
||||
@matching_requests_count = performed_requests.count do |request|
|
||||
!arg_list_matcher || arg_list_matcher.args_match?(request)
|
||||
end
|
||||
expectation_method = EXPECTATION_TYPES[expectation_type]
|
||||
expected_number.public_send(expectation_method, matching_requests_count)
|
||||
end
|
||||
|
||||
def with(*args, &block)
|
||||
@arg_list_matcher = ArgListMatcher.new(*args, &block)
|
||||
self
|
||||
end
|
||||
|
||||
EXPECTATION_TYPES.each_key do |type|
|
||||
define_method type do |count|
|
||||
@expectation_type = type
|
||||
@expected_number = Integer(count)
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
def times
|
||||
self
|
||||
end
|
||||
|
||||
def failure_message
|
||||
"expected to #{base_message}"
|
||||
end
|
||||
|
||||
def failure_message_when_negated
|
||||
"expected not to #{base_message}"
|
||||
end
|
||||
|
||||
def supports_block_expectations?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :bot, :action, :expectation_type, :expected_number,
|
||||
:arg_list_matcher, :matching_requests_count
|
||||
|
||||
def base_message
|
||||
"make #{expectation_type.to_s.tr('_', ' ')} #{expected_number} " \
|
||||
"#{bot.inspect}.#{action} requests,".tap do |msg|
|
||||
msg << " with #{arg_list_matcher}," if arg_list_matcher
|
||||
msg << " but made #{matching_requests_count}"
|
||||
if performed_requests
|
||||
actual_args = performed_requests.map(&:inspect).join(', ')
|
||||
msg << ", and #{performed_requests.count} with #{actual_args}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check that bot performed request to telegram API:
|
||||
#
|
||||
# expect { dispatch_message('Hi!') }.
|
||||
# to make_telegram_request(bot, :sendMessage).
|
||||
# with(text: 'Hello!', chat_id: chat_id)
|
||||
def make_telegram_request(bot, action)
|
||||
MakeTelegramRequest.new(bot, action)
|
||||
end
|
||||
|
||||
# Helper for asserting message is sent. Note that options are checked
|
||||
# with `hash_including`. For strict checks use #make_telegram_request.
|
||||
def send_telegram_message(bot, text = nil, options = {})
|
||||
text = a_string_matching(text) if text.is_a?(Regexp)
|
||||
options = options.merge(text: text) if text
|
||||
description = "send telegram message #{text.inspect}"
|
||||
MakeTelegramRequest.new(bot, :sendMessage, description: description).
|
||||
with(hash_including(options))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,14 +50,14 @@ module Telegram
|
||||
# ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
|
||||
# process(:help, *args)
|
||||
#
|
||||
class UpdatesController < AbstractController::Base # rubocop:disable ClassLength
|
||||
class UpdatesController < AbstractController::Base
|
||||
abstract!
|
||||
|
||||
require 'telegram/bot/updates_controller/session'
|
||||
require 'telegram/bot/updates_controller/log_subscriber'
|
||||
require 'telegram/bot/updates_controller/instrumentation'
|
||||
require 'telegram/bot/updates_controller/reply_helpers'
|
||||
autoload :CallbackQueyContext, 'telegram/bot/updates_controller/callback_query_context'
|
||||
autoload :CallbackQueryContext, 'telegram/bot/updates_controller/callback_query_context'
|
||||
autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
|
||||
|
||||
include AbstractController::Callbacks
|
||||
@@ -140,10 +140,12 @@ module Telegram
|
||||
@_payload, @_payload_type = payload_data
|
||||
end
|
||||
|
||||
# Accessor to `'chat'` field of payload. Can be overriden with `chat` option
|
||||
# for #initialize.
|
||||
# Accessor to `'chat'` field of payload. Also tries `'chat'` in `'message'`
|
||||
# when there is no such field in payload.
|
||||
#
|
||||
# Can be overriden with `chat` option for #initialize.
|
||||
def chat
|
||||
@_chat ||= payload && payload['chat']
|
||||
@_chat ||= payload.try! { |x| x['chat'] || x['message'] && x['message']['chat'] }
|
||||
end
|
||||
|
||||
# Accessor to `'from'` field of payload. Can be overriden with `from` option
|
||||
|
||||
@@ -3,7 +3,7 @@ module Telegram
|
||||
class UpdatesController
|
||||
# Use separate actions for different callback queries.
|
||||
# It doesn't require session support. Simply add `%{context}:` prefix to data.
|
||||
module CallbackQueyContext
|
||||
module CallbackQueryContext
|
||||
protected
|
||||
|
||||
# Uses #context_from_callback_query as context name.
|
||||
|
||||
@@ -14,11 +14,14 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
|
||||
let(:bot_name) { 'bot' }
|
||||
let(:session) { controller.send(:session) }
|
||||
|
||||
include Telegram::Bot::RSpec::ClientMatchers
|
||||
|
||||
def dispatch(bot = self.bot, update = self.update)
|
||||
controller.dispatch_again(bot, update)
|
||||
end
|
||||
|
||||
def dispatch_message(text, options = {})
|
||||
def dispatch_message(text, options = nil)
|
||||
options ||= respond_to?(:default_message_options) ? default_message_options : {}
|
||||
update = build_update :message, options.merge(text: text)
|
||||
dispatch bot, update
|
||||
end
|
||||
@@ -34,4 +37,9 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
|
||||
else input
|
||||
end
|
||||
end
|
||||
|
||||
# Matcher to check response. Make sure to define `let(:chat_id)`.
|
||||
def respond_with_message(expected)
|
||||
send_telegram_message(bot, expected, chat_id: chat_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Telegram
|
||||
module Bot
|
||||
VERSION = '0.9.0.alpha2'.freeze
|
||||
VERSION = '0.9.0'.freeze
|
||||
|
||||
def self.gem_version
|
||||
Gem::Version.new VERSION
|
||||
|
||||
87
spec/telegram/bot/rspec/client_matchers_spec.rb
Обычный файл
87
spec/telegram/bot/rspec/client_matchers_spec.rb
Обычный файл
@@ -0,0 +1,87 @@
|
||||
RSpec.describe Telegram::Bot::RSpec::ClientMatchers do
|
||||
let(:bot) { Telegram::Bot::ClientStub.new('TestBot') }
|
||||
let(:other_bot) { Telegram::Bot::ClientStub.new('OtherTestBot') }
|
||||
|
||||
include described_class
|
||||
|
||||
describe '#make_telegram_request' do
|
||||
context 'without args' do
|
||||
it 'works for matching' do
|
||||
block = ->(*) { bot.send_message(text: 'test') }
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage)
|
||||
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).exactly(3).times
|
||||
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).at_least(2).times
|
||||
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).at_most(4).times
|
||||
end
|
||||
|
||||
it 'works for not-matching' do
|
||||
block = ->(*) { bot.get_me }
|
||||
expect {}.to_not make_telegram_request(bot, :getMe)
|
||||
expect { other_bot.get_me }.to_not make_telegram_request(bot, :getMe)
|
||||
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe)
|
||||
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).exactly(2).times
|
||||
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).exactly(4).times
|
||||
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).at_least(4).times
|
||||
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).at_most(2).times
|
||||
end
|
||||
end
|
||||
|
||||
context 'with args' do
|
||||
it 'works for exact matching' do
|
||||
args = {text: 'test', parseMode: :Markdown}
|
||||
block = ->(*) { bot.send_message(args) }
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage).with(args)
|
||||
expect(&block).to_not make_telegram_request(bot, :sendMessage).with(args.except(:text))
|
||||
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).
|
||||
with(args).exactly(3).times
|
||||
end
|
||||
|
||||
it 'works for block matchers' do
|
||||
args = {text: 'test', parseMode: :Markdown}
|
||||
block = ->(*) { bot.send_message(args) }
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage).
|
||||
with { |actual| expect(actual).to eq(args) }
|
||||
# It ignores block's result! Custom expectations must be used inside block.
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage).
|
||||
with { |actual| actual == {} }
|
||||
expect do
|
||||
expect(&block).to_not make_telegram_request(bot, :sendMessage).
|
||||
with { |actual| expect(actual).to eq({}) }
|
||||
end.to raise_error RSpec::Expectations::ExpectationNotMetError
|
||||
n = -1
|
||||
expect { 3.times { |i| bot.send_message text: i } }.
|
||||
to make_telegram_request(bot, :sendMessage).exactly(3).times.
|
||||
with { |actual| expect(actual).to eq(text: n += 1) }
|
||||
end
|
||||
|
||||
it 'works for RSpec::Mocks matchers' do
|
||||
args = {text: 'test', parseMode: :Markdown}
|
||||
block = ->(*) { bot.send_message(args) }
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage).with(hash_including(args))
|
||||
expect(&block).to make_telegram_request(bot, :sendMessage).with(
|
||||
text: a_string_matching(/est$/),
|
||||
parseMode: :Markdown,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_telegram_message' do
|
||||
let(:args) { {text: 'test', parseMode: :Markdown} }
|
||||
let(:block) { ->(*) { bot.send_message(args) } }
|
||||
|
||||
it 'works for matching' do
|
||||
expect(&block).to send_telegram_message(bot, 'test')
|
||||
expect(&block).to send_telegram_message(bot, /^tes/)
|
||||
expect(&block).to send_telegram_message(bot, 'test', parseMode: :Markdown)
|
||||
expect(&block).to send_telegram_message(bot, a_string_including('es'))
|
||||
end
|
||||
|
||||
it 'works for not-matching' do
|
||||
expect(&block).to_not send_telegram_message(bot, 'test!')
|
||||
expect(&block).to_not send_telegram_message(bot, /tes$/)
|
||||
expect(&block).to_not send_telegram_message(bot, 'test', parseMode: :HTML)
|
||||
expect(&block).to_not send_telegram_message(bot, a_string_including('smth'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
RSpec.describe Telegram::Bot::UpdatesController::CallbackQueyContext do
|
||||
RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
|
||||
include_context 'telegram/bot/updates_controller'
|
||||
let(:controller_class) do
|
||||
described_class = self.described_class
|
||||
|
||||
@@ -293,4 +293,49 @@ RSpec.describe Telegram::Bot::UpdatesController do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#chat' do
|
||||
subject { controller.chat }
|
||||
let(:payload_type) { :message }
|
||||
let(:payload) { {chat: 'test_value'} }
|
||||
it { should eq payload[:chat] }
|
||||
|
||||
context 'when payload is not set' do
|
||||
let(:payload) {}
|
||||
it { should eq nil }
|
||||
end
|
||||
|
||||
context 'when payload has no such field' do
|
||||
let(:payload) { {smth: 'other'} }
|
||||
it { should eq nil }
|
||||
|
||||
context 'but has `message`' do
|
||||
let(:payload) { {message: message} }
|
||||
let(:message) { {text: 'Hello bot!'} }
|
||||
it { should eq nil }
|
||||
|
||||
context 'with `chat` set' do
|
||||
let(:message) { super().merge(chat: 'test value') }
|
||||
it { should eq message[:chat] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#from' do
|
||||
subject { controller.from }
|
||||
let(:payload_type) { :message }
|
||||
let(:payload) { {from: 'test_value'} }
|
||||
it { should eq payload[:from] }
|
||||
|
||||
context 'when payload is not set' do
|
||||
let(:payload) {}
|
||||
it { should eq nil }
|
||||
end
|
||||
|
||||
context 'when payload has no such field' do
|
||||
let(:payload) { {smth: 'other'} }
|
||||
it { should eq nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Ссылка в новой задаче
Block a user