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

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

..

28 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
da93abaaf7 v0.12.1 2017-10-06 11:23:56 +03:00
printercu
0ae17fb34e Merge pull request #45 from telegram-bot-rb/update_sessions
Update sessions
2017-10-06 11:19:01 +03:00
Max Melentiev
7e41bd4f9b Allow use different sessions for MessageContext 2017-10-06 11:10:05 +03:00
Max Melentiev
e529a79339 Don't use Rails.cache as fallback for session_store
It's NullStore in development by default so session does not work,
and indeed should not be dependent.
2017-10-06 11:07:11 +03:00
Max Melentiev
ef4f3caa5f Make #session raise error when store is not configured
Add more explanation on sessions in readme
2017-10-06 11:05:25 +03:00
Max Melentiev
df7039326d Run set_webhook task always in sync way
Fixes telegram-bot-rb/telegram_bot_app#7
2017-10-05 22:11:22 +03:00
Max Melentiev
4f47506e22 Fix typo in readme 2017-08-11 15:10:05 +03:00
Max Melentiev
6abbe5489d v0.12.0 2017-08-11 15:04:41 +03:00
Max Melentiev
ff7de976d0 Add specs for #edit_message 2017-08-11 15:04:19 +03:00
Max Melentiev
aaba27eb70 New API methods and payload types (up to Bot API 3.2) 2017-08-11 15:04:02 +03:00
printercu
88a4a9aab3 Merge pull request #37 from zuf/master
update rails dependency (<5.2 instead <=5.1)
2017-08-11 13:43:13 +03:00
Egor
9387043ec1 fix rails dependency (<5.2 instead <=5.1) 2017-08-10 18:07:21 +03:00
Max Melentiev
3dca35ba9a callback_query context for RSpec 2017-07-25 10:58:18 +03:00
Max Melentiev
6879618c00 edit_message helper 2017-07-25 10:49:39 +03:00
Max Melentiev
6e4a69b6b1 Fix storing token in ClientStub when config hash is given 2017-05-17 17:45:21 +06:00
printercu
b8db576452 Merge pull request #31 from telegram-bot-rb/integration_test
Integration test with rails app and two bots
2017-05-15 22:20:26 +06:00
Max Melentiev
2fffdeb475 Integration test with rails app and two bots 2017-05-15 22:11:52 +06:00
Max Melentiev
9defa42f40 Add notes on testing multiple bots to readme
Fixes #29
2017-05-11 14:01:42 +06:00
Max Melentiev
0320507abe Telegram.bot raises error when there is no default config 2017-05-11 13:57:29 +06:00
Max Melentiev
cd3d3739a4 v0.11.3 2017-05-05 17:30:49 +06:00
Max Melentiev
a054ac704c Build with rails 5.1 on travis 2017-05-05 17:28:59 +06:00
printercu
23aa92e614 Merge pull request #28 from ironbrains/master
Increase dependencies version
2017-05-05 17:19:05 +06:00
Bulat F
a49dca66fa Increase dependencies version
- Add support `activesupport 5.1`
- Add support `actionpack 5.1`
2017-05-05 10:16:30 +03:00
Max Melentiev
2d5ca981ce Rails 5.1 deep symbolized secrets support
Fixes #26
2017-04-22 12:02:59 +03:00
Max Melentiev
cf9bd87cf2 Add notice about how to require gem to Readme
Fixes #24
2017-03-11 21:33:20 +03:00
Max Melentiev
48ff7a4f43 Fix poller for typed responses
Fixes: #22
2017-03-05 17:56:32 +03:00
Max Melentiev
17260cb5a9 v0.11.0 2017-02-10 09:34:15 +03:00
Max Melentiev
817af95369 Remove Bot::StaleChat in favor of Bot::Forbidden 2017-02-10 09:33:41 +03:00
35 изменённых файлов: 568 добавлений и 111 удалений

1
.gitignore поставляемый
Просмотреть файл

@@ -6,4 +6,5 @@
/doc/ /doc/
/pkg/ /pkg/
/spec/reports/ /spec/reports/
/log/
/tmp/ /tmp/

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

@@ -15,6 +15,7 @@ Style/DotPosition: {EnforcedStyle: trailing}
Style/FirstParameterIndentation: {EnforcedStyle: consistent} Style/FirstParameterIndentation: {EnforcedStyle: consistent}
Style/IfUnlessModifier: {Enabled: false} Style/IfUnlessModifier: {Enabled: false}
Style/ModuleFunction: {Enabled: false} Style/ModuleFunction: {Enabled: false}
Style/MultilineMethodCallIndentation: {EnforcedStyle: indented}
Style/MultilineOperationIndentation: {EnforcedStyle: indented} Style/MultilineOperationIndentation: {EnforcedStyle: indented}
Style/NestedParenthesizedCalls: {Enabled: false} Style/NestedParenthesizedCalls: {Enabled: false}
Style/PredicateName: {Enabled: false} Style/PredicateName: {Enabled: false}

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

@@ -5,5 +5,6 @@ rvm:
env: env:
- RAILS=4 - RAILS=4
- RAILS=5 - RAILS=5
- RAILS=5_1
notifications: notifications:
email: false email: false

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

@@ -1,3 +1,39 @@
# Unreleased
# 0.12.1
- Fix `set_webhook` rake task for async bots with self-issued certificates.
- Make `#session` raise error when store is not configured,
don't use Rails.cache as fallback for session_store.
- Allow use different sessions for MessageContext.
# 0.12.0
- New API methods and payload types (up to Bot API 3.2).
- Fix rails 5.1.x support.
- RSpec context for callback_query updates.
- `edit_message` helper.
- ClientStub saves and returns token. Fixes testing multiple bots.
- Raise descriptive error when accessing not-configured bot.
# 0.11.3
- Release dependencies for Rails 5.1.
# 0.11.2
- Rails 5.1 deep symbolized secrets support.
# 0.11.1
- Fixed poller for typed response.
# 0.11.0
- Remove Bot::StaleChat in favor of Bot::Forbidden, as Telegram adds more
and more new descriptions.
Please open an issue if you face a problem.
# 0.10.2 # 0.10.2
- Support `(edited_)channel_post` updates. - Support `(edited_)channel_post` updates.

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

@@ -1,14 +1,22 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
case ENV['RAILS']
when '5'
gem 'actionpack', '5.0.0.rc1'
when '4'
gem 'actionpack', '~> 4.2'
end
group :development do group :development do
case ENV['RAILS']
when '5_1'
gem 'railties', '5.1.0'
gem 'actionpack', '5.1.0'
when '5'
gem 'railties', '5.0.2'
gem 'actionpack', '5.0.2'
when '4'
gem 'railties', '~> 4.2'
gem 'actionpack', '~> 4.2'
else
gem 'railties'
gem 'actionpack'
end
gem 'sdoc', '~> 0.4.1' gem 'sdoc', '~> 0.4.1'
gem 'pry', '~> 0.10.1' gem 'pry', '~> 0.10.1'
gem 'pry-byebug', '~> 3.2.0' gem 'pry-byebug', '~> 3.2.0'
@@ -17,6 +25,7 @@ group :development do
gem 'rspec', '~> 3.5.0' gem 'rspec', '~> 3.5.0'
gem 'rspec-its', '~> 1.1.0' gem 'rspec-its', '~> 1.1.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rubocop', '~> 0.37.0' gem 'rubocop', '~> 0.37.0'

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

@@ -24,9 +24,11 @@ Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bo
with session, keyboards and inline queries. with session, keyboards and inline queries.
Run it on your local machine in 1 minute! Run it on your local machine in 1 minute!
And here is [app teamplate](https://github.com/telegram-bot-rb/rails_template) And here is [app template](https://github.com/telegram-bot-rb/rails_template)
to generate clean app in seconds. to generate clean app in seconds.
Examples and cookbook in [wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki).
## Installation ## Installation
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
@@ -43,6 +45,12 @@ Or install it yourself as:
$ gem install telegram-bot $ gem install telegram-bot
Require if necessary:
```ruby
require 'telegram/bot'
```
## Usage ## Usage
### Configuration ### Configuration
@@ -100,7 +108,7 @@ bot.get_me.class # => Telegram::Bot::Types::User
``` ```
Any API request error will raise `Telegram::Bot::Error` with description in its message. Any API request error will raise `Telegram::Bot::Error` with description in its message.
Special `Telegram::Bot::StaleChat` is raised when bot can't post messages to the chat anymore. Special `Telegram::Bot::Forbidden` is raised when bot can't post messages to the chat anymore.
### Controller ### Controller
@@ -160,6 +168,20 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
#### Reply helpers
There are helpers to respond for basic actions. They just set chat/message/query
identifiers from update. See `ReplyHelpers` method for more information.
Here are this methods signatures:
```ruby
def respond_with(type, params); end
def reply_with(type, params); end
def answer_inline_query(results, params = {}); end
def answer_callback_query(text, params = {}); end
def edit_message(type, params = {}); end
```
#### Optional typecasting #### Optional typecasting
You can enable typecasting of `update` with `telegram-bot-types` by including You can enable typecasting of `update` with `telegram-bot-types` by including
@@ -177,12 +199,33 @@ end
#### Session #### Session
There is support for sessions using `ActiveSupport::Cache` stores. This API is very close to ActiveController's session API, but works different
under the hood. Cookies can not be used to store session id or
whole session (like CookieStore does). So it uses key-value store and `session_key`
method to build identifier from update.
Store can be one of numerous `ActiveSupport::Cache` stores.
While `:file_store` is suitable for development and single-server deployments
without heavy load, it doesn't scale well. Key-value databases with persistance
like Redis are more appropriate for production use.
```ruby ```ruby
# configure store in env files: # In rails app store can be configured in env files:
config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month} config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month}
# In other app it can be done for all controllers with:
Telegram::Bot::UpdatesController.session_store = :redis_store, {expires_in: 1.month}
# or for specific one:
OneOfUpdatesController.session_store = :redis_store, {expires_in: 1.month}
```
Default session id is made from bot's username and `(from || chat)['id']`.
It means that session will be the same for updates from user in every chat,
and different for every user in the same group chat.
To change this behavior you can override `session_key` method, or even
define multiple sessions in single controller. For details see `Session` module.
```ruby
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: # or just shortcut:
@@ -200,12 +243,16 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
private private
# By default it uses bot's username and user's id as a session key.
# Chat's id is used only when `from` field is empty. # In this case session will persist for user only in specific chat.
# Override `session_key` method to change this behavior. # Same user in other chat will have different session.
def session_key def session_key
# In this case session will persist for user only in specific chat: "#{bot.username}:#{chat['id']}:#{from['id']}" if chat && from
"#{bot.username}:#{chat['id']}:#{from['id']}" end
# This session will be the same for all updates in chat.
def chat_session
@_chat_session ||= self.class.build_session(chat && "#{bot.username}:#{chat['id']}")
end end
end end
``` ```
@@ -335,6 +382,8 @@ Telegram::Bot::ClientStub.stub_all!
RSpec.configure do |config| RSpec.configure do |config|
# ... # ...
config.after { Telegram.bot.reset } config.after { Telegram.bot.reset }
# or for multiple bots:
config.after { Telegram.bots.each_value(&:reset) }
# ... # ...
end end
``` ```
@@ -353,6 +402,13 @@ RSpec.describe TelegramWebhooksController, :telegram_bot do
subject { -> { dispatch_command :start } } subject { -> { dispatch_command :start } }
it { should respond_with_message 'Hi there!' } it { should respond_with_message 'Hi there!' }
end end
# There is context for callback queries with related matchers.
describe '#hey_callback_query', :callback_query do
let(:data) { "hey:#{name}" }
let(:name) { 'Joe' }
it { should answer_callback_query('Hey Joe') }
it { should edit_current_message :text, text: 'Done' }
end end
# For controller specs use # For controller specs use
@@ -370,6 +426,10 @@ expect(&process_update).
to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
``` ```
Place integration tests inside `spec/requests`
when using RSpec's `infer_spec_type_from_file_location!`,
or just add `type: :request` to `describe`.
See sample app for more examples. See sample app for more examples.
### Deploying ### Deploying

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

@@ -21,7 +21,7 @@ namespace :telegram do
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
url = routes.send("#{route_name}_url") url = routes.send("#{route_name}_url")
puts "Setting webhook for #{key}..." puts "Setting webhook for #{key}..."
bot.set_webhook(url: url, certificate: cert) bot.async(false) { bot.set_webhook(url: url, certificate: cert) }
end end
end end
end end

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

@@ -6,26 +6,7 @@ module Telegram
module Bot module Bot
class Error < StandardError; end class Error < StandardError; end
class NotFound < Error; end class NotFound < Error; end
class Forbidden < Error; end
# Error class for events when chat is not available anymore for bot.
# While Telegram has same error codes for different messages and there is no
# official docs for this error codes it uses `description` to
# check response.
class StaleChat < Error
DESCRIPTIONS = [
'Bot was blocked',
'bot was kicked',
"can't write to",
'group chat is deactivated',
].freeze
class << self
def match_response?(response)
description = response['description'].to_s
DESCRIPTIONS.any? { |x| description[x] }
end
end
end
autoload :Async, 'telegram/bot/async' autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan' autoload :Botan, 'telegram/bot/botan'

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

@@ -5,7 +5,7 @@ require 'active_support/core_ext/hash/keys'
module Telegram module Telegram
module Bot module Bot
class Client class Client # rubocop:disable ClassLength
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
autoload :TypedResponse, 'telegram/bot/client/typed_response' autoload :TypedResponse, 'telegram/bot/client/typed_response'
@@ -46,18 +46,20 @@ module Telegram
@base_uri = format URL_TEMPLATE, self.token @base_uri = format URL_TEMPLATE, self.token
end end
def request(action, body = {}) # rubocop:disable PerceivedComplexity def request(action, body = {})
res = http_request("#{base_uri}#{action}", self.class.prepare_body(body)) res = http_request("#{base_uri}#{action}", self.class.prepare_body(body))
status = res.status status = res.status
return JSON.parse(res.body) if 300 > status return JSON.parse(res.body) if 300 > status
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = "#{res.reason}: #{result && result['description'] || '-'}" err_msg = result && result['description'] || '-'
if result if result
# NotFound is raised only for valid responses from Telegram # This errors are raised only for valid responses from Telegram
raise NotFound, err_msg if 404 == status case status
raise StaleChat, err_msg if StaleChat.match_response?(result) when 403 then raise Forbidden, err_msg
when 404 then raise NotFound, err_msg
end
end end
raise Error, err_msg raise Error, "#{res.reason}: #{err_msg}"
end end
# Splited to the sections similar to API docs. # Splited to the sections similar to API docs.
@@ -68,6 +70,8 @@ module Telegram
setWebhook setWebhook
answerCallbackQuery answerCallbackQuery
deleteChatPhoto
exportChatInviteLink
forwardMessage forwardMessage
getChat getChat
getChatAdministrators getChatAdministrators
@@ -78,6 +82,9 @@ module Telegram
getUserProfilePhotos getUserProfilePhotos
kickChatMember kickChatMember
leaveChat leaveChat
pinChatMessage
promoteChatMember
restrictChatMember
sendAudio sendAudio
sendChatAction sendChatAction
sendContact sendContact
@@ -85,18 +92,35 @@ module Telegram
sendLocation sendLocation
sendMessage sendMessage
sendPhoto sendPhoto
sendSticker
sendVenue sendVenue
sendVideo sendVideo
sendVideoNote
sendVoice sendVoice
setChatDescription
setChatPhoto
setChatTitle
unbanChatMember unbanChatMember
unpinChatMessage
deleteMessage
editMessageCaption editMessageCaption
editMessageReplyMarkup editMessageReplyMarkup
editMessageText editMessageText
sendSticker
getStickerSet
uploadStickerFile
createNewStickerSet
addStickerToSet
setStickerPositionInSet
deleteStickerFromSet
answerInlineQuery answerInlineQuery
sendInvoice
answerShippingQuery
answerPreCheckoutQuery
getGameHighScores getGameHighScores
sendGame sendGame
setGameScore setGameScore

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

@@ -35,6 +35,7 @@ module Telegram
end end
def initialize(token = nil, username = nil, **options) def initialize(token = nil, username = nil, **options)
@token = token || options[:token]
@username = username || options[:username] || token @username = username || options[:username] || token
reset reset
end end

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

@@ -1,5 +1,6 @@
require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/hash/indifferent_access'
module Telegram module Telegram
module Bot module Bot
@@ -33,7 +34,11 @@ module Telegram
# Default bot. # Default bot.
def bot def bot
@bot ||= bots[:default] @bot ||= bots.fetch(:default) do
raise 'Default bot is not configured.' \
' Add :default to bots_config' \
' or use telegram.bot/telegram.bots.default section in secrets.yml.'
end
end end
# Hash of botan clients made from #bots. # Hash of botan clients made from #bots.
@@ -48,9 +53,10 @@ module Telegram
def bots_config def bots_config
@bots_config ||= @bots_config ||=
if defined?(Rails.application) if defined?(Rails.application)
telegram_config = Rails.application.secrets[:telegram] || {} secrets = Rails.application.secrets.
(telegram_config['bots'] || {}).symbolize_keys.tap do |config| fetch(:telegram, {}).with_indifferent_access
default = telegram_config['bot'] secrets.fetch(:bots, {}).symbolize_keys.tap do |config|
default = secrets[:bot]
config[:default] = default if default config[:default] = default if default
end end
else else

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

@@ -18,7 +18,7 @@ module Telegram
ActiveSupport.on_load('telegram.bot.updates_controller') do ActiveSupport.on_load('telegram.bot.updates_controller') do
self.logger = options.logger || Rails.logger self.logger = options.logger || Rails.logger
self.session_store = options.session_store || Rails.cache self.session_store = options.session_store if options.session_store
end end
end end

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

@@ -14,7 +14,7 @@ RSpec.shared_context 'telegram/bot/integration' do
} }
end end
let(:clear_session?) { described_class.respond_to?(:session_store) } let(:clear_session?) { described_class.respond_to?(:session_store) }
before { described_class.session_store.clear if clear_session? } before { described_class.session_store.try!(:clear) if clear_session? }
include Telegram::Bot::RSpec::ClientMatchers include Telegram::Bot::RSpec::ClientMatchers
@@ -36,14 +36,48 @@ RSpec.shared_context 'telegram/bot/integration' do
end end
# Matcher to check response. Make sure to define `let(:chat_id)`. # Matcher to check response. Make sure to define `let(:chat_id)`.
def respond_with_message(expected) def respond_with_message(expected = Regexp.new(''))
raise 'Define chat_id to use respond_with_message' unless defined?(chat_id) raise 'Define chat_id to use respond_with_message' unless defined?(chat_id)
send_telegram_message(bot, expected, chat_id: chat_id) send_telegram_message(bot, expected, chat_id: chat_id)
end end
end end
RSpec.shared_context 'telegram/bot/callback_query', callback_query: true do
include_context 'telegram/bot/integration'
subject { -> { dispatch callback_query: payload } }
let(:payload) { {id: 11, from: from, message: message, data: data} }
let(:message) { {message_id: 22, chat: chat, text: 'message text'} }
# Matcher to check that origin message got edited.
def edit_current_message(type, options = {})
description = 'edit current message'
options = options.merge(
message_id: message[:message_id],
chat_id: chat_id,
)
Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
bot, :"editMessage#{type.to_s.camelize}", description: description
).with(hash_including(options))
end
# Matcher to check that callback query is answered.
def answer_callback_query(text = Regexp.new(''), options = {})
description = "answer callback query with #{text.inspect}"
text = a_string_matching(text) if text.is_a?(Regexp)
options = options.merge(
callback_query_id: payload[:id],
text: text,
)
Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
bot, :answerCallbackQuery, description: description
).with(hash_including(options))
end
end
RSpec.configure do |config| RSpec.configure do |config|
if config.respond_to?(:include_context) if config.respond_to?(:include_context)
config.include_context 'telegram/bot/integration', :telegram_bot config.include_context 'telegram/bot/integration', :telegram_bot
config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query
end end
end end

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

@@ -86,6 +86,8 @@ module Telegram
inline_query inline_query
chosen_inline_result chosen_inline_result
callback_query callback_query
shipping_query
pre_checkout_query
).freeze ).freeze
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)") CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
@@ -169,15 +171,21 @@ module Telegram
# Returns array `[is_command?, action, args]`. # Returns array `[is_command?, action, args]`.
def action_for_payload def action_for_payload
if payload_type if payload_type
send("action_for_#{payload_type}") || [false, payload_type, [payload]] send("action_for_#{payload_type}") || action_for_default_payload
else else
[false, :unsupported_payload_type, []] [false, :unsupported_payload_type, []]
end end
end end
def action_for_default_payload
[false, payload_type, [payload]]
end
# If payload is a message with command, then returned action is an # If payload is a message with command, then returned action is an
# action for this command. # action for this command.
# Separate method, so it can be easily overriden (ex. MessageContext). # Separate method, so it can be easily overriden (ex. MessageContext).
#
# This is not used for edited messages/posts. It process them as basic updates.
def action_for_message def action_for_message
cmd, args = self.class.command_from_text(payload['text'], bot_username) cmd, args = self.class.command_from_text(payload['text'], bot_username)
cmd &&= self.class.action_for_command(cmd) cmd &&= self.class.action_for_command(cmd)
@@ -185,12 +193,6 @@ module Telegram
end end
alias_method :action_for_channel_post, :action_for_message alias_method :action_for_channel_post, :action_for_message
# It doesn't extract commands from edited messages. Just process
# them as usual ones.
def action_for_edited_message
end
alias_method :action_for_edited_channel_post, :action_for_edited_message
def action_for_inline_query def action_for_inline_query
[false, payload_type, [payload['query'], payload['offset']]] [false, payload_type, [payload['query'], payload['offset']]]
end end
@@ -208,6 +210,11 @@ module Telegram
def action_missing(*) def action_missing(*)
end end
PAYLOAD_TYPES.each do |type|
method = :"action_for_#{type}"
alias_method method, :action_for_default_payload unless instance_methods.include?(method)
end
ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self) ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self)
end end
end end

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

@@ -15,7 +15,6 @@ module Telegram
# because `data` param is controlled by client. # because `data` param is controlled by client.
def action_for_callback_query def action_for_callback_query
context, new_data = context_from_callback_query context, new_data = context_from_callback_query
# binding.pry
if context if context
action_name = "#{context}_callback_query" action_name = "#{context}_callback_query"
[false, action_name, [new_data]] if action_method?(action_name) [false, action_name, [new_data]] if action_method?(action_name)

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

@@ -7,14 +7,11 @@ module Telegram
include Session include Session
included do
# As we use before_action context is cleared anyway,
# no matter we used it or not.
singleton_class.send :attr_reader, :context_handlers, :context_to_action
@context_handlers = {}
end
module ClassMethods module ClassMethods
def context_handlers
@_context_handlers ||= {}
end
# Registers handler for context. # Registers handler for context.
# #
# context_handler :rename do |*| # context_handler :rename do |*|
@@ -39,6 +36,8 @@ module Telegram
context_handlers[context] = action || context context_handlers[context] = action || context
end end
attr_reader :context_to_action
# Use it to use context value as action name for all contexts # Use it to use context value as action name for all contexts
# which miss handlers. # which miss handlers.
# For security reasons it supports only action methods and will # For security reasons it supports only action methods and will
@@ -59,10 +58,16 @@ module Telegram
# according to previous request. # according to previous request.
attr_reader :context attr_reader :context
# Controller may have multiple sessions, let it be possible
# to select session for message context.
def message_context_session
session
end
# Fetches context and finds handler for it. If message has new command, # Fetches context and finds handler for it. If message has new command,
# it has higher priority than contextual action. # it has higher priority than contextual action.
def action_for_message def action_for_message
val = session.delete(:context) val = message_context_session.delete(:context)
@context = val && val.to_sym @context = val && val.to_sym
super || context && begin super || context && begin
handler = handler_for_context handler = handler_for_context
@@ -72,7 +77,7 @@ module Telegram
# Save context for the next request. # Save context for the next request.
def save_context(context) def save_context(context)
session[:context] = context message_context_session[:context] = context
end end
def handler_for_context def handler_for_context

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

@@ -38,6 +38,19 @@ module Telegram
) )
bot.answer_callback_query(params) bot.answer_callback_query(params)
end end
# Edit message from callback query.
def edit_message(type, params = {})
params =
if message_id = payload['inline_message_id'] # rubocop:disable AssignmentInCondition
params.merge(inline_message_id: message_id)
elsif message = payload['message'] # rubocop:disable AssignmentInCondition
params.merge(chat_id: message['chat']['id'], message_id: message['message_id'])
else
raise 'Can not edit message without `inline_message_id` or `message`'
end
bot.public_send("edit_message_#{type}", params)
end
end end
end end
end end

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

@@ -8,19 +8,26 @@ module Telegram
module Session module Session
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods
# Builds session with given key and optional store (default to session_store).
# This way it's easier to define multiple custom sessions,
# ex. one for group chat and one for user.
def build_session(key, store = session_store)
raise 'session_store is not configured' unless store
key ? SessionHash.new(store, key) : NullSessionHash.new
end
end
def process_action(*) def process_action(*)
super super
ensure ensure
session.commit session.commit if @_session
end end
protected protected
def session def session
@_session ||= begin @_session ||= self.class.build_session(session_key)
key = session_key
key ? SessionHash.new(self.class.session_store, key) : NullSessionHash.new
end
end end
def session_key def session_key

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

@@ -65,14 +65,16 @@ module Telegram
def fetch_updates def fetch_updates
response = bot.async(false) { bot.get_updates(offset: offset, timeout: timeout) } response = bot.async(false) { bot.get_updates(offset: offset, timeout: timeout) }
return unless response['ok'] && response['result'].any? updates = response.is_a?(Array) ? response : response['result']
return unless updates && updates.any?
reload! do reload! do
response['result'].each do |update| updates.each do |update|
@offset = update['update_id'] + 1 @offset = update['update_id'] + 1
yield update yield update
end end
end end
rescue Timeout::Error # rubocop:disable HandleExceptions rescue Timeout::Error
log { 'Fetch timeout' }
end end
def reload! def reload!

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

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

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

@@ -0,0 +1,13 @@
require 'integration_helper'
RSpec.describe DefaultBotController, :telegram_bot, type: :request do
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'from default' }
end
describe '#load_session' do
subject { -> { dispatch_command :load_session } }
it { should_not raise_error }
end
end

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

@@ -0,0 +1,9 @@
require 'integration_helper'
RSpec.describe NamedBotController, :telegram_bot, type: :request do
let(:bot) { Telegram.bots[:named] }
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'from named' }
end
end

14
spec/integration/requests/other_bot_spec.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
require 'integration_helper'
RSpec.describe OtherBotController, :telegram_bot, type: :request do
let(:bot) { Telegram.bots[:other] }
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'from other' }
end
describe '#load_session' do
subject { -> { dispatch_command :load_session } }
it { should raise_error(/session_store is not configured/) }
end
end

76
spec/integration_helper.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,76 @@
require 'telegram/bot/rspec/integration'
require 'action_controller'
require 'action_dispatch'
require 'action_dispatch/testing/integration'
require 'rails'
require 'telegram/bot/railtie'
require 'rspec/rails/adapters'
require 'rspec/rails/fixture_support'
require 'rspec/rails/example/rails_example_group'
require 'rspec/rails/example/request_example_group'
ENV['RAILS_ENV'] = 'test'
class TestApplication < Rails::Application
config.eager_load = false
config.log_level = :debug
config.action_dispatch.show_exceptions = false
secrets[:secret_key_base] = 'test'
secrets[:telegram] = {
bot: 'default_token',
bots: {
other: {token: 'other_token'},
named: {token: 'named_token', username: 'TestBot'},
},
}
end
Rails.application.initialize!
# # Controllers
%w(default other named).each do |bot_name|
controller = Class.new(Telegram::Bot::UpdatesController) do
define_method :start do |*|
respond_with :message, text: "from #{bot_name}"
end
end
Object.const_set("#{bot_name}_bot_controller".camelize, controller)
end
[DefaultBotController, OtherBotController].each do |klass|
klass.class_eval do
use_session!
define_method :load_session do |*|
session[:test]
end
end
end
DefaultBotController.session_store = :memory_store
RSpec.configure do |config|
config.include RSpec::Rails::RequestExampleGroup, type: :request
config.around type: :request do |ex|
begin
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!
ex.run
ensure
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!(false)
end
end
config.before type: :request do
# Redefine routes before every example, so it does not depent on order.
Rails.application.routes.draw do
require 'telegram/bot/routes_helper'
extend Telegram::Bot::RoutesHelper
telegram_webhooks default: DefaultBotController,
other: OtherBotController,
named: NamedBotController
end
end
end

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

@@ -16,6 +16,7 @@ $LOAD_PATH.unshift GEM_ROOT.join('lib')
require 'telegram/bot' require 'telegram/bot'
require 'telegram/bot/updates_controller/rspec_helpers' require 'telegram/bot/updates_controller/rspec_helpers'
require 'telegram/bot/types' require 'telegram/bot/types'
require 'active_support/core_ext/object/json'
Dir[GEM_ROOT.join('spec/support/**/*.rb')].each { |f| require f } Dir[GEM_ROOT.join('spec/support/**/*.rb')].each { |f| require f }

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

@@ -3,7 +3,7 @@ RSpec.describe Telegram::Bot::Botan do
let(:token) { 'token' } let(:token) { 'token' }
include_examples 'initializers', :botans include_examples 'initializers', :botans
include_examples 'async', request_args: -> { [double(:method), double(:url)] } it_behaves_like 'async', request_args: -> { [double(:method), double(:url)] }
describe '.new' do describe '.new' do
subject { described_class.new(*args) } subject { described_class.new(*args) }

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

@@ -4,7 +4,7 @@ RSpec.describe Telegram::Bot::Client do
let(:botan_token) { double(:botan_token) } let(:botan_token) { double(:botan_token) }
include_examples 'initializers' include_examples 'initializers'
include_examples 'async', request_args: -> { [double(:action), {body: :content}] } it_behaves_like 'async', request_args: -> { [double(:action), {body: :content}] }
describe '.prepare_body' do describe '.prepare_body' do
subject { described_class.prepare_body(input) } subject { described_class.prepare_body(input) }
@@ -48,4 +48,49 @@ RSpec.describe Telegram::Bot::Client do
its(:base_uri) { should include args[0][:token] } its(:base_uri) { should include args[0][:token] }
end end
end end
describe '#request' do
subject { -> { instance.request(action, request_body) } }
let(:action) { :some_action }
let(:url) { "#{format described_class::URL_TEMPLATE, token}#{action}" }
let(:request_body) { double(:body) }
let(:prepared_body) { double(:prepared_body) }
let(:response) { HTTP::Message.new_response(body).tap { |x| x.status = status } }
let(:status) { 200 }
let(:body) { body_json.to_json }
let(:body_json) { {'param' => 'val', 'description' => 'some description'} }
before do
expect(described_class).to receive(:prepare_body).with(request_body) { prepared_body }
expect(instance).to receive(:http_request).with(url, prepared_body) { response }
end
around { |ex| Telegram::Bot::ClientStub.stub_all!(false) { ex.run } }
shared_examples 'invalid body' do |error = Telegram::Bot::Error|
context 'when body is not json' do
let(:body) { '{' }
it { should raise_error error }
end
end
its(:call) { should eq body_json }
include_examples 'invalid body', JSON::ParserError
context 'when status is 403' do
let(:status) { 403 }
it { should raise_error Telegram::Bot::Forbidden, body_json['description'] }
include_examples 'invalid body'
end
context 'when status is 404' do
let(:status) { 404 }
it { should raise_error Telegram::Bot::NotFound, body_json['description'] }
include_examples 'invalid body'
end
context 'when status is other' do
let(:status) { 500 }
it { should raise_error Telegram::Bot::Error, /#{body_json['description']}/ }
end
end
end end

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

@@ -46,8 +46,14 @@ RSpec.describe Telegram::Bot::ClientStub do
context 'when username and token are given' do context 'when username and token are given' do
let(:args) { %w(token superbot) } let(:args) { %w(token superbot) }
its(:token) { should eq nil } its(:token) { should eq args[0] }
its(:username) { should eq args[1] } its(:username) { should eq args[1] }
end end
context 'when hash config is given' do
let(:args) { [token: 'token', username: 'superbot'] }
its(:token) { should eq args[0][:token] }
its(:username) { should eq args[0][:username] }
end
end end
end end

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

@@ -22,8 +22,13 @@ RSpec.describe Telegram::Bot::ConfigMethods do
end end
describe '#bot' do describe '#bot' do
subject { registry.bot } subject { -> { registry.bot } }
it { should eq registry.bots[:default] } its(:call) { should eq registry.bots[:default] }
context 'when bot is not configured' do
let(:config) { super().except(:default) }
it { should raise_error(/bot is not configured/) }
end
end end
describe '#bots' do describe '#bots' do
@@ -67,6 +72,7 @@ RSpec.describe Telegram::Bot::ConfigMethods do
context 'when not configured' do context 'when not configured' do
let(:registry) { Object.new.tap { |x| x.extend described_class } } let(:registry) { Object.new.tap { |x| x.extend described_class } }
before { hide_const('Rails') }
it { should eq({}) } it { should eq({}) }
context 'in rails environment' do context 'in rails environment' do
@@ -87,6 +93,12 @@ RSpec.describe Telegram::Bot::ConfigMethods do
end end
it { should include default: config[:bot] } it { should include default: config[:bot] }
it { should include config[:bots] } it { should include config[:bots] }
context 'on rails >5.1 (deep symbolized keys)' do
let(:secrets) { {telegram: config.deep_symbolize_keys} }
it { should include default: config[:bot] }
it { should include config[:bots] }
end
end end
end end
end end

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

@@ -48,4 +48,32 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
end end
end end
describe '#edit_message' do
subject { controller.edit_message(type, params) }
let(:type) { :reply_markup }
it { expect { subject }.to raise_error(/Can not edit message without/) }
context 'when inline_message_id is present' do
let(:payload) { {inline_message_id: double(:message_id)} }
it 'sets inline_message_id' do
expect(bot).to receive("edit_message_#{type}").with(params.merge(
inline_message_id: payload[:inline_message_id],
)) { result }
should eq result
end
end
context 'when message is present' do
let(:payload) { {message: super().merge(chat: chat)} }
it 'sets chat_id & message_id' do
expect(bot).to receive("edit_message_#{type}").with(params.merge(
message_id: payload[:message][:message_id],
chat_id: payload[:message][:chat]['id'],
)) { result }
should eq result
end
end
end
end end

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

@@ -54,4 +54,45 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
end end
end end
end end
describe '.build_session' do
subject { controller_class.build_session(key, *args) }
let(:key) {}
let(:args) { [] }
it { expect { subject }.to raise_error(/session_store is not configured/) }
shared_examples 'NullSessionHash when key is not present' do |store_proc|
it { should be_instance_of(described_class::NullSessionHash) }
context 'and key is present' do
let(:key) { :test_key }
it 'is valid SessionHash' do
expect(subject).to be_instance_of(described_class::SessionHash)
expect(subject.id).to eq key
expect(subject.instance_variable_get(:@store)).to be(instance_exec(&store_proc))
end
end
end
context 'when store configured' do
before { controller_class.session_store = nil }
include_examples 'NullSessionHash when key is not present',
-> { controller_class.session_store }
end
context 'when store is given' do
let(:args) { [double(:store)] }
include_examples 'NullSessionHash when key is not present', -> { args[0] }
end
end
describe '.session_store=' do
subject { ->(val) { controller_class.session_store = val } }
it 'casts to AS::Cache' do
expect { subject[:null_store] }.to change(controller_class, :session_store).
to(instance_of(ActiveSupport::Cache::NullStore))
expect { subject[nil] }.to change(controller_class, :session_store).
to(instance_of(ActiveSupport::Cache::MemoryStore))
end
end
end end

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

@@ -108,7 +108,7 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
%w(message channel_post).each do |type| %w(message channel_post).each do |type|
context 'when payload is edited_message' do context "when payload is edited_#{type}" do
let(:payload_type) { "edited_#{type}" } let(:payload_type) { "edited_#{type}" }
it { should eq [false, payload_type, [payload]] } it { should eq [false, payload_type, [payload]] }
end end
@@ -142,6 +142,22 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
end end
end end
custom_payload_types = %w(
message
edited_message
channel_post
edited_channel_post
inline_query
chosen_inline_result
callback_query
)
(described_class::PAYLOAD_TYPES - custom_payload_types).each do |type|
context "when payload is #{type}" do
let(:payload_type) { type }
it { should eq [false, payload_type, [payload]] }
end
end
end end
context 'when `update` is a virtus model' do context 'when `update` is a virtus model' do

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

@@ -1,9 +1,39 @@
RSpec.describe Telegram::Bot::UpdatesPoller do RSpec.describe Telegram::Bot::UpdatesPoller do
describe '#initialize' do let(:instance) { described_class.new(bot, controller) }
subject { described_class.new bot, controller } let(:bot) { Telegram::Bot::Client.new('token') }
let(:bot) { double } let(:controller) { double(:controller) }
let(:controller) { double }
describe '#initialize' do
subject { instance }
it { should be } it { should be }
end end
describe '#fetch_updates' do
subject { -> { instance.fetch_updates(&block) } }
let(:block) { ->(x) { expect(x).to eq expected_results.shift } }
let(:results) { [{update_id: 12}, {update_id: 34}] }
let(:expected_results) { results.as_json }
let(:request_result) { {ok: true, result: results}.as_json }
before do
allow(bot).to receive(:get_updates) do
expect(bot.async).to be_falsy
request_result
end
end
it { should change(instance, :offset).to(results.last[:update_id] + 1) }
it { should change { expected_results }.to([]) }
context 'with typed response' do
let(:request_result) { results.as_json.map { |x| Telegram::Bot::Types::Update.new(x) } }
let(:expected_results) { request_result.dup }
it { should change(instance, :offset).to(results.last[:update_id] + 1) }
it { should change { expected_results }.to([]) }
end
context 'when bot is in async mode' do
let(:bot) { Telegram::Bot::Client.new('token', async: Class.new) }
it { should change { expected_results }.to([]) }
end
end
end end

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

@@ -2,25 +2,4 @@ RSpec.describe Telegram::Bot do
it 'has a version number' do it 'has a version number' do
expect(described_class::VERSION).not_to be nil expect(described_class::VERSION).not_to be nil
end end
describe described_class::StaleChat do
describe '.match_response?' do
subject { ->(val) { described_class.match_response?(val) } }
it 'returns true for specific errors' do
expect(subject.call({})).to eq false
expect(subject.call('description' => 'test')).to eq false
expect(subject.call('description' => 'Error: bot was kicked from')).to eq true
expect(subject.call(
'description' => "Forbidden: can't write to private chat with deleted user"
)).to eq true
expect(subject.call(
'description' => 'Bad request: group chat is deactivated'
)).to eq true
expect(subject.call(
'description' => 'Forbidden: Bot was blocked by the user'
)).to eq true
end
end
end
end end

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

@@ -20,8 +20,8 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '~> 2.0' spec.required_ruby_version = '~> 2.0'
spec.add_dependency 'activesupport', '>= 4.0', '< 5.1' spec.add_dependency 'activesupport', '>= 4.0', '< 5.2'
spec.add_dependency 'actionpack', '>= 4.0', '< 5.1' spec.add_dependency 'actionpack', '>= 4.0', '< 5.2'
spec.add_dependency 'httpclient', '~> 2.7' spec.add_dependency 'httpclient', '~> 2.7'
spec.add_development_dependency 'bundler', '~> 1.11' spec.add_development_dependency 'bundler', '~> 1.11'
spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rake', '~> 10.0'