1
0
зеркало из https://github.com/glebtv/telegram-bot.git synced 2026-08-28 15:26:18 +03:00

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

...

27 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
2eb2670ed2 v0.13.1 2018-05-28 13:27:35 +03:00
Max Melentiev
6a5e642470 Fix #chat for non-message updates with TypedUpdates enabled
Fixes #82
2018-05-19 12:53:15 +03:00
printercu
4ff5a58b98 Merge pull request #79 from telegram-bot-rb/fix-travis
Fix travis build
2018-05-14 13:20:35 +03:00
printercu
ffb13321b4 Fix travis build 2018-05-14 12:18:14 +03:00
printercu
3bdc4f5ae0 Merge pull request #72 from WA9ACE/patch-1
Spelling correction
2018-04-25 11:15:23 +03:00
Max Melentiev
5603016e4a Extracted typed response mappings to telegram-bot-types gem
Fixes #75
2018-04-25 11:07:34 +03:00
Caleb Albritton
fa6b9a825e Spelling correction 2018-04-13 11:50:05 -04:00
printercu
bdff6b2e1c Merge pull request #65 from telegram-bot-rb/rails-6
Prepare for rails 6
2018-02-21 21:34:49 +03:00
Max Melentiev
f39a451192 Add rails-6 to build matrix 2018-02-21 20:35:03 +03:00
Max Melentiev
9af6de8d36 Avoid Hash#transform_values. Fixes #63 2018-02-21 19:49:09 +03:00
Max Melentiev
c86b2ef09b v0.13.0 2018-02-02 11:03:14 +03:00
Max Melentiev
bfcb1c5f41 Deleted page from wiki with old routes examples, remove this link from readme 2018-02-02 11:01:22 +03:00
Max Melentiev
88c8d4fd4e Deprecate telegram_webhooks in favor of telegram_webhook 2018-02-02 10:52:48 +03:00
Max Melentiev
06253c940e Extract methods in UpdatesPoller 2018-02-02 10:37:45 +03:00
printercu
eb24816e1e Merge pull request #61 from telegram-bot-rb/test-cc
Setup codeclimate integration
2018-01-18 14:12:11 +03:00
Max Melentiev
a67a643c7b Build new rubies on travis 2018-01-18 14:04:39 +03:00
Max Melentiev
28db729320 Update changelog 2018-01-18 12:43:10 +03:00
Max Melentiev
7f1f2bb884 Fix readme 2018-01-18 12:42:11 +03:00
Max Melentiev
88ae24b7e8 Fix codeclimate issues 2018-01-18 12:42:04 +03:00
Max Melentiev
3a876a42df Setup codeclimate 2018-01-18 12:31:32 +03:00
Yuri S
66991d3653 Support for :credentials on Rails v5.2.x. (#60) 2018-01-17 10:40:24 +03:00
Max Melentiev
5407c5028f Fix specs for rails 4 2018-01-09 12:00:49 +03:00
Max Melentiev
b41032eee9 Update rubocop 2018-01-05 14:21:08 +03:00
Max Melentiev
707e532963 Specs for instrumentation 2018-01-05 14:20:19 +03:00
Max Melentiev
45e8a7283f rescue_from feature 2018-01-05 13:19:44 +03:00
Max Melentiev
70a1ed8a8f Compact requires 2018-01-05 12:52:46 +03:00
Max Melentiev
384d203b59 Fix spec helpers for callback queries 2017-12-04 09:47:17 +03:00
35 изменённых файлов: 633 добавлений и 173 удалений

8
.codeclimate.yml Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
checks:
method-complexity:
config:
threshold: 6 # should be just fine
plugins:
rubocop:
enabled: true

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

@@ -1,3 +1,6 @@
# Rubocop does not support 2.0
Gemspec/RequiredRubyVersion: {Enabled: false}
Rails: {Enabled: true}
Layout/AlignParameters:

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

@@ -1,10 +1,34 @@
language: ruby
cache: bundler
rvm:
- 2.2.3
- 2.5
- 2.4
gemfile:
- gemfiles/rails_42.gemfile
- gemfiles/rails_50.gemfile
- gemfiles/rails_51.gemfile
- gemfiles/rails_52.gemfile
- gemfiles/rails_51.gemfile
matrix:
include:
# old rails
- rvm: 2.5
gemfile: gemfiles/rails_50.gemfile
- rvm: 2.5
gemfile: gemfiles/rails_42.gemfile
# old rubies
- rvm: 2.3
gemfile: gemfiles/rails_52.gemfile
# future rails
- rvm: 2.5
gemfile: gemfiles/rails_60.gemfile
# Allow rails 6 to fail while master branch is used
allow_failures:
- rvm: 2.5
gemfile: gemfiles/rails_60.gemfile
notifications:
email: false
# for 2.5.0 until 2.5.1 is released:
# https://github.com/travis-ci/travis-ci/issues/8978
# https://github.com/travis-ci/travis-ci/issues/8969#issuecomment-354135622
before_install:
- gem update --system
- gem install bundler

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

@@ -1,3 +1,15 @@
appraise 'rails-60' do
git 'https://github.com/rails/rails.git' do
gem 'actionpack', '~> 6.0.0.alpha'
gem 'railties', '~> 6.0.0.alpha'
end
end
appraise 'rails-52' do
gem 'actionpack', '~> 5.2.0.beta2'
gem 'railties', '~> 5.2.0.beta2'
end
appraise 'rails-51' do
gem 'actionpack', '~> 5.1'
gem 'railties', '~> 5.1'

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

@@ -1,6 +1,21 @@
# Unreleased
# 0.13.1
# 0.12.2
- Extracted typed response mappings to telegram-bot-types gem.
It now provides definitions for all API v3.5 methods.
- Fix #chat for non-message updates with TypedUpdates enabled
# 0.13.0
- `rescue_from`.
- Support for `credentials` store in Rails 5.2.
- Deprecate `telegram_webhooks` in favor of `telegram_webhook`.
It was too complicated and such routes looked ugly.
# 0.12.4
- Fix spec helpers for callback queries.
# 0.12.3
- New methods from Bot API v3.5
- Collect all api helper-methods in Client::ApiHelper module.

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

@@ -8,13 +8,13 @@ group :development do
gem 'pry-byebug', '~> 3.2.0'
gem 'sdoc', '~> 0.4.1'
gem 'telegram-bot-types', '~> 0.3.0'
gem 'telegram-bot-types', '~> 0.6.0'
gem 'rspec', '~> 3.5.0'
gem 'rspec-its', '~> 1.1.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rubocop', '~> 0.51.0'
gem 'rubocop', '~> 0.52.1'
gem 'coveralls', '~> 0.8.2', require: false
end

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

@@ -11,7 +11,7 @@ in development, but you can use poller in production if you want.
Package contains:
- Ligthweight client for bot API (with fast and thread-safe
- Lightweight client for bot API (with fast and thread-safe
[httpclient](https://github.com/nahi/httpclient) under the hood).
- Controller with message parser: define methods for commands, not `case` branches.
- Middleware and routes helpers for production env.
@@ -130,6 +130,9 @@ can represent actions as separate methods keeping source much more readable and
New instance of controller is instantiated for each update.
This way every update is processed in isolation from others.
Bot controllers like usual rails controllers provides features like callbacks,
`rescue_from` and instrumentation.
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
# use callbacks like in any other controllers
@@ -325,29 +328,37 @@ Callback queries without prefix stay untouched.
# This one handles `set_value:%{something}`.
def set_value_callback_query(new_value = nil, *)
save_this(value)
answer_callback_query('Saved!)
answer_callback_query('Saved!')
end
# And this one is for `make_cool:%{something}`
def make_cool_callback_query(thing = nil, *)
do_it(thing)
answer_callback_query("#{thing} is cool now! Like a callback query context.")
answer_callback_query("#{thing} is cool now! Like a callback query context.")
end
```
### Routes in Rails app
There is `telegram_webhooks` helper for rails app to define routes for webhooks.
There is `telegram_webhook` helper for rails app to define routes for webhooks.
It defines routes at `telegram/#{bot.token}` and connects bots with controller.
For more options see [examples in wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki/Routes-helpers-in-details).
```ruby
# Create routes for all Telegram.bots using single controller:
telegram_webhooks TelegramController
# Most off apps would require
telegram_webhook TelegramController
# which is same as
telegram_webhook TelegramController, :default
# Use different controllers for each bot:
telegram_webhooks chat: TelegramChatController,
auction: TelegramAuctionController
telegram_webhook TelegramChatController, :chat
telegram_webhook TelegramAuctionController, :auction
# Defined route is named and its name depends on `Telegram.bots`.
# For single bot it will use 'telegram_webhook',
# for multiple bots it uses bot's key in the `Telegram.bots` as prefix
# (eg. `chat_telegram_webhook`).
# You can override this with `as` option:
telegram_webhook TelegramController, as: :custom_telegram_webhook
```
#### Processesing updates
@@ -360,7 +371,7 @@ There are several options to run it automatically:
- Use poller (described in the next section).
To run action without update (ex., send notifications from jobs),
you can call `#process` directly. In this case controller can be initialized
you can call `#process` directly. In this case controller can be initialized
with `:from` and/or `:chat` options instead of `update` object:
```ruby
@@ -374,7 +385,7 @@ Use `rake telegram:bot:poller` to run poller in rails app. It automatically load
changes without restart in development env.
Optionally pass bot id in `BOT` envvar (`BOT=chat`) to specify bot to run poller for.
This task requires `telegram_webhooks` helper to be used as it connects bots with controller.
This task requires `telegram_webhook` helper to be used as it connects bots with controller.
To run poller in other cases use:
```ruby
@@ -523,7 +534,7 @@ Yes, it's threadsafe too.
## Development
After checking out the repo, run `bin/setup` to install dependencies.
After checking out the repo, run `bin/setup` to install dependencies and git hooks.
Then, run `appraisal rake spec` to run the tests.
You can also run `bin/console` for an interactive prompt that will allow you to experiment.

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

@@ -10,11 +10,11 @@ group :development do
gem "pry", "~> 0.10.1"
gem "pry-byebug", "~> 3.2.0"
gem "sdoc", "~> 0.4.1"
gem "telegram-bot-types", "~> 0.3.0"
gem "telegram-bot-types", "~> 0.6.0"
gem "rspec", "~> 3.5.0"
gem "rspec-its", "~> 1.1.0"
gem "rspec-rails", "~> 3.5.0"
gem "rubocop", "~> 0.51.0"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false
end

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

@@ -10,11 +10,11 @@ group :development do
gem "pry", "~> 0.10.1"
gem "pry-byebug", "~> 3.2.0"
gem "sdoc", "~> 0.4.1"
gem "telegram-bot-types", "~> 0.3.0"
gem "telegram-bot-types", "~> 0.6.0"
gem "rspec", "~> 3.5.0"
gem "rspec-its", "~> 1.1.0"
gem "rspec-rails", "~> 3.5.0"
gem "rubocop", "~> 0.51.0"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false
end

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

@@ -10,11 +10,11 @@ group :development do
gem "pry", "~> 0.10.1"
gem "pry-byebug", "~> 3.2.0"
gem "sdoc", "~> 0.4.1"
gem "telegram-bot-types", "~> 0.3.0"
gem "telegram-bot-types", "~> 0.6.0"
gem "rspec", "~> 3.5.0"
gem "rspec-its", "~> 1.1.0"
gem "rspec-rails", "~> 3.5.0"
gem "rubocop", "~> 0.51.0"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false
end

21
gemfiles/rails_52.gemfile Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 5.2.0.beta2"
gem "railties", "~> 5.2.0.beta2"
group :development do
gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1"
gem "pry-byebug", "~> 3.2.0"
gem "sdoc", "~> 0.4.1"
gem "telegram-bot-types", "~> 0.6.0"
gem "rspec", "~> 3.5.0"
gem "rspec-its", "~> 1.1.0"
gem "rspec-rails", "~> 3.5.0"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false
end
gemspec path: "../"

23
gemfiles/rails_60.gemfile Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
# This file was generated by Appraisal
source "https://rubygems.org"
git "https://github.com/rails/rails.git" do
gem "actionpack", "~> 6.0.0.alpha"
gem "railties", "~> 6.0.0.alpha"
end
group :development do
gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1"
gem "pry-byebug", "~> 3.2.0"
gem "sdoc", "~> 0.4.1"
gem "telegram-bot-types", "~> 0.6.0"
gem "rspec", "~> 3.5.0"
gem "rspec-its", "~> 1.1.0"
gem "rspec-rails", "~> 3.5.0"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false
end
gemspec path: "../"

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

@@ -5,9 +5,22 @@ module Telegram
module Bot
class Error < StandardError; end
class NotFound < Error; end
# Raised for valid telegram response with 403 status code.
class Forbidden < Error; end
# Raised for valid telegram response with 404 status code.
class NotFound < Error; end
module_function
def deprecation_0_14
@deprecation ||= begin
require 'active_support/deprecation'
ActiveSupport::Deprecation.new('0.14', 'Telegram::Bot')
end
end
autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client'

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

@@ -4,7 +4,7 @@ require 'httpclient'
module Telegram
module Bot
class Client
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
URL_TEMPLATE = 'https://api.telegram.org/bot%<token>s/'.freeze
autoload :TypedResponse, 'telegram/bot/client/typed_response'
extend Initializers
@@ -36,6 +36,18 @@ module Telegram
def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(prepare_body(body))]
end
def error_for_response(response)
result = JSON.parse(response.body) rescue nil # rubocop:disable RescueModifier
return Error.new(response.reason) unless result
message = result['description'] || '-'
# This errors are raised only for valid responses from Telegram
case response.status
when 403 then Forbidden.new(message)
when 404 then NotFound.new(message)
else Error.new("#{response.reason}: #{message}")
end
end
end
attr_reader :client, :token, :username, :base_uri
@@ -44,23 +56,13 @@ module Telegram
@client = HTTPClient.new
@token = token || options[:token]
@username = username || options[:username]
@base_uri = format URL_TEMPLATE, self.token
@base_uri = format(URL_TEMPLATE, token: self.token)
end
def request(action, body = {})
res = http_request("#{base_uri}#{action}", self.class.prepare_body(body))
status = res.status
return JSON.parse(res.body) if status < 300
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = result && result['description'] || '-'
if result
# This errors are raised only for valid responses from Telegram
case status
when 403 then raise Forbidden, err_msg
when 404 then raise NotFound, err_msg
end
end
raise Error, "#{res.reason}: #{err_msg}"
response = http_request("#{base_uri}#{action}", self.class.prepare_body(body))
raise self.class.error_for_response(response) if response.status >= 300
JSON.parse(response.body)
end
# Endpoint for low-level request. For easy host highjacking & instrumentation.

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

@@ -1,35 +1,23 @@
require 'telegram/bot/client/api_helper'
require 'active_support/core_ext/string/inflections'
module Telegram
module Bot
class Client
# Actions with type-casted results. Install `telegram-bot-types` gem first.
module TypedResponse
{
getFile: :File,
getMe: :User,
getUpdates: [:Update],
getUserProfilePhotos: :UserProfilePhotos,
# First we define methods for every available api method to return `result`
# field instead of object.
ApiHelper.methods_list.each do |method|
define_method(method.to_s.underscore) do |*args|
super(*args)['result']
end
end
forwardMessage: :Message,
sendAudio: :Message,
sendDocument: :Message,
sendLocation: :Message,
sendMessage: :Message,
sendPhoto: :Message,
sendSticker: :Message,
sendVideo: :Message,
sendVoice: :Message,
}.each do |method, type|
next unless type
if type.is_a?(Array)
type_class = Types.const_get(type.first)
define_method(method.to_s.underscore) do |*args|
request(method, *args)['result'].map { |x| type_class.new(x) }
end
else
type_class = Types.const_get(type)
define_method(method.to_s.underscore) do |*args|
type_class.new request(method, *args)['result']
end
# And then override some of them which has castable results.
Types::Response::WRAPPED_METHODS.each do |method, type|
define_method(method.to_s.underscore) do |*args|
Types::Response.wrap(super(*args)['result'], type)
end
end
end

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

@@ -1,5 +1,4 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/hash/indifferent_access'
module Telegram
@@ -43,7 +42,7 @@ module Telegram
# Hash of botan clients made from #bots.
def botans
@botans ||= bots.transform_values(&:botan)
@botans ||= bots.map { |k, v| [k, v.botan] }.to_h
end
# Returns config for .bots method. By default uses `telegram['bots']` section
@@ -53,8 +52,9 @@ module Telegram
def bots_config
@bots_config ||=
if defined?(Rails.application)
secrets = Rails.application.secrets.
fetch(:telegram, {}).with_indifferent_access
app = Rails.application
store = app.respond_to?(:credentials) ? app.credentials : app.secrets
secrets = store.fetch(:telegram, {}).with_indifferent_access
secrets.fetch(:bots, {}).symbolize_keys.tap do |config|
default = secrets[:bot]
config[:default] = default if default

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

@@ -28,40 +28,49 @@ module Telegram
# telegram_webhooks TelegramController
#
# # Or pass custom bots usin any of supported config options:
# telegram_webhooks TelegramController,
# bot,
# {token: token, username: username},
# other_bot_token
#
# # Use different controllers for each bot:
# telegram_webhooks bot => TelegramChatController,
# other_bot => TelegramAuctionController
#
# # telegram_webhooks creates named routes. See
# # RoutesHelper.route_name_for_bot for more info.
# # You can override this options or specify others:
# telegram_webhooks TelegramController, as: :my_webhook
# telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
# other_bot => TelegramAuctionController,
# admin_chat: TelegramAdminChatController
#
# telegram_webhooks TelegramController, [
# bot,
# {token: token, username: username},
# other_bot_token,
# ]
def telegram_webhooks(controllers, bots = nil, **options)
Bot.deprecation_0_14.deprecation_warning(:telegram_webhooks, <<-TXT.strip_heredoc)
It brings unnecessary complexity and encourages writeng less readable code.
Please use telegram_webhook method instead.
It's signature `telegram_webhook(controller, bot = :default, **options)`.
Multiple-bot environments now requires calling this method in a loop
or using statement for each bot.
TXT
unless controllers.is_a?(Hash)
bots = bots ? Array.wrap(bots) : Telegram.bots.values
controllers = Hash[bots.map { |x| [x, controllers] }]
end
controllers.each do |bot, controller|
bot = Client.wrap(bot)
controller, bot_options = controller if controller.is_a?(Array)
params = {
to: Middleware.new(bot, controller),
as: RoutesHelper.route_name_for_bot(bot),
format: false,
}.merge!(options).merge!(bot_options || {})
post("telegram/#{RoutesHelper.escape_token bot.token}", params)
UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode?
telegram_webhook(controller, bot, options.merge(bot_options || {}))
end
end
# Define route which processes requests using given controller and bot.
#
# telegram_webhook TelegramController, bot
#
# telegram_webhook TelegramController
# # same as:
# telegram_webhook TelegramController, :default
#
# # pass additional options
# telegram_webhook TelegramController, :default, as: :custom_route_name
def telegram_webhook(controller, bot = :default, **options)
bot = Client.wrap(bot)
params = {
to: Middleware.new(bot, controller),
as: RoutesHelper.route_name_for_bot(bot),
format: false,
}.merge!(options)
post("telegram/#{RoutesHelper.escape_token bot.token}", params)
UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode?
end
end
end
end

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

@@ -1,8 +1,10 @@
RSpec.shared_context 'telegram/bot/integration' do
let(:bot) { Telegram.bot }
let(:default_message_options) { {from: from, chat: chat} }
let(:from) { {id: from_id} }
let(:from_id) { 123 }
let(:chat) { {id: chat_id} }
let(:chat_id) { 456 }
let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} }
let(:controller_path) do
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
Rails.application.routes.url_helpers.public_send("#{route_name}_path")

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

@@ -53,12 +53,19 @@ module Telegram
class UpdatesController < AbstractController::Base # rubocop:disable ClassLength
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 :CallbackQueryContext, 'telegram/bot/updates_controller/callback_query_context'
autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
%w[
instrumentation
log_subscriber
reply_helpers
rescue
session
].each { |file| require "telegram/bot/updates_controller/#{file}" }
%w[
CallbackQueryContext
MessageContext
TypedUpdate
].each { |mod| autoload mod, "telegram/bot/updates_controller/#{mod.underscore}" }
include AbstractController::Callbacks
# Redefine callbacks with default terminator.
@@ -72,11 +79,11 @@ module Telegram
end
include AbstractController::Translation
include Rescue
include ReplyHelpers
prepend Instrumentation
extend Session::ConfigMethods
include Instrumentation
autoload :TypedUpdate, 'telegram/bot/updates_controller/typed_update'
extend Session::ConfigMethods
PAYLOAD_TYPES = %w[
message
@@ -113,10 +120,17 @@ module Telegram
# any commands.
def command_from_text(text, username = nil)
return unless text
match = text.match CMD_REGEX
match = text.match(CMD_REGEX)
return unless match
return if match[3] && username != true && match[3] != username
[match[1], text.split.drop(1)]
mention = match[3]
[match[1], text.split.drop(1)] if username == true || !mention || mention == username
end
def payload_from_update(update)
update && PAYLOAD_TYPES.find do |type|
item = update[type]
return [item, type] if item
end
end
end
@@ -135,13 +149,7 @@ module Telegram
@_update = update
@_bot = bot
@_chat, @_from = options && options.values_at(:chat, :from)
payload_data = nil
update && PAYLOAD_TYPES.find do |type|
item = update[type]
payload_data = [item, type] if item
end
@_payload, @_payload_type = payload_data
@_payload, @_payload_type = self.class.payload_from_update(update)
end
# Accessor to `'chat'` field of payload. Also tries `'chat'` in `'message'`
@@ -149,7 +157,14 @@ module Telegram
#
# Can be overriden with `chat` option for #initialize.
def chat
@_chat ||= payload.try! { |x| x['chat'] || x['message'] && x['message']['chat'] }
@_chat ||=
if payload
if payload.is_a?(Hash)
payload['chat'] || payload['message'] && payload['message']['chat']
else
payload.try(:chat) || payload.try(:message).try!(:chat)
end
end
end
# Accessor to `'from'` field of payload. Can be overriden with `from` option

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

@@ -4,12 +4,13 @@ module Telegram
# Most methods are taken from ActionController::Instrumentation,
# some are slightly modified.
module Instrumentation
class << self
def prepended(base)
base.send :config_accessor, :logger
base.extend ClassMethods
end
extend ActiveSupport::Concern
included do
config_accessor :logger
end
class << self
def instrument(action, *args, &block)
ActiveSupport::Notifications.instrument(
"#{action}.updates_controller.bot.telegram",

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

@@ -0,0 +1,20 @@
require 'active_support/rescuable'
module Telegram
module Bot
class UpdatesController
module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
private
def process_action(*)
super
rescue Exception => exception # rubocop:disable RescueException
rescue_with_handler(exception) || raise
end
end
end
end
end

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

@@ -35,7 +35,7 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
def deep_stringify(input)
case input
when Array then input.map(&method(__callee__))
when Hash then input.transform_keys(&:to_s).transform_values(&method(__callee__))
when Hash then input.map { |k, v| [k.to_s, deep_stringify(v)] }.to_h
else input
end
end

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

@@ -41,40 +41,54 @@ module Telegram
def start
return if running
@running = true
log { 'Started bot poller.' }
while running
begin
fetch_updates do |update|
controller.dispatch(bot, update)
end
rescue Interrupt
@running = false
rescue StandardError => e
logger.error { ([e.message] + e.backtrace).join("\n") } if logger
end
begin
@running = true
log { 'Started bot poller.' }
run
rescue Interrupt
nil # noop
ensure
@running = false
end
log { 'Stop polling bot updates.' }
log { 'Stoped polling bot updates.' }
end
def run
while running
updates = fetch_updates
process_updates(updates) if updates && updates.any?
end
end
# Method to stop poller from other thread.
def stop
return unless running
log { 'Killing polling thread.' }
log { 'Stopping polling bot updates.' }
@running = false
end
def fetch_updates
def fetch_updates(offset = self.offset)
response = bot.async(false) { bot.get_updates(offset: offset, timeout: timeout) }
updates = response.is_a?(Array) ? response : response['result']
return unless updates && updates.any?
response.is_a?(Array) ? response : response['result']
rescue Timeout::Error
log { 'Fetch timeout' }
nil
end
def process_updates(updates)
reload! do
updates.each do |update|
@offset = update['update_id'] + 1
yield update
process_update(update)
end
end
rescue Timeout::Error
log { 'Fetch timeout' }
rescue StandardError => e
logger.error { ([e.message] + e.backtrace).join("\n") } if logger
end
# Override this method to setup custom error collector.
def process_update(update)
controller.dispatch(bot, update)
end
def reload!

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

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

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

@@ -15,14 +15,21 @@ 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] = {
telegram_config = {
bot: 'default_token',
bots: {
other: {token: 'other_token'},
named: {token: 'named_token', username: 'TestBot'},
},
}
if respond_to?(:credentials)
credentials.config[:telegram] = telegram_config
else
secrets[:secret_key_base] = 'test'
secrets[:telegram] = telegram_config
end
end
Rails.application.initialize!
@@ -68,9 +75,9 @@ RSpec.configure do |config|
require 'telegram/bot/routes_helper'
extend Telegram::Bot::RoutesHelper
telegram_webhooks default: DefaultBotController,
other: OtherBotController,
named: NamedBotController
telegram_webhook DefaultBotController, :default
telegram_webhook OtherBotController, :other
telegram_webhook NamedBotController, :named
end
end
end

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

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

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

@@ -3,7 +3,7 @@ RSpec.describe Telegram::Bot::Client::TypedResponse do
describe '#get_me' do
subject { bot.get_me }
before { expect(bot).to receive(:request).with(:getMe) { response } }
before { expect(bot).to receive(:request).with('getMe') { response } }
let(:response) { {'ok' => true, 'result' => {'id' => user_id}} }
let(:user_id) { 123 }
it { should be_instance_of Telegram::Bot::Types::User }
@@ -17,7 +17,7 @@ RSpec.describe Telegram::Bot::Client::TypedResponse do
describe '#get_updates' do
subject { bot.get_updates }
before { expect(bot).to receive(:request).with(:getUpdates) { response } }
before { expect(bot).to receive(:request).with('getUpdates') { response } }
let(:response) { {'ok' => true, 'result' => [{'update_id' => update_id}]} }
let(:update_id) { 123 }
it { should be_instance_of Array }
@@ -29,4 +29,16 @@ RSpec.describe Telegram::Bot::Client::TypedResponse do
it { expect { subject }.to raise_error Telegram::Bot::Error }
end
end
describe '#delete_webhook' do
subject { bot.delete_webhook }
before { expect(bot).to receive(:request).with('deleteWebhook') { response } }
let(:response) { {'ok' => true, 'result' => true} }
it { should eq true }
context 'on error' do
let(:response) { raise Telegram::Bot::Error }
it { expect { subject }.to raise_error Telegram::Bot::Error }
end
end
end

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

@@ -52,7 +52,7 @@ RSpec.describe Telegram::Bot::Client do
describe '#request' do
subject { -> { instance.request(action, request_body) } }
let(:action) { :some_action }
let(:url) { "#{format described_class::URL_TEMPLATE, token}#{action}" }
let(:url) { "#{format(described_class::URL_TEMPLATE, token: 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 } }

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

@@ -40,6 +40,62 @@ RSpec.describe Telegram::Bot::RoutesHelper do
end
end
describe '#telegram_webhook' do
subject { mapper.telegram_webhook(*input) }
let(:mapper) { double(:mapper).tap { |x| x.extend described_class } }
let(:bots) { {default: bot, other: other_bot} }
let(:controller) { double(:controller, name: :controller) }
let(:other_controller) { double(:other_controller, name: :other_controller) }
before { allow(Telegram).to receive(:bots) { bots } }
def assert_routes(bot, controller, route_name, options) # rubocop:disable AbcSize
expected_path = options.delete(:path) || "telegram/#{bot.token}"
expect(mapper).to receive(:post) do |path, params|
expect(path).to eq expected_path
middleware = params[:to]
expect(middleware.controller).to eq(controller)
expect(middleware.bot.token).to eq(bot.token)
expect(middleware.bot.username).to eq(bot.username)
expect(params[:as]).to eq route_name
expect(params).to include(options) if options
end
subject
end
context 'when called with controller' do
let(:input) { [controller, option: :val] }
it 'creates routes for default bot and this controller' do
assert_routes bot, controller, 'default_telegram_webhook', option: :val
end
context 'and bot does not have configured token' do
let(:bot) { create_bot(nil) }
it 'creates routes for default bot and this controller' do
assert_routes bot, controller, 'default_telegram_webhook', option: :val
end
end
context 'and bot has colon in token' do
let(:bot) { create_bot('some:token') }
it 'replaces colon with underscore' do
assert_routes bot, controller, 'default_telegram_webhook',
option: :val,
path: 'telegram/some_token'
end
end
end
context 'when called with controller and smth castable to bot' do
let(:input) { [controller, 'custom_token', option: :val] }
it 'creates routes for every created bot and controller' do
assert_routes create_bot('custom_token'), controller, 'telegram_webhook',
option: :val
end
end
end
describe '#telegram_webhooks' do
subject { mapper.telegram_webhooks(*input) }
let(:mapper) { double(:mapper).tap { |x| x.extend described_class } }
@@ -47,6 +103,7 @@ RSpec.describe Telegram::Bot::RoutesHelper do
let(:controller) { double(:controller, name: :controller) }
let(:other_controller) { double(:other_controller, name: :other_controller) }
before { allow(Telegram).to receive(:bots) { bots } }
around { |ex| Telegram::Bot.deprecation_0_14.silence { ex.run } }
def assert_routes(*expected) # rubocop:disable AbcSize
expected.each do |(bot, controller, route_name, options)|

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

@@ -69,4 +69,44 @@ RSpec.describe 'Integrations helper', :telegram_bot do
it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" }
end
end
describe 'callback queries', :callback_query do
let(:controller) do
Class.new(Telegram::Bot::UpdatesController) do
include Telegram::Bot::UpdatesController::CallbackQueryContext
def callback_query(data = nil, *)
answer_callback_query "data: #{data}"
end
def context_callback_query(data = nil, *)
answer_callback_query "data: #{data}", extra: :param
end
def answer_and_edit_callback_query(data = nil, *)
answer_callback_query "data: #{data}"
edit_message :text, text: 'edited-text', extra: :param
end
end
end
describe '#callback_query' do
let(:data) { 'unknown:command' }
it { should answer_callback_query("data: #{data}") }
end
describe '#context_callback_query' do
let(:data) { 'context:test:payload' }
it { should answer_callback_query('data: test:payload', extra: :param) }
it { should_not edit_current_message(:text) }
end
describe '#answer_and_edit_callback_query' do
let(:data) { 'answer_and_edit:test:payload' }
it do
should answer_callback_query(/test:payload/).
and edit_current_message(:text, text: /edited/, extra: :param)
end
end
end
end

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

@@ -0,0 +1,91 @@
RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do
include_context 'telegram/bot/updates_controller'
subject { -> { dispatch } }
let(:update) do
build_update :message, default_message_options.merge(text: '/start')
end
let(:controller_class) do
Class.new(Telegram::Bot::UpdatesController) do
def start(*)
end
end
end
let(:action_scope) { 'updates_controller.bot.telegram' }
let(:events) { Hash.new { |h, k| h[k] = [] } }
let!(:subscriber) do
ActiveSupport::Notifications.subscribe(/#{action_scope}/) do |action, *args|
events[action] << args
end
end
after { ActiveSupport::Notifications.unsubscribe(subscriber) }
def actions_list(*prefixes)
prefixes.map { |x| action_name(x) }
end
def action_name(prefix)
"#{prefix}.#{action_scope}"
end
describe '#process_action' do
it 'fires start_processing and process_action events' do
should change(events, :keys).by(actions_list(:start_processing, :process_action))
action = action_name(:start_processing)
expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(update: update)
action = action_name(:process_action)
expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(update: update)
end
end
describe '#halted_callback_hook' do
let(:controller_class) do
super().try(:class_eval) do |x|
x.before_action :halter_method
def halter_method
require 'active_support/version'
if ActiveSupport::VERSION::MAJOR >= 5
throw :abort
else
false
end
end
self
end
end
it 'fires halted_callback event' do
should change(events, :keys).
by(actions_list(:start_processing, :halted_callback, :process_action))
action = action_name(:halted_callback)
expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(filter: :halter_method)
end
end
describe '#respond_with' do
before do
def controller.start(*)
respond_with :message, text: 'sample response'
end
end
it 'fires respond_with event' do
should change(events, :keys).
by(actions_list(:start_processing, :respond_with, :process_action))
action = action_name(:respond_with)
expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(type: :message)
end
end
end

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

@@ -0,0 +1,31 @@
RSpec.describe Telegram::Bot::UpdatesController::Rescue do
include_context 'telegram/bot/updates_controller'
describe '#process_action' do
subject { -> { dispatch_message("/#{command} x y z") } }
let(:controller_class) do
Class.new(Telegram::Bot::UpdatesController) do
rescue_from ArgumentError, with: -> { respond_with :message, text: 'Rescued' }
def rescuable(*)
raise ArgumentError, 'rescuable'
end
def not_rescuable(*)
raise 'not_rescuable'
end
end
end
context 'when exception is rescued' do
let(:command) { :rescuable }
it { should respond_with_message 'Rescued' }
end
context 'when exception is not rescued' do
let(:command) { :not_rescuable }
it { should raise_error('not_rescuable') }
end
end
end

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

@@ -9,17 +9,40 @@ RSpec.describe Telegram::Bot::UpdatesController::TypedUpdate do
context 'when `update` is a virtus model' do
subject { controller }
%w[
message
inline_query
chosen_inline_result
].each do |type|
unique_types = Telegram::Bot::UpdatesController::PAYLOAD_TYPES - %w[
edited_message
channel_post
edited_channel_post
]
unique_types.each do |type|
context "with #{type}" do
type_class = Telegram::Bot::Types.const_get(type.camelize)
let(:payload_type) { type }
let(:payload) { {} }
let(:payload) do
{}.tap do |result|
result[:chat] = chat if type_class.instance_methods.include?(:chat)
result[:from] = from if type_class.instance_methods.include?(:from)
end
end
let(:chat) { {id: 'chat_id'} }
let(:from) { {id: 'from_id'} }
its(:payload_type) { should eq payload_type }
its(:payload) { should be_instance_of type_class }
if type_class.instance_methods.include?(:chat)
# Virtus does not support ==. :(
its(:chat) { should be_instance_of Telegram::Bot::Types::Chat }
its('chat.to_hash') { should include chat }
else
its(:chat) { should eq nil }
end
if type_class.instance_methods.include?(:from)
its(:from) { should be_instance_of Telegram::Bot::Types::User }
its('from.to_hash') { should include from }
else
its(:from) { should eq nil }
end
end
end
end

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

@@ -8,12 +8,32 @@ RSpec.describe Telegram::Bot::UpdatesPoller do
it { should be }
end
describe '#fetch_updates' do
subject { -> { instance.fetch_updates(&block) } }
describe '#process_updates' do
subject { -> { instance.process_updates(updates) } }
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 }
let(:updates) { [{update_id: 12}, {update_id: 34}].as_json }
let(:processed_updates) { [] }
before do
allow(controller).to receive(:dispatch) do |bot, update|
expect(bot).to eq self.bot
processed_updates << update
end
end
it { should change(instance, :offset).to(updates.last['update_id'] + 1) }
it { should change(self, :processed_updates).to(updates) }
context 'with typed response' do
let(:updates) { super().map { |x| Telegram::Bot::Types::Update.new(x) } }
it { should change(instance, :offset).to(updates.last['update_id'] + 1) }
it { should change(self, :processed_updates).to(updates) }
end
end
describe '#fetch_updates' do
subject { instance.fetch_updates }
let(:updates) { [{update_id: 12}, {update_id: 34}] }
let(:request_result) { {ok: true, result: updates}.as_json }
before do
allow(bot).to receive(:get_updates) do
expect(bot.async).to be_falsy
@@ -21,19 +41,17 @@ RSpec.describe Telegram::Bot::UpdatesPoller do
end
end
it { should change(instance, :offset).to(results.last[:update_id] + 1) }
it { should change { expected_results }.to([]) }
it { should eq updates.as_json }
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([]) }
let(:updates) { super().map { |x| Telegram::Bot::Types::Update.new(x.as_json) } }
let(:request_result) { updates }
it { should eq updates }
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([]) }
it { should eq updates.as_json }
end
end
end

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

@@ -23,6 +23,6 @@ Gem::Specification.new do |spec|
spec.add_dependency 'activesupport', '>= 4.0', '< 6.0'
spec.add_dependency 'httpclient', '~> 2.7'
spec.add_development_dependency 'bundler', '~> 1.11'
spec.add_development_dependency 'bundler', '~> 1.16'
spec.add_development_dependency 'rake', '~> 10.0'
end