1
0
зеркало из https://github.com/glebtv/telegram-bot.git synced 2026-09-03 09:46:17 +03:00

Сравнить коммиты

...

10 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
ec1d5bad7d v0.9.0 2016-11-07 20:24:01 +03:00
Max Melentiev
194fc78202 RSpec matchers 2016-11-07 20:17:34 +03:00
Max Melentiev
ececfa660e [Docs] Note about queue adapters 2016-10-19 14:20:39 +03:00
printercu
9e67122465 Merge pull request #2 from telegram-bot-rb/async
Async requests
2016-10-19 14:00:26 +03:00
Max Melentiev
caee9809d0 More docs on async mode 2016-10-19 13:59:54 +03:00
Max Melentiev
4b511f51a7 update changelog 2016-10-19 13:36:03 +03:00
Max Melentiev
d2d6202fa7 API methods from 2016-10-03 update
https://core.telegram.org/bots/api-changelog#october-3-2016
2016-10-17 17:43:13 +03:00
Max Melentiev
dd38cfea93 Take chat from message for callback queries 2016-10-07 12:45:16 +03:00
Max Melentiev
3cb6e930b8 Fix typo in module name: CallbackQueyContext -> CallbackQueryContext 2016-09-22 01:12:19 +03:00
Max Melentiev
e42a293804 edit_message_* methods 2016-09-21 22:53:06 +03:00
14 изменённых файлов: 363 добавлений и 13 удалений

Просмотреть файл

@@ -1,6 +1,12 @@
# Unreleased # 0.9.0
- Async API requests.
- One more description for StaleChat error. - 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 # 0.8.0

Просмотреть файл

@@ -17,7 +17,8 @@ Package contains:
- Middleware and routes helpers for production env. - Middleware and routes helpers for production env.
- Poller with automatic source-reloader for development env. - Poller with automatic source-reloader for development env.
- Rake tasks to update webhook urls. - 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) Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app)
with session, keyboards and inline queries. with session, keyboards and inline queries.
@@ -132,6 +133,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# do_smth_with(data) # do_smth_with(data)
# There are `chat` & `from` shortcut methods. # 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!' response = from ? "Hello #{from['username']}!" : 'Hi there!'
# There is `respond_with` helper to set `chat_id` from received message: # There is `respond_with` helper to set `chat_id` from received message:
respond_with :message, text: response respond_with :message, text: response
@@ -246,7 +248,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end 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 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 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 `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 Check out `telegram/bot/updates_controller/rspec_helpers` and
`telegram/bot/updates_controller/testing`. `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 ### Deploying
Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots. 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. - 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. To enable this mode add `async: true` to bot's and botan's config.
For more information and custom configuration check out For more information and custom configuration check out
[docs](http://www.rubydoc.info/github/telegram-bot-rb/telegram-bot/master/Telegram/Bot/Async) or [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). [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 ## Development
After checking out the repo, run `bin/setup` to install dependencies. After checking out the repo, run `bin/setup` to install dependencies.

Просмотреть файл

@@ -34,6 +34,7 @@ module Telegram
autoload :DebugClient, 'telegram/bot/debug_client' autoload :DebugClient, 'telegram/bot/debug_client'
autoload :Initializers, 'telegram/bot/initializers' autoload :Initializers, 'telegram/bot/initializers'
autoload :Middleware, 'telegram/bot/middleware' autoload :Middleware, 'telegram/bot/middleware'
autoload :RSpec, 'telegram/bot/rspec'
autoload :UpdatesController, 'telegram/bot/updates_controller' autoload :UpdatesController, 'telegram/bot/updates_controller'
autoload :UpdatesPoller, 'telegram/bot/updates_poller' autoload :UpdatesPoller, 'telegram/bot/updates_poller'
end end

Просмотреть файл

@@ -63,21 +63,27 @@ module Telegram
%w( %w(
answerCallbackQuery answerCallbackQuery
answerInlineQuery answerInlineQuery
editMessageCaption
editMessageReplyMarkup
editMessageText
forwardMessage forwardMessage
getChat getChat
getChatAdministrators getChatAdministrators
getChatMember getChatMember
getChatMembersCount getChatMembersCount
getFile getFile
getGameHighScores
getMe getMe
getUpdates getUpdates
getUserProfilePhotos getUserProfilePhotos
getWebhookInfo
kickChatMember kickChatMember
leaveChat leaveChat
sendAudio sendAudio
sendChatAction sendChatAction
sendContact sendContact
sendDocument sendDocument
sendGame
sendLocation sendLocation
sendMessage sendMessage
sendPhoto sendPhoto
@@ -85,6 +91,7 @@ module Telegram
sendVenue sendVenue
sendVideo sendVideo
sendVoice sendVoice
setGameScore
setWebhook setWebhook
unbanChatMember unbanChatMember
).each do |method| ).each do |method|

Просмотреть файл

@@ -43,7 +43,7 @@ module Telegram
@requests = Hash.new { |h, k| h[k] = [] } @requests = Hash.new { |h, k| h[k] = [] }
end end
def request(action, body) def request(action, body = {})
requests[action.to_sym] << body requests[action.to_sym] << body
end end
end end

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 Обычный файл
Просмотреть файл

@@ -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). # ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
# process(:help, *args) # process(:help, *args)
# #
class UpdatesController < AbstractController::Base # rubocop:disable ClassLength class UpdatesController < AbstractController::Base
abstract! abstract!
require 'telegram/bot/updates_controller/session' require 'telegram/bot/updates_controller/session'
require 'telegram/bot/updates_controller/log_subscriber' require 'telegram/bot/updates_controller/log_subscriber'
require 'telegram/bot/updates_controller/instrumentation' require 'telegram/bot/updates_controller/instrumentation'
require 'telegram/bot/updates_controller/reply_helpers' 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' autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
include AbstractController::Callbacks include AbstractController::Callbacks
@@ -140,10 +140,12 @@ module Telegram
@_payload, @_payload_type = payload_data @_payload, @_payload_type = payload_data
end end
# Accessor to `'chat'` field of payload. Can be overriden with `chat` option # Accessor to `'chat'` field of payload. Also tries `'chat'` in `'message'`
# for #initialize. # when there is no such field in payload.
#
# Can be overriden with `chat` option for #initialize.
def chat def chat
@_chat ||= payload && payload['chat'] @_chat ||= payload.try! { |x| x['chat'] || x['message'] && x['message']['chat'] }
end end
# Accessor to `'from'` field of payload. Can be overriden with `from` option # Accessor to `'from'` field of payload. Can be overriden with `from` option

Просмотреть файл

@@ -3,7 +3,7 @@ module Telegram
class UpdatesController class UpdatesController
# Use separate actions for different callback queries. # Use separate actions for different callback queries.
# It doesn't require session support. Simply add `%{context}:` prefix to data. # It doesn't require session support. Simply add `%{context}:` prefix to data.
module CallbackQueyContext module CallbackQueryContext
protected protected
# Uses #context_from_callback_query as context name. # 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(:bot_name) { 'bot' }
let(:session) { controller.send(:session) } let(:session) { controller.send(:session) }
include Telegram::Bot::RSpec::ClientMatchers
def dispatch(bot = self.bot, update = self.update) def dispatch(bot = self.bot, update = self.update)
controller.dispatch_again(bot, update) controller.dispatch_again(bot, update)
end 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) update = build_update :message, options.merge(text: text)
dispatch bot, update dispatch bot, update
end end
@@ -34,4 +37,9 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
else input else input
end end
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 end

Просмотреть файл

@@ -1,6 +1,6 @@
module Telegram module Telegram
module Bot module Bot
VERSION = '0.9.0.alpha2'.freeze VERSION = '0.9.0'.freeze
def self.gem_version def self.gem_version
Gem::Version.new VERSION Gem::Version.new VERSION

Просмотреть файл

@@ -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' include_context 'telegram/bot/updates_controller'
let(:controller_class) do let(:controller_class) do
described_class = self.described_class described_class = self.described_class

Просмотреть файл

@@ -293,4 +293,49 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
end 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 end