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

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

..

80 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
b40e4b432d Merge pull request #198 from telegram-bot-rb/nested_files
Add support for editMessageMedia similar to sendMediaGroup
2023-01-14 13:18:38 +00:00
Max Melentiev
ef5ec81386 Add support for editMessageMedia similar to sendMediaGroup 2023-01-13 20:33:30 +00:00
Max Melentiev
7d8a19d3c8 Pass action parameter to RequestBodyFormatter 2022-03-13 21:39:55 +00:00
Max Melentiev
380f6686fc Update to Bot API 5.7 2022-03-12 19:37:53 +00:00
Max Melentiev
ea64230eab Merge pull request #168 from telegram-bot-rb/nested_files
Fix sending File objects in nested objects (ex., in sendMediaGroup)
2022-03-12 19:34:57 +00:00
Max Melentiev
5b9706ed47 Fix sending File objects in nested objects in sendMediaGroup 2022-03-12 19:33:17 +00:00
Max Melentiev
2d73d1c879 Merge pull request #174 from telegram-bot-rb/rails-7
Rails 7.0 support
2022-01-04 22:54:17 +00:00
Max Melentiev
88af1152cf Rails 7.0 support 2022-01-04 22:49:53 +00:00
Max Melentiev
586217585a Merge pull request #164 from KonstantinReido/patch-1
Fix typo in README.md
2021-08-20 12:36:46 +01:00
Konstantin Reido
f83bf92601 Fix typo in README.md 2021-08-20 11:26:00 +03:00
Max Melentiev
3049ced903 Fix tests 2021-06-26 21:25:43 +01:00
Max Melentiev
07faf87ed7 Merge pull request #163 from telegram-bot-rb/update_api
Update to Bot API 5.3
2021-06-25 19:43:17 +01:00
Max Melentiev
ac45940937 Switch from travis-ci.org to travis-ci.com 2021-06-25 19:42:59 +01:00
Max Melentiev
a49ef51da9 Update to Bot API 5.3 2021-06-25 19:31:28 +01:00
Max Melentiev
4f669fccc4 Merge pull request #157 from AlexVPopov/table-of-contents
Add a table of contents to README
2021-03-31 20:56:59 +01:00
Alexander Popov
e06252907a Add a table of contents to README 2021-03-31 21:11:08 +03:00
Max Melentiev
af229c8bcc Fix typos in readme, make some points more clear 2021-02-19 09:38:54 +00:00
Max Melentiev
a7cf3754be v0.15.3 2021-02-15 13:54:10 +00:00
Max Melentiev
783d9c13e9 Merge pull request #156 from telegram-bot-rb/ruby-3
Fix translation helpers to support ruby 3
2021-02-15 13:52:09 +00:00
Max Melentiev
5417511f06 Fix translation helpers to support ruby 3 2021-02-15 09:44:19 +00:00
Max Melentiev
a0ca383bab v0.15.2 2021-01-26 08:37:02 +00:00
Max Melentiev
1c141f782e Merge pull request #153 from telegram-bot-rb/ruby-3.0
Ruby 3.0 support
2021-01-26 08:30:24 +00:00
Max Melentiev
791c4233ca Ruby 3.0 support, drop support for Ruby < 2.4 2021-01-09 11:35:13 +00:00
Max Melentiev
036fbd20d5 v0.15.1 2020-12-20 08:25:35 +03:00
Max Melentiev
0590967694 Merge pull request #150 from telegram-bot-rb/rails-61
Add rails 6.1 support
2020-12-20 05:23:57 +00:00
Max Melentiev
4ab1f07bed Add rails 6.1 support 2020-12-11 21:00:15 +03:00
Max Melentiev
8cd948a544 v0.15.0 2020-12-06 18:37:47 +03:00
Max Melentiev
863e838533 Remove deprecated rspec/integration 2020-12-06 18:35:10 +03:00
Max Melentiev
2a2fafa536 Merge pull request #147 from telegram-bot-rb/update_api
Update to Bot API 5.0
2020-12-06 15:32:58 +00:00
Max Melentiev
4b9eedb5a2 Add server option for client to support local bot API servers 2020-11-27 18:47:47 +00:00
Max Melentiev
f654071c4c Drop Initializer module
It was required for Botan support.
2020-11-27 09:39:40 +00:00
Max Melentiev
e178f808b6 Update to Bot API 5.0, add rake tasks for deleteWebhook, close & logOut 2020-11-27 09:33:39 +00:00
Max Melentiev
d173443c56 Merge pull request #142 from telegram-bot-rb/path_with_hashed_token
Use token hash in routes instead of plain value, support :path option
2020-09-26 16:14:04 +01:00
Max Melentiev
97e45d94f2 Use token hash in routes instead of plain value, support :path option 2020-09-06 20:13:08 +01:00
Max Melentiev
03472e3d46 Merge pull request #141 from astrophena/updates-poller-typo
Fix a typo in updates poller
2020-07-25 15:29:59 +03:00
Ilya Mateyko
bc2b09a3b0 Fix a typo in updates poller 2020-07-23 01:09:13 +03:00
Max Melentiev
8494c7df0c v0.14.4 2020-04-15 20:40:55 +03:00
Max Melentiev
a2659e00c2 Bump rake version to resolve vulnerability notification 2020-04-04 22:36:41 +03:00
Max Melentiev
e43c611d9d Merge pull request #131 from telegram-bot-rb/update_api
Update to Bot API 4.7
2020-04-04 22:35:33 +03:00
Max Melentiev
b84c48bc1d Update to Bot API 4.7 2020-04-04 22:26:26 +03:00
Max Melentiev
43cd096a21 Fix spelling in Async docs 2020-01-25 15:49:28 +03:00
Maximilian Haack
903e14a3e5 Fix make_telegram_request examples in README (#129) 2020-01-25 13:34:48 +03:00
Max Melentiev
da31cd148a v0.14.3 2019-08-24 19:34:34 +03:00
Max Melentiev
552f3b3353 Update to Bot API 4.4 2019-08-24 19:33:46 +03:00
Max Melentiev
e5f93f608b Merge pull request #121 from telegram-bot-rb/rails_60
Require tests against rails 6.0, run tests against rails 6.1.alpha
2019-08-19 22:10:50 +03:00
Max Melentiev
7e823b4354 Require tests against rails 6.0, run tests against rails 6.1.alpha
Rails 6.0 requires ruby >= 2.5, so ruby 2.4 is not tested by default
and replaced with 2.6
2019-08-19 11:06:48 +03:00
Max Melentiev
ca4ea9e6cb v0.14.2 2019-06-30 21:15:53 +03:00
Max Melentiev
68ec0257bd Add note on editing rails 6.0 credential 2019-06-30 21:10:22 +03:00
Max Melentiev
b50bd30137 Add methods from api v4.3 2019-06-30 20:57:50 +03:00
Max Melentiev
80362da270 Add answer_shipping_query helper 2019-06-30 20:56:41 +03:00
Muhammad Nuzaihan Bin Kamal Luddin
294d762785 Add answer_pre_checkout_query helper (#118) 2019-06-30 20:39:43 +03:00
printercu
1cb3265371 Merge pull request #113 from telegram-bot-rb/fix_travis
Remove workaround for ruby 2.5.0 to fix travis builds
2019-02-15 20:03:31 +05:30
Max Melentiev
c44529725f Relax bundler dependency 2019-02-15 19:46:01 +05:30
Max Melentiev
fa43561765 Remove workaround for ruby 2.5.0 to fix travis builds 2019-02-15 19:27:18 +05:30
printercu
cea856c928 Merge pull request #110 from biow0lf/master
Fix typo
2019-02-15 19:25:32 +05:30
Igor Zubkov
5a93c1efc6 Fix typo 2019-01-24 03:47:05 +02:00
Max Melentiev
38ee303912 Fix doc-comment for latest changes 2018-09-23 10:39:56 +03:00
Max Melentiev
380d13f65a Update to Bot API 4.1 2018-09-03 08:57:10 +03:00
Max Melentiev
20427499c3 Read config from secrets when credentials don't provide it in rails >= 5.2 2018-06-27 09:47:26 +03:00
Max Melentiev
8936a7a96c Remove botan.io support
https://github.com/botanio/sdk#this-service-will-be-shut-down-on-25th-may-2018
2018-06-22 07:45:08 +03:00
printercu
e2e32e041a Merge pull request #94 from KonstantinReido/master
Changed README a bit
2018-06-22 07:34:40 +03:00
Konstantin Reido
543258fa11 Remove section about Botan.io because this service isn’t available anymore 2018-06-22 00:52:27 +03:00
Konstantin Reido
4bf5956b41 Add a note to README about credentials API appeared in rails 5.2 2018-06-22 00:30:11 +03:00
Konstantin Reido
8b6dc60aaa Fix typo in README 2018-06-22 00:25:20 +03:00
Max Melentiev
b6bef94081 Fix spelling in readme 2018-06-07 16:57:14 +06:00
Max Melentiev
1f62636a91 v0.14.0 2018-06-07 16:49:50 +06:00
printercu
64d00681a3 Merge pull request #85 from telegram-bot-rb/commands
Refactor actions system
2018-06-07 14:42:12 +06:00
Max Melentiev
409d6e4aa8 Use not confusing naming in readme 2018-06-07 14:38:51 +06:00
Max Melentiev
b5f4c97f44 Class-level helper for lazy translations 2018-06-07 14:26:13 +06:00
Max Melentiev
7ff89d012c Translation helper strips ! from action name for lazy translations 2018-06-07 14:10:22 +06:00
Max Melentiev
6dd705fe89 Drop .context_handler, .context_to_action! methods 2018-06-07 12:32:09 +06:00
Max Melentiev
9310fa613b Use bang-methods as actions for commands 2018-06-07 12:32:08 +06:00
printercu
8db3204609 Merge pull request #84 from telegram-bot-rb/unify_specs
Unify specs
2018-06-07 12:29:31 +06:00
Max Melentiev
fb02c34be9 Add deprecated fallback at telegram/bot/rspec/integration 2018-06-07 12:20:34 +06:00
Max Melentiev
a9b45c70cf Update readme 2018-06-06 01:47:17 +06:00
Daniel Pietersen
ad798cd48d Testing Cleanup
Thought I'd suggest some edits based on grammar and some spelling mistakes considering how useful this Bot has been for the project I'm working on. Feel free to ignore if it's not needed.
2018-06-06 01:37:34 +06:00
Max Melentiev
4d76941d03 Support for testing bots in poller mode and non-Rails apps 2018-05-30 11:43:05 +03:00
Max Melentiev
050df20533 Require action_dispatch fully
It uses autoload, so it's lazy-loaded. However there are missing some
internal requires so it will asks for more not-related `requires`
when requiring some files directly.
2018-05-30 11:43:05 +03:00
Max Melentiev
673986ca7e Make integration & controller specs consistent 2018-05-30 11:43:05 +03:00
Max Melentiev
eeb2569bd7 Remve deprecated telegram_webhooks 2018-05-28 19:49:11 +03:00
82 изменённых файлов: 1801 добавлений и 1361 удалений

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

@@ -40,6 +40,7 @@ Style/IfUnlessModifier: {Enabled: false}
# Consistent to other definitions. # Consistent to other definitions.
Style/EmptyMethod: {EnforcedStyle: expanded} Style/EmptyMethod: {EnforcedStyle: expanded}
Style/Lambda: {EnforcedStyle: literal}
Style/ModuleFunction: {Enabled: false} Style/ModuleFunction: {Enabled: false}
Style/NestedParenthesizedCalls: {Enabled: false} Style/NestedParenthesizedCalls: {Enabled: false}
Style/SignalException: {EnforcedStyle: only_raise} Style/SignalException: {EnforcedStyle: only_raise}

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

@@ -1,34 +1,30 @@
os: linux
dist: xenial
language: ruby language: ruby
cache: bundler cache: bundler
rvm: rvm:
- 2.6
- 2.5 - 2.5
- 2.4
gemfile: gemfile:
- gemfiles/rails_61.gemfile
- gemfiles/rails_60.gemfile
- gemfiles/rails_52.gemfile - gemfiles/rails_52.gemfile
- gemfiles/rails_51.gemfile jobs:
matrix:
include: include:
# ruby 3.x
- rvm: 3.0
gemfile: gemfiles/rails_61.gemfile
- rvm: 3.0
gemfile: gemfiles/rails_60.gemfile
# old rails # old rails
- rvm: 2.5
gemfile: gemfiles/rails_51.gemfile
- rvm: 2.5 - rvm: 2.5
gemfile: gemfiles/rails_50.gemfile gemfile: gemfiles/rails_50.gemfile
- rvm: 2.5 - rvm: 2.5
gemfile: gemfiles/rails_42.gemfile gemfile: gemfiles/rails_42.gemfile
# old rubies # old rubies
- rvm: 2.3 - rvm: 2.4
gemfile: gemfiles/rails_52.gemfile 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: notifications:
email: false 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,26 +1,34 @@
appraise 'rails-70' do
gem 'actionpack', '~> 7.0.0'
gem 'railties', '~> 7.0.0'
end
appraise 'rails-61' do
gem 'actionpack', '~> 6.1.0'
gem 'railties', '~> 6.1.0'
end
appraise 'rails-60' do appraise 'rails-60' do
git 'https://github.com/rails/rails.git' do gem 'actionpack', '~> 6.0.0'
gem 'actionpack', '~> 6.0.0.alpha' gem 'railties', '~> 6.0.0'
gem 'railties', '~> 6.0.0.alpha'
end
end end
appraise 'rails-52' do appraise 'rails-52' do
gem 'actionpack', '~> 5.2.0.beta2' gem 'actionpack', '~> 5.2.0'
gem 'railties', '~> 5.2.0.beta2' gem 'railties', '~> 5.2.0'
end end
appraise 'rails-51' do appraise 'rails-51' do
gem 'actionpack', '~> 5.1' gem 'actionpack', '~> 5.1.0'
gem 'railties', '~> 5.1' gem 'railties', '~> 5.1.0'
end end
appraise 'rails-50' do appraise 'rails-50' do
gem 'actionpack', '~> 5.0' gem 'actionpack', '~> 5.0.0'
gem 'railties', '~> 5.0' gem 'railties', '~> 5.0.0'
end end
appraise 'rails-42' do appraise 'rails-42' do
gem 'actionpack', '~> 4.2' gem 'actionpack', '~> 4.2.0'
gem 'railties', '~> 4.2' gem 'railties', '~> 4.2.0'
end end

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

@@ -1,3 +1,80 @@
# Unreleased
# 0.15.7
- Add support for editMessageMedia similar to sendMediaGroup.
# 0.15.6
- Rails 7.0 support.
- Fix sending File objects in nested objects in sendMediaGroup.
- Update to Bot API 5.7
# 0.15.4
- Update to Bot API 5.3
# 0.15.3
- Ruby 3.0 support: fix translation helpers.
# 0.15.2
- Ruby 3.0 support. Drop support for Ruby < 2.4.
# 0.15.1
- Rails 6.1 support.
# 0.15.0
- Add `:path` option to `telegram_webhook` route helper.
- __Breaking change!__ Default route is generated using hashed bot token.
Please reconfigure webhook after update (`rake telegram:bot:set_webhook`).
- Update to Bot API 5.0, add rake tasks for `deleteWebhook`, `close` & `logOut`.
- Add `server` option for client to support local bot API servers.
# 0.14.4
- Update to Bot API 4.7
# 0.14.3
- Rails 6.0 support.
- Update to Bot API 4.4.
# 0.14.2
- Add reply helpers: `answer_pre_checkout_query`, `answer_shipping_query`.
- Update to Bot API 4.3.
# 0.14.1
- Read config from secrets when credentials don't provide it in rails >= 5.2.
- Remove botan.io support. It's already shut down, so it should not be a braking change.
https://github.com/botanio/sdk#this-service-will-be-shut-down-on-25th-may-2018
- Update to Bot API 4.1.
# 0.14.0
- Make integration & controller specs consistent.
__Breaking changes__ for controller specs:
- Changed signature `dispatch(bot, update) => dispatch(update, bot)`.
- `update` helper is symbolized by default.
- `build_update(type, data)` is dropped in favor of `deep_stringify(type => data)`.
- Provide support for integration testing of bots in poller mode and non-Rails apps.
__Breaking changes__:
- Requiring `telegram/bot/rspec/integration` is deprecated in favor of
`telegram/bot/rspec/integration/rails`.
- `:telegram_bot` rspec tag is replaced with `telegram_bot: :rails`.
- __Breaking change__. Use bang-methods as actions for commands.
This prevents calling context contextual actions and payload specific actions with commands.
Translation helper strips `!` from action name for lazy translations.
- __Breaking change__. Drop `.context_handler`, `.context_to_action!` methods.
Use pass action name directly to `#save_context`.
It's the same as `.context_to_action!` is enabled by default.
- Class-level helper for lazy translations.
# 0.13.1 # 0.13.1
- Extracted typed response mappings to telegram-bot-types gem. - Extracted typed response mappings to telegram-bot-types gem.

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

@@ -4,17 +4,17 @@ gemspec
group :development do group :development do
gem 'appraisal', '~> 2.2' gem 'appraisal', '~> 2.2'
gem 'pry', '~> 0.10.1' gem 'pry', '~> 0.13.1'
gem 'pry-byebug', '~> 3.2.0' gem 'pry-byebug', '~> 3.9.0'
gem 'sdoc', '~> 0.4.1' gem 'sdoc', '~> 2.0.3'
gem 'telegram-bot-types', '~> 0.6.0' gem 'telegram-bot-types', '~> 0.6.3'
gem 'rspec', '~> 3.5.0' gem 'rspec', '~> 3.10.0'
gem 'rspec-its', '~> 1.1.0' gem 'rspec-its', '~> 1.3.0'
gem 'rspec-rails', '~> 3.5.0' gem 'rspec-rails', '~> 4.0.2'
gem 'rubocop', '~> 0.52.1' gem 'rubocop', '~> 0.52.1'
gem 'coveralls', '~> 0.8.2', require: false gem 'coveralls', '~> 0.8.23', require: false
end end

278
README.md
Просмотреть файл

@@ -2,7 +2,7 @@
[![Gem Version](https://badge.fury.io/rb/telegram-bot.svg)](http://badge.fury.io/rb/telegram-bot) [![Gem Version](https://badge.fury.io/rb/telegram-bot.svg)](http://badge.fury.io/rb/telegram-bot)
[![Code Climate](https://codeclimate.com/github/telegram-bot-rb/telegram-bot/badges/gpa.svg)](https://codeclimate.com/github/telegram-bot-rb/telegram-bot) [![Code Climate](https://codeclimate.com/github/telegram-bot-rb/telegram-bot/badges/gpa.svg)](https://codeclimate.com/github/telegram-bot-rb/telegram-bot)
[![Build Status](https://travis-ci.org/telegram-bot-rb/telegram-bot.svg)](https://travis-ci.org/telegram-bot-rb/telegram-bot) [![Build Status](https://app.travis-ci.com/telegram-bot-rb/telegram-bot.svg)](https://app.travis-ci.com/github/telegram-bot-rb/telegram-bot)
Tools for developing Telegram bots. Best used with Rails, but can be used in Tools for developing Telegram bots. Best used with Rails, but can be used in
[standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Not-rails-application). [standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Not-rails-application).
@@ -17,7 +17,7 @@ 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 mode](#async-mode)__ for Telegram and/or Botan API. - __[Async mode](#async-mode)__.
Let the queue adapter handle network errors! 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)
@@ -25,9 +25,33 @@ 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 template](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 new rails app in seconds.
Examples and cookbook in [wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki). See examples and cookbook in [the wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki).
## Table of Contents
* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
* [Configuration in Rails app](#configuration-in-rails-app)
* [Client](#client)
* [Typed responses](#typed-responses)
* [Controller](#controller)
* [Reply helpers](#reply-helpers)
* [Optional typecasting](#optional-typecasting)
* [Session](#session)
* [Message context](#message-context)
* [Callback queries](#callback-queries)
* [Routes in Rails app](#routes-in-rails-app)
* [Processing updates](#processing-updates)
* [Development & Debugging](#development--debugging)
* [Testing](#testing)
* [Deployment](#deployment)
* [Async mode](#async-mode)
* [Limitations](#limitations)
* [Development](#development)
* [Contributing](#contributing)
## Installation ## Installation
@@ -39,11 +63,15 @@ gem 'telegram-bot'
And then execute: And then execute:
$ bundle ```
bundle
```
Or install it yourself as: Or install it yourself as:
$ gem install telegram-bot ```
gem install telegram-bot
```
Require if necessary: Require if necessary:
@@ -53,9 +81,32 @@ require 'telegram/bot'
## Usage ## Usage
### Configuration
While clients can be instantiated explicitly, there is `Telegram.bots_config=` method
to configure app-wide clients, which are accessible via `Telegram.bots`.
It accepts hash of `{bot_id: bot_config}`, and there is special id `:default`
which is used for `Telegram.bot`.
```ruby
Telegram.bots_config = {
default: DEFAULT_BOT_TOKEN,
chat: {
token: CHAT_BOT_TOKEN,
username: 'ChatBot', # to support commands with mentions (/help@ChatBot)
server: 'http://local.bot.api.server', # for Local Bot API Server
},
}
Telegram.bot.get_updates
Telegram.bot == Telegram.bots[:default] # true
Telegram.bots[:chat].send_message(...)
```
### Configuration in Rails app ### Configuration in Rails app
Add `telegram` section into `secrets.yml`: In Rails app `Telegram.bots_config` is read from `secrets.yml` automatically
from `telegram` section:
```yml ```yml
development: development:
@@ -66,6 +117,7 @@ development:
bot: bot:
token: TOKEN token: TOKEN
username: SomeBot username: SomeBot
server: http://local.bot.api.server
# For multiple bots in single app use hash of `internal_bot_id => settings` # For multiple bots in single app use hash of `internal_bot_id => settings`
bots: bots:
@@ -77,6 +129,17 @@ development:
username: ChatBot username: ChatBot
``` ```
For Rails >= 5.2 `Telegram::Bot` searches for config first in credentials and then in secrets.
To use credentials as config store, add telegram section to credentials instead of secrets using
`rails credentials:edit`. In this case be aware of that [Rails < 6.0 may not load
credentials in dev environment by default](https://github.com/telegram-bot-rb/telegram-bot/issues/74#issuecomment-384205609).
In Rails >= 6.0 run `rails credentials:edit --environment development` to configure bot
in each environment.
I suggest not using Rails 5.2 credentials because it can lead to leakage of sensitive data
and it's more difficult to use in multiple environments. See
[secure_credentials](https://github.com/printercu/secure_credentials) gem for better option.
From now clients will be accessible with `Telegram.bots[:chat]` or `Telegram.bots[:auction]`. From now clients will be accessible with `Telegram.bots[:chat]` or `Telegram.bots[:auction]`.
Single bot can be accessed with `Telegram.bot` or `Telegram.bots[:default]`. Single bot can be accessed with `Telegram.bot` or `Telegram.bots[:default]`.
@@ -96,9 +159,9 @@ bot.send_message(chat_id: chat_id, text: 'Test')
``` ```
There is no magic, they just pass params as is and set `path_suffix`. There is no magic, they just pass params as is and set `path_suffix`.
See [`Client`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/client.rb) See [list of available methods](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/client/api_methods.txt)
class for list of available methods. Please open PR or issue if it misses methods from fetched from [API docs](https://core.telegram.org/bots/api).
new API versions. Please open PR or issue if it misses methods from new API versions.
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::Forbidden` 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.
@@ -135,40 +198,41 @@ Bot controllers like usual rails controllers provides features like callbacks,
```ruby ```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController class Telegram::WebhookController < Telegram::Bot::UpdatesController
# use callbacks like in any other controllers # use callbacks like in any other controller
around_action :with_locale around_action :with_locale
# Every update can have one of: message, inline_query, chosen_inline_result, # Every update has one of: message, inline_query, chosen_inline_result,
# callback_query, etc. # callback_query, etc.
# Define method with same name to respond to this updates. # Define method with the same name to handle this type of update.
def message(message) def message(message)
# message can be also accessed via instance method
message == self.payload # true
# store_message(message['text']) # store_message(message['text'])
end end
# This basic methods receives commonly used params: # For the following types of updates commonly used params are passed as arguments,
# full payload object is available with `payload` instance method.
# #
# message(payload) # message(payload)
# inline_query(query, offset) # inline_query(query, offset)
# chosen_inline_result(result_id, query) # chosen_inline_result(result_id, query)
# callback_query(data) # callback_query(data)
# Define public methods to respond to commands. # Define public methods ending with `!` to handle commands.
# Command arguments will be parsed and passed to the method. # Command arguments will be parsed and passed to the method.
# Be sure to use splat args and default values to not get errors when # Be sure to use splat args and default values to not get errors when
# someone passed more or less arguments in the message. # someone passed more or less arguments in the message.
# def start!(word = nil, *other_words)
# For some commands like /message or /123 method names should start with # do_smth_with(word)
# `on_` to avoid conflicts.
def start(data = nil, *) # full message object is also available via `payload` instance method:
# do_smth_with(data) # process_raw_message(payload['text'])
# There are `chat` & `from` shortcut methods. # There are `chat` & `from` shortcut methods.
# For callback queries `chat` if taken from `message` when it's available. # For callback queries `chat` is 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
# `reply_with` also sets `reply_to_message_id`: # `reply_with` also sets `reply_to_message_id`:
reply_with :photo, photo: File.open('party.jpg') reply_with :photo, photo: File.open('party.jpg')
end end
@@ -191,9 +255,10 @@ end
#### Reply helpers #### Reply helpers
There are helpers to respond for basic actions. They just set chat/message/query There are helpers for basic responses. They just set chat/message/query
identifiers from update. See [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) module for more information. identifiers from the update. See
Here are this methods signatures: [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb)
module for more information. Here are these methods signatures:
```ruby ```ruby
def respond_with(type, params); end def respond_with(type, params); end
@@ -201,12 +266,14 @@ def reply_with(type, params); end
def answer_inline_query(results, params = {}); end def answer_inline_query(results, params = {}); end
def answer_callback_query(text, params = {}); end def answer_callback_query(text, params = {}); end
def edit_message(type, params = {}); end def edit_message(type, params = {}); end
def answer_pre_checkout_query(ok, params = {}); end
def answer_shipping_query(ok, 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
`Telegram::Bot::UpdatesPoller::TypedUpdate`: `Telegram::Bot::UpdatesController::TypedUpdate`:
```ruby ```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController class Telegram::WebhookController < Telegram::Bot::UpdatesController
@@ -256,11 +323,11 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# You can override global config for this controller. # You can override global config for this controller.
self.session_store = :file_store self.session_store = :file_store
def write(text = nil, *) def write!(text = nil, *)
session[:text] = text session[:text] = text
end end
def read(*) def read!(*)
respond_with :message, text: session[:text] respond_with :message, text: session[:text]
end end
@@ -283,35 +350,28 @@ it asks you for additional argument. There is `MessageContext` for this:
class Telegram::WebhookController < Telegram::Bot::UpdatesController class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::MessageContext include Telegram::Bot::UpdatesController::MessageContext
def rename(*) def rename!(*)
# set context for the next message # set context for the next message
save_context :rename save_context :rename_from_message
respond_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
# register context handlers to handle this context # register context handlers to handle this context
context_handler :rename do |*words| def rename_from_message(*words)
update_name words[0] update_name words[0]
respond_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
end end
# You can do it in other way: # You can use same action name as context name:
def rename(name = nil, *) def rename!(name = nil, *)
if name if name
update_name name update_name name
respond_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
else else
save_context :rename save_context :rename!
respond_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
end end
# This will call #rename like if it is called with message '/rename %text%'
context_handler :rename
# If you have a lot of such methods you can call this method
# to use context value as action name for all contexts which miss handlers:
context_to_action!
end end
``` ```
@@ -341,7 +401,7 @@ end
### Routes in Rails app ### Routes in Rails app
There is `telegram_webhook` 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. It defines routes at `telegram/#{hash_of(bot.token)}` and connects bots with controller.
```ruby ```ruby
# Most off apps would require # Most off apps would require
@@ -361,7 +421,7 @@ telegram_webhook TelegramAuctionController, :auction
telegram_webhook TelegramController, as: :custom_telegram_webhook telegram_webhook TelegramController, as: :custom_telegram_webhook
``` ```
#### Processesing updates #### Processing updates
To process update with controller call `.dispatch(bot, update)` on it. To process update with controller call `.dispatch(bot, update)` on it.
There are several options to run it automatically: There are several options to run it automatically:
@@ -394,11 +454,11 @@ Telegram::Bot::UpdatesPoller.start(bot, controller_class)
### Testing ### Testing
There is `Telegram::Bot::ClientStub` class to stub client for tests. There is a `Telegram::Bot::ClientStub` class to stub client for tests.
Instead of performing API requests it stores them in `requests` hash. Instead of performing API requests it stores them in a `requests` hash.
To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before
initializing clients. Here is template for RSpec: initializing clients. Here is a template for RSpec:
```ruby ```ruby
# environments/test.rb # environments/test.rb
@@ -416,47 +476,83 @@ RSpec.configure do |config|
end end
``` ```
There are integration and controller contexts for RSpec and some built-in matchers: RSpec contexts and helpers are included automatically for groups and examples with matching
tags. In RSpec < 3.4 it's required to use `include_context` explicitly.
See [list of available helpers](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec)
for details.
There are 3 types of integration tests:
- `:rails` - for testing bot in webhooks-mode in Rails application.
It simulates webhook requests POSTing data to controller's endpoint.
It works on the top of requests specs, so `rspec-rails` gem is required.
- `:rack` - For testing bot in webhooks-mode in non-Rails application.
It uses `rack-test` gem to POST requests to bot's endpoint.
- `:poller` - Calls `.dispatch` directly on controller class.
Pick the appropriate one, then require `telegram/bot/rspec/integration/#{type}`
and mark spec group with tag `telegram_bot: type`. See configuration options
for each type in
[telegram/bot/rspec/integration/](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec/integration).
Here is an example test for a Rails app:
```ruby ```ruby
# spec/requests/telegram_webhooks_spec.rb # spec/requests/telegram_webhooks_spec.rb
require 'telegram/bot/rspec/integration' require 'telegram/bot/rspec/integration/rails'
RSpec.describe TelegramWebhooksController, :telegram_bot do RSpec.describe TelegramWebhooksController, telegram_bot: :rails do
# for old rspec add: # for old RSpec:
# include_context 'telegram/bot/integration' # include_context 'telegram/bot/integration/rails'
describe '#start' do # Main method is #dispatch(update). Some helpers are:
# dispatch_message(text, options = {})
# dispatch_command(cmd, *args)
# Available matchers can be found in Telegram::Bot::RSpec::ClientMatchers.
it 'shows usage of basic matchers' do
# The most basic one is #make_telegram_request(bot, action).
# It works similar to `receive` matcher and supports chaining `.with(args).exactly(n).times`.
expect { dispatch_command(:start) }.to make_telegram_request(bot, :sendMessage).
with(hash_including(text: 'msg text'))
# There are some shortcuts for dispatching basic updates and testing responses.
expect { dispatch_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option)
end
describe '#start!' do
subject { -> { dispatch_command :start } } subject { -> { dispatch_command :start } }
# Using built in matcher for `respond_to`:
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. # There is context for callback queries with related matchers,
# use :callback_query tag to include it.
describe '#hey_callback_query', :callback_query do describe '#hey_callback_query', :callback_query do
let(:data) { "hey:#{name}" } let(:data) { "hey:#{name}" }
let(:name) { 'Joe' } let(:name) { 'Joe' }
it { should answer_callback_query('Hey Joe') } it { should answer_callback_query('Hey Joe') }
it { should edit_current_message :text, text: 'Done' } it { should edit_current_message :text, text: 'Done' }
end
end end
# For controller specs use
require 'telegram/bot/updates_controller/rspec_helpers'
RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do
# for old rspec add:
# include_context 'telegram/bot/updates_controller'
end
# Matchers are available for custom specs:
include Telegram::Bot::RSpec::ClientMatchers
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'))
``` ```
Place integration tests inside `spec/requests` There is a context for testing bot controller in the way similar to Rails controller tests.
when using RSpec's `infer_spec_type_from_file_location!`, It's supposed to be a low-level alternative for integration tests. Among the differences is
or just add `type: :request` to `describe`. that controller tests use a single controller instance for all dispatches in specific example,
session is stubbed (does not use configured store engine), and update is not serialized
so it also supports mocks. This can be useful for unit testing, but should not be used as
the default way to test the bot.
```ruby
require 'telegram/bot/updates_controller/rspec_helpers'
RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do
# for old RSpec:
# include_context 'telegram/bot/updates_controller'
# Same helpers and matchers like dispatch_command, answer_callback_query are available here.
end
```
See sample app for more examples. See sample app for more examples.
@@ -466,48 +562,12 @@ While webhooks-mode is prefered, poller still can be used in production.
See [comparison and examples](https://github.com/telegram-bot-rb/telegram-bot/wiki/Deployment) See [comparison and examples](https://github.com/telegram-bot-rb/telegram-bot/wiki/Deployment)
for details. for details.
### Botan.io metrics
Initialize with `bot = Telegram::Bot::Client.new(token, botan: 'botan token')`
or just add `botan` key in `secrets.yml`:
```yml
telegram:
bot:
token: bot_token
botan: botan_token
```
Access to Botan client with `bot.botan`.
Use `bot.botan.track(event, uid, payload)` to track events.
There are some helpers for controllers in `Telegram::Bot::Botan::ControllerHelpers`:
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::Botan::ControllerHelpers
# This will track with event: action_name & data: payload
before_action :botan_track_action
def smth(*)
# This will track event for current user only when botan is configured.
botan_track :my_event, custom_data
# or get access directly to botan client:
botan.track(...)
end
end
```
There is no stubbing for botan clients, so don't set botan token in tests.
### Async mode ### Async mode
There is built in support for async requests using ActiveJob. Without Rails There is built in support for async requests using ActiveJob. Without Rails
you can implement your own worker class to handle such requests. This allows: you can implement your own worker class to handle such requests. This allows:
- Process updates very fast, without waiting for telegram and botan responses. - Process updates very fast, without waiting for telegram responses.
- Handle and retry network and other errors with queue adapter. - Handle and retry network and other errors with queue adapter.
- ??? - ???
@@ -515,7 +575,7 @@ 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 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. 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 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).

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

@@ -25,7 +25,7 @@ method_list = headers.
map { |g| g.reject { |x| x.match?(NOT_METHOD_REGEXP) } }. map { |g| g.reject { |x| x.match?(NOT_METHOD_REGEXP) } }.
reject(&:empty?) reject(&:empty?)
api_version = doc.text.match(/^(Bot API ([\d\.]+))\.?$/) api_version = doc.text.match(/^(?:Introducing )?(Bot API ([\d\.]+))\.?$/)
result = ['# Generated with bin/fetch-telegram-methods'] result = ['# Generated with bin/fetch-telegram-methods']
result << "# #{api_version[1]}" if api_version result << "# #{api_version[1]}" if api_version

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

@@ -2,20 +2,20 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "actionpack", "~> 4.2" gem "actionpack", "~> 4.2.0"
gem "railties", "~> 4.2" gem "railties", "~> 4.2.0"
group :development do group :development do
gem "appraisal", "~> 2.2" gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1" gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.2.0" gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 0.4.1" gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.0" gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.5.0" gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.1.0" gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 3.5.0" gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1" gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false gem "coveralls", "~> 0.8.23", require: false
end end
gemspec path: "../" gemspec path: "../"

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

@@ -2,20 +2,20 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "actionpack", "~> 5.0" gem "actionpack", "~> 5.0.0"
gem "railties", "~> 5.0" gem "railties", "~> 5.0.0"
group :development do group :development do
gem "appraisal", "~> 2.2" gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1" gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.2.0" gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 0.4.1" gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.0" gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.5.0" gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.1.0" gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 3.5.0" gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1" gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false gem "coveralls", "~> 0.8.23", require: false
end end
gemspec path: "../" gemspec path: "../"

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

@@ -2,20 +2,20 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "actionpack", "~> 5.1" gem "actionpack", "~> 5.1.0"
gem "railties", "~> 5.1" gem "railties", "~> 5.1.0"
group :development do group :development do
gem "appraisal", "~> 2.2" gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1" gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.2.0" gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 0.4.1" gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.0" gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.5.0" gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.1.0" gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 3.5.0" gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1" gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false gem "coveralls", "~> 0.8.23", require: false
end end
gemspec path: "../" gemspec path: "../"

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

@@ -2,20 +2,20 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "actionpack", "~> 5.2.0.beta2" gem "actionpack", "~> 5.2.0"
gem "railties", "~> 5.2.0.beta2" gem "railties", "~> 5.2.0"
group :development do group :development do
gem "appraisal", "~> 2.2" gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1" gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.2.0" gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 0.4.1" gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.0" gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.5.0" gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.1.0" gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 3.5.0" gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1" gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false gem "coveralls", "~> 0.8.23", require: false
end end
gemspec path: "../" gemspec path: "../"

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

@@ -2,22 +2,20 @@
source "https://rubygems.org" source "https://rubygems.org"
git "https://github.com/rails/rails.git" do gem "actionpack", "~> 6.0.0"
gem "actionpack", "~> 6.0.0.alpha" gem "railties", "~> 6.0.0"
gem "railties", "~> 6.0.0.alpha"
end
group :development do group :development do
gem "appraisal", "~> 2.2" gem "appraisal", "~> 2.2"
gem "pry", "~> 0.10.1" gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.2.0" gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 0.4.1" gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.0" gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.5.0" gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.1.0" gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 3.5.0" gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1" gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.2", require: false gem "coveralls", "~> 0.8.23", require: false
end end
gemspec path: "../" gemspec path: "../"

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 6.1.0"
gem "railties", "~> 6.1.0"
group :development do
gem "appraisal", "~> 2.2"
gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 7.0.0"
gem "railties", "~> 7.0.0"
group :development do
gem "appraisal", "~> 2.2"
gem "pry", "~> 0.13.1"
gem "pry-byebug", "~> 3.9.0"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.10.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 0.52.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -14,15 +14,22 @@ namespace :telegram do
desc 'Set webhook urls for all bots' desc 'Set webhook urls for all bots'
task set_webhook: :environment do task set_webhook: :environment do
routes = Rails.application.routes.url_helpers Telegram::Bot::Tasks.set_webhook
cert_file = ENV['CERT'] end
cert = File.open(cert_file) if cert_file
Telegram.bots.each do |key, bot| desc 'Delete webhooks for all or specific BOT'
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) task :delete_webhook do
url = routes.send("#{route_name}_url") Telegram::Bot::Tasks.delete_webhook
puts "Setting webhook for #{key}..." end
bot.async(false) { bot.set_webhook(url: url, certificate: cert) }
end desc 'Perform logOut command for all or specific BOT'
task :log_out do
Telegram::Bot::Tasks.log_out
end
desc 'Perform `close` command for all or specific BOT'
task :close do
Telegram::Bot::Tasks.close
end end
end end
end end

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

@@ -14,21 +14,20 @@ module Telegram
module_function module_function
def deprecation_0_14 def deprecation_0_16
@deprecation ||= begin @deprecation ||= begin
require 'active_support/deprecation' require 'active_support/deprecation'
ActiveSupport::Deprecation.new('0.14', 'Telegram::Bot') ActiveSupport::Deprecation.new('0.16', 'Telegram::Bot')
end end
end end
autoload :Async, 'telegram/bot/async' autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client' autoload :Client, 'telegram/bot/client'
autoload :ClientStub, 'telegram/bot/client_stub' autoload :ClientStub, 'telegram/bot/client_stub'
autoload :DebugClient, 'telegram/bot/debug_client' autoload :DebugClient, 'telegram/bot/debug_client'
autoload :Initializers, 'telegram/bot/initializers'
autoload :Middleware, 'telegram/bot/middleware' autoload :Middleware, 'telegram/bot/middleware'
autoload :RSpec, 'telegram/bot/rspec' autoload :RSpec, 'telegram/bot/rspec'
autoload :Tasks, 'telegram/bot/tasks'
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

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

@@ -1,28 +1,23 @@
module Telegram module Telegram
module Bot module Bot
# Telegram & Botan clients can perform requests in async way with # Telegram clients can perform requests in async way with
# any job adapter (ActiveJob by default). Using Rails you don't need any # any job adapter (ActiveJob by default). Using Rails you don't need any
# additional configuration. However you may want to enable async requests # additional configuration. However you may want to enable async requests
# by default with `async: true` in `secrets.yml`. Botan client doesn't inherit # by default with `async: true` in `secrets.yml`.
# async setting from client and must be configured separately.
# #
# telegram: # telegram:
# bots: # bots:
# chat_async: # chat_async:
# token: secret # token: secret
# async: true # enable async mode for client # async: true # enable async mode for client
# botan: botan_token # in this way botan will not be async
# botan: # in this way - it's in async mode
# token: botan_token
# async: true
# #
# Without Rails To start using async requests # Without Rails To start using async requests
# initialize client with `id` kwarg and make sure the client is # initialize client with `id` kwarg and make sure the client is
# accessible via `Teletgram.bots[id]` in job worker. Or just use # accessible via `Teletgram.bots[id]` in job worker. Or just use
# `Telegram.bots_config=` for configuration. # `Telegram.bots_config=` for configuration.
# #
# Being in async mode `#request` enqueues job instead to perform # Being in async mode `#request` enqueues job to perform
# http request instead of performing it directly. # http request instead of performing it immediately.
# Async behavior is controlled with `#async=` writer # Async behavior is controlled with `#async=` writer
# and can be enabled/disabled for the block with `#async`: # and can be enabled/disabled for the block with `#async`:
# #
@@ -34,7 +29,7 @@ module Telegram
# while `#async(val, &block)` is thread-safe. # while `#async(val, &block)` is thread-safe.
# #
# It can be set with custom job class or classname. By default it defines # It can be set with custom job class or classname. By default it defines
# job classes for every client class, inherited from ApplicationRecord, which # job classes inherited from ApplicationJob, which
# can be accessed via `.default_async_job`. You can integrate it with any # can be accessed via `.default_async_job`. You can integrate it with any
# other job provider by defining a class with `.perform_later(bot_id, *args)` # other job provider by defining a class with `.perform_later(bot_id, *args)`
# method. See Async::Job for implemetation. # method. See Async::Job for implemetation.

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

@@ -1,53 +0,0 @@
module Telegram
module Bot
class Botan
TRACK_URI = 'https://api.botan.io/track'.freeze
autoload :ClientHelpers, 'telegram/bot/botan/client_helpers'
autoload :ControllerHelpers, 'telegram/bot/botan/controller_helpers'
class Error < Bot::Error; end
extend Initializers
prepend Async
include DebugClient
class << self
def by_id(id)
Telegram.botans[id]
end
def prepare_async_args(method, uri, query = {}, body = nil)
[method.to_s, uri.to_s, Async.prepare_hash(query), body]
end
end
attr_reader :client, :token
def initialize(token = nil, **options)
@client = HTTPClient.new
@token = token || options[:token]
end
def track(event, uid, payload = {})
request(:post, TRACK_URI, {name: event, uid: uid}, payload.to_json)
end
def request(method, uri, query = {}, body = nil)
res = http_request(method, uri, query.merge(token: token), body)
status = res.status
return JSON.parse(res.body) if status < 300
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = "#{res.reason}: #{result && result['info'] || '-'}"
raise Error, err_msg
end
def http_request(method, uri, query, body)
client.request(method, uri, query, body)
end
def inspect
"#<#{self.class.name}##{object_id}(#{@id})>"
end
end
end
end

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

@@ -1,15 +0,0 @@
module Telegram
module Bot
class Botan
# Helpers for botan.io metrics.
module ClientHelpers
attr_reader :botan
def initialize(*, botan: nil, **options)
super
@botan = Botan.wrap(botan, id: id) if botan
end
end
end
end
end

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

@@ -1,33 +0,0 @@
module Telegram
module Bot
class Botan
# Helpers for botan.io metrics.
module ControllerHelpers
class MissingFrom < Error; end
protected
def botan
@botan ||= bot.try!(:botan)
end
# Track custom event for user taken from `from` field:
#
# botan_track :my_event, {data: :val}
#
def botan_track(event, data = {})
raise MissingFrom, 'Can not track without user' unless from
botan.try! { |x| x.track(event, from['id'], data) }
end
# Track current action and payload for current user. Best used with `before_action`:
#
# before_action :botan_track_action
#
def botan_track_action
botan_track(action_name, payload)
end
end
end
end
end

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

@@ -1,21 +1,32 @@
require 'json' require 'active_support/core_ext/hash/keys'
require 'httpclient' require 'httpclient'
module Telegram module Telegram
module Bot module Bot
class Client class Client
URL_TEMPLATE = 'https://api.telegram.org/bot%<token>s/'.freeze SERVER = 'https://api.telegram.org'.freeze
URL_TEMPLATE = '%<server>s/bot%<token>s/'.freeze
autoload :RequestBodyFormatter, 'telegram/bot/client/request_body_formatter'
autoload :TypedResponse, 'telegram/bot/client/typed_response' autoload :TypedResponse, 'telegram/bot/client/typed_response'
extend Initializers
prepend Async prepend Async
prepend Botan::ClientHelpers
include DebugClient include DebugClient
require 'telegram/bot/client/api_helper' require 'telegram/bot/client/api_helper'
include ApiHelper include ApiHelper
class << self class << self
# Accepts different options to initialize bot.
def wrap(input, **options)
case input
when Symbol then by_id(input) or raise "#{name} #{input.inspect} not configured"
when self then input
when Hash then new(**input.symbolize_keys, **options)
else new(input, **options)
end
end
def by_id(id) def by_id(id)
Telegram.bots[id] Telegram.bots[id]
end end
@@ -25,16 +36,8 @@ module Telegram
prepend TypedResponse prepend TypedResponse
end end
# Encodes nested hashes as json.
def prepare_body(body)
body = body.dup
body.each do |k, val|
body[k] = val.to_json if val.is_a?(Hash) || val.is_a?(Array)
end
end
def prepare_async_args(action, body = {}) def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(prepare_body(body))] [action.to_s, Async.prepare_hash(RequestBodyFormatter.format(body, action))]
end end
def error_for_response(response) def error_for_response(response)
@@ -52,15 +55,15 @@ module Telegram
attr_reader :client, :token, :username, :base_uri attr_reader :client, :token, :username, :base_uri
def initialize(token = nil, username = nil, **options) def initialize(token = nil, username = nil, server: SERVER, **options)
@client = HTTPClient.new @client = HTTPClient.new
@token = token || options[:token] @token = token || options[:token]
@username = username || options[:username] @username = username || options[:username]
@base_uri = format(URL_TEMPLATE, token: self.token) @base_uri = format(URL_TEMPLATE, server: server, token: self.token)
end end
def request(action, body = {}) def request(action, body = {})
response = http_request("#{base_uri}#{action}", self.class.prepare_body(body)) response = http_request("#{base_uri}#{action}", RequestBodyFormatter.format(body, action))
raise self.class.error_for_response(response) if response.status >= 300 raise self.class.error_for_response(response) if response.status >= 300
JSON.parse(response.body) JSON.parse(response.body)
end end

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

@@ -1,5 +1,5 @@
# Generated with bin/fetch-telegram-methods # Generated with bin/fetch-telegram-methods
# Bot API 3.5. # Bot API 5.7
getUpdates getUpdates
setWebhook setWebhook
@@ -7,12 +7,16 @@ deleteWebhook
getWebhookInfo getWebhookInfo
getMe getMe
logOut
close
sendMessage sendMessage
forwardMessage forwardMessage
copyMessage
sendPhoto sendPhoto
sendAudio sendAudio
sendDocument sendDocument
sendVideo sendVideo
sendAnimation
sendVoice sendVoice
sendVideoNote sendVideoNote
sendMediaGroup sendMediaGroup
@@ -21,32 +25,49 @@ editMessageLiveLocation
stopMessageLiveLocation stopMessageLiveLocation
sendVenue sendVenue
sendContact sendContact
sendPoll
sendDice
sendChatAction sendChatAction
getUserProfilePhotos getUserProfilePhotos
getFile getFile
kickChatMember banChatMember
unbanChatMember unbanChatMember
restrictChatMember restrictChatMember
promoteChatMember promoteChatMember
setChatAdministratorCustomTitle
banChatSenderChat
unbanChatSenderChat
setChatPermissions
exportChatInviteLink exportChatInviteLink
createChatInviteLink
editChatInviteLink
revokeChatInviteLink
approveChatJoinRequest
declineChatJoinRequest
setChatPhoto setChatPhoto
deleteChatPhoto deleteChatPhoto
setChatTitle setChatTitle
setChatDescription setChatDescription
pinChatMessage pinChatMessage
unpinChatMessage unpinChatMessage
unpinAllChatMessages
leaveChat leaveChat
getChat getChat
getChatAdministrators getChatAdministrators
getChatMembersCount getChatMemberCount
getChatMember getChatMember
setChatStickerSet setChatStickerSet
deleteChatStickerSet deleteChatStickerSet
answerCallbackQuery answerCallbackQuery
setMyCommands
deleteMyCommands
getMyCommands
editMessageText editMessageText
editMessageCaption editMessageCaption
editMessageMedia
editMessageReplyMarkup editMessageReplyMarkup
stopPoll
deleteMessage deleteMessage
sendSticker sendSticker
@@ -56,6 +77,7 @@ createNewStickerSet
addStickerToSet addStickerToSet
setStickerPositionInSet setStickerPositionInSet
deleteStickerFromSet deleteStickerFromSet
setStickerSetThumb
answerInlineQuery answerInlineQuery
@@ -63,6 +85,8 @@ sendInvoice
answerShippingQuery answerShippingQuery
answerPreCheckoutQuery answerPreCheckoutQuery
setPassportDataErrors
sendGame sendGame
setGameScore setGameScore
getGameHighScores getGameHighScores

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

@@ -0,0 +1,61 @@
require 'json'
module Telegram
module Bot
class Client
# Encodes nested hashes and arrays as json and extract File objects from them
# to the top level. Top-level File objects are handled by httpclient.
# More details: https://core.telegram.org/bots/api#sending-files
module RequestBodyFormatter
extend self
def format(body, action)
body = body.dup
case action.to_s
when 'sendMediaGroup'
extract_files_from_array!(body, :media)
when 'editMessageMedia'
replace_field(body, :media) do |value|
files = {}
extract_files_from_hash(value, files).tap { body.merge!(files) }
end
end
body.each do |key, val|
body[key] = val.to_json if val.is_a?(Hash) || val.is_a?(Array)
end
end
private
# Detects field by symbol or string name and replaces it with mapped value.
def replace_field(hash, field_name)
field_name = [field_name.to_sym, field_name.to_s].find { |x| hash.key?(x) }
hash[field_name] = yield hash[field_name] if field_name
end
def extract_files_from_array!(hash, field_name)
replace_field(hash, field_name) do |value|
break value unless value.is_a?(Array)
files = {}
value.map { |x| extract_files_from_hash(x, files) }.
tap { hash.merge!(files) }
end
end
# Replace File objects with `attach` URIs. File objects are added into `files` hash.
def extract_files_from_hash(hash, files)
return hash unless hash.is_a?(Hash)
hash.transform_values do |value|
if value.is_a?(File)
arg_name = "_file#{files.size}"
files[arg_name] = value
"attach://#{arg_name}"
else
value
end
end
end
end
end
end
end

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

@@ -5,11 +5,11 @@ module Telegram
attr_reader :requests attr_reader :requests
module StubbedConstructor module StubbedConstructor
def new(*args) def new(*args, **kwargs)
if self == ClientStub || !ClientStub.stub_all? if self == ClientStub || !ClientStub.stub_all?
super super
else else
ClientStub.new(*args) ClientStub.new(*args, **kwargs)
end end
end end
end end

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

@@ -40,11 +40,6 @@ module Telegram
end end
end end
# Hash of botan clients made from #bots.
def botans
@botans ||= bots.map { |k, v| [k, v.botan] }.to_h
end
# Returns config for .bots method. By default uses `telegram['bots']` section # Returns config for .bots method. By default uses `telegram['bots']` section
# from `secrets.yml` merging `telegram['bot']` at `:default` key. # from `secrets.yml` merging `telegram['bot']` at `:default` key.
# #
@@ -53,10 +48,12 @@ module Telegram
@bots_config ||= @bots_config ||=
if defined?(Rails.application) if defined?(Rails.application)
app = Rails.application app = Rails.application
store = app.respond_to?(:credentials) ? app.credentials : app.secrets store = app.credentials[:telegram] if app.respond_to?(:credentials)
secrets = store.fetch(:telegram, {}).with_indifferent_access store ||= app.secrets[:telegram] if app.respond_to?(:secrets)
secrets.fetch(:bots, {}).symbolize_keys.tap do |config| store ||= {}
default = secrets[:bot] store = store.with_indifferent_access
store.fetch(:bots, {}).symbolize_keys.tap do |config|
default = store[:bot]
config[:default] = default if default config[:default] = default if default
end end
else else
@@ -69,7 +66,6 @@ module Telegram
@bots = nil @bots = nil
@bot = nil @bot = nil
@bots_config = nil @bots_config = nil
@botans = nil
end end
end end
end end

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

@@ -1,21 +0,0 @@
require 'active_support/core_ext/hash/keys'
module Telegram
module Bot
module Initializers
# Accepts different options to initialize bot.
def wrap(input, **options)
case input
when Symbol then by_id(input) or raise "#{name} #{input.inspect} not configured"
when self then input
when Hash then new(**input.symbolize_keys, **options)
else new(input, **options)
end
end
def by_id(_id)
raise 'Not implemented'
end
end
end
end

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

@@ -1,8 +1,7 @@
require 'active_support/concern' require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/json' require 'active_support/json'
require 'action_dispatch/http/mime_type' require 'action_dispatch'
require 'action_dispatch/http/request'
module Telegram module Telegram
module Bot module Bot

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

@@ -1,3 +1,6 @@
require 'base64'
require 'openssl'
require 'telegram/bot' require 'telegram/bot'
require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/wrap'
@@ -17,37 +20,8 @@ module Telegram
end || 'telegram_webhook' end || 'telegram_webhook'
end end
# Replaces colon with underscore so rails don't treat it as def token_hash(token)
# route parameter. Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.digest(token), padding: false)
def escape_token(token)
token && token.tr(':', '_')
end
end
# # Create routes for all Telegram.bots to use same controller:
# telegram_webhooks TelegramController
#
# # Or pass custom bots usin any of supported config options:
# 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|
controller, bot_options = controller if controller.is_a?(Array)
telegram_webhook(controller, bot, options.merge(bot_options || {}))
end end
end end
@@ -61,14 +35,17 @@ module Telegram
# #
# # pass additional options # # pass additional options
# telegram_webhook TelegramController, :default, as: :custom_route_name # telegram_webhook TelegramController, :default, as: :custom_route_name
def telegram_webhook(controller, bot = :default, **options) #
# # Default path is generated using hashed bot token. Override it using:
# telegram_webhook TelegramController, :default, path: 'top/secret'
def telegram_webhook(controller, bot = :default, path: nil, **options)
bot = Client.wrap(bot) bot = Client.wrap(bot)
params = { params = {
to: Middleware.new(bot, controller), to: Middleware.new(bot, controller),
as: RoutesHelper.route_name_for_bot(bot), as: RoutesHelper.route_name_for_bot(bot),
format: false, format: false,
}.merge!(options) }.merge!(options)
post("telegram/#{RoutesHelper.escape_token bot.token}", params) post(path || "telegram/#{bot.token && RoutesHelper.token_hash(bot.token)}", params)
UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode? UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode?
end end
end end

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

@@ -2,6 +2,15 @@ module Telegram
module Bot module Bot
module RSpec module RSpec
autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers' autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers'
module_function
# Yelds a block if `include_context` is supported.
def with_include_context
::RSpec.configure do |config|
yield(config) if config.respond_to?(:include_context)
end
end
end end
end end
end end

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

@@ -0,0 +1,39 @@
require 'telegram/bot/rspec'
require 'telegram/bot/rspec/message_helpers'
# Shared helpers for testing callback query updates.
RSpec.shared_context 'telegram/bot/callback_query' do
include_context 'telegram/bot/message_helpers'
subject { -> { dispatch callback_query: payload } }
let(:payload) { {id: callback_query_id, from: from, message: message, data: data} }
let(:callback_query_id) { 11 }
let(:message_id) { 22 }
let(:message) { {message_id: message_id, chat: chat, text: 'message text'} }
let(:data) { raise '`let(:data) { "callback query data here" }` is required' }
# 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

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

@@ -1,85 +0,0 @@
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(:controller_path) do
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
Rails.application.routes.url_helpers.public_send("#{route_name}_path")
end
let(:request_headers) do
{
'ACCEPT' => 'application/json',
'Content-Type' => 'application/json',
}
end
let(:clear_session?) { described_class.respond_to?(:session_store) }
before { described_class.session_store.try!(:clear) if clear_session? }
include Telegram::Bot::RSpec::ClientMatchers
def dispatch(update)
if ActionPack::VERSION::MAJOR >= 5
post(controller_path, params: update.to_json, headers: request_headers)
else
post(controller_path, update.to_json, request_headers)
end
end
def dispatch_message(text, options = {})
dispatch message: default_message_options.merge(options).merge(text: text)
end
def dispatch_command(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
dispatch_message("/#{args.join ' '}", options)
end
# Matcher to check response. Make sure to define `let(:chat_id)`.
def respond_with_message(expected = Regexp.new(''))
raise 'Define chat_id to use respond_with_message' unless defined?(chat_id)
send_telegram_message(bot, expected, chat_id: chat_id)
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|
if config.respond_to?(:include_context)
config.include_context 'telegram/bot/integration', :telegram_bot
config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query
end
end

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

@@ -0,0 +1,14 @@
require 'telegram/bot/rspec/integration/shared'
RSpec.shared_context 'telegram/bot/integration/poller' do
include_context 'telegram/bot/integration/shared'
let(:controller_class) { described_class }
def dispatch(update)
controller_class.dispatch(bot, update.as_json)
end
end
Telegram::Bot::RSpec.with_include_context do |config|
config.include_context 'telegram/bot/integration/poller', telegram_bot: :poller
end

24
lib/telegram/bot/rspec/integration/rack.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
require 'telegram/bot/rspec/integration/shared'
require 'rack/test'
RSpec.shared_context 'telegram/bot/integration/rack' do
include_context 'telegram/bot/integration/shared'
include Rack::Test::Methods
let(:request_path) { raise '`let(:request_path) { path to bot }` is required' }
let(:app) { raise '`let(:app) { your rack app here }` is required' }
let(:request_headers) do
{
'ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/json',
}
end
def dispatch(update)
post request_path, update.to_json, request_headers
end
end
Telegram::Bot::RSpec.with_include_context do |config|
config.include_context 'telegram/bot/integration/rack', telegram_bot: :rack
end

28
lib/telegram/bot/rspec/integration/rails.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,28 @@
require 'telegram/bot/rspec/integration/shared'
RSpec.shared_context 'telegram/bot/integration/rails', type: :request do
include_context 'telegram/bot/integration/shared'
let(:controller_path) do
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
Rails.application.routes.url_helpers.public_send("#{route_name}_path")
end
let(:request_headers) do
{
'Accept' => 'application/json',
'Content-Type' => 'application/json',
}
end
def dispatch(update)
if ActionPack::VERSION::MAJOR >= 5
post(controller_path, params: update.to_json, headers: request_headers)
else
post(controller_path, update.to_json, request_headers)
end
end
end
Telegram::Bot::RSpec.with_include_context do |config|
config.include_context 'telegram/bot/integration/rails', telegram_bot: :rails
end

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

@@ -0,0 +1,14 @@
require 'active_support/json'
require 'telegram/bot'
require 'telegram/bot/rspec/message_helpers'
require 'telegram/bot/rspec/callback_query_helpers'
RSpec.shared_context 'telegram/bot/integration/shared' do
include Telegram::Bot::RSpec::ClientMatchers
include_context 'telegram/bot/message_helpers'
include_context 'telegram/bot/callback_query', :callback_query
let(:bot) { Telegram.bot }
let(:clear_session?) { described_class.respond_to?(:session_store) }
before { described_class.session_store.try!(:clear) if clear_session? }
end

26
lib/telegram/bot/rspec/message_helpers.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
# Shared helpers for testing message updates.
RSpec.shared_context 'telegram/bot/message_helpers' do
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 }
# Shortcut for dispatching messages with default params.
def dispatch_message(text, options = {})
dispatch message: default_message_options.merge(options).merge(text: text)
end
# Dispatch command message.
def dispatch_command(cmd, *args)
options = args.last.is_a?(Hash) ? args.pop : {}
args.unshift("/#{cmd}")
dispatch_message(args.join(' '), options)
end
# Matcher to check response. Make sure to define `let(:chat_id)`.
def respond_with_message(expected = Regexp.new(''))
raise 'Define chat_id to use respond_with_message' unless defined?(chat_id)
send_telegram_message(bot, expected, chat_id: chat_id)
end
end

63
lib/telegram/bot/tasks.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,63 @@
# frozen_string_literal: true
module Telegram
module Bot
module Tasks
extend self
def set_webhook
routes = Rails.application.routes.url_helpers
cert_file = ENV['CERT']
cert = File.open(cert_file) if cert_file
each_bot do |key, bot|
route_name = RoutesHelper.route_name_for_bot(bot)
url = routes.send("#{route_name}_url")
say("Setting webhook for #{key}...")
bot.set_webhook(
url: url,
certificate: cert,
ip_address: ENV['IP_ADDRESS'],
drop_pending_updates: drop_pending_updates,
)
end
end
def delete_webhook
each_bot do |key, bot|
say("Deleting webhook for #{key}...")
bot.delete_webhook(drop_pending_updates: drop_pending_updates)
end
end
def log_out
each_bot do |key, bot|
say("Logging out #{key}...")
bot.log_out
end
end
def close
each_bot do |key, bot|
say("Closing #{key}...")
bot.close
end
end
private
def say(text)
puts(text) unless Rails.env.test? # rubocop:disable Rails/Output
end
def each_bot(&block)
id = ENV['BOT'].try!(:to_sym)
bots = id ? {id => Client.by_id(id)} : Telegram.bots
bots.each { |key, bot| bot.async(false) { block[key, bot] } }
end
def drop_pending_updates
ENV['DROP_PENDING_UPDATES'].try!(:downcase) == 'true'
end
end
end
end

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

@@ -1,4 +1,5 @@
require 'abstract_controller' require 'abstract_controller'
require 'active_support/core_ext/string/inflections'
require 'active_support/callbacks' require 'active_support/callbacks'
require 'active_support/version' require 'active_support/version'
@@ -6,13 +7,13 @@ module Telegram
module Bot module Bot
# Base class to create update processors. With callbacks, session and helpers. # Base class to create update processors. With callbacks, session and helpers.
# #
# Define public methods for each command and they will be called when # Public methods ending with `!` handle messages with commands.
# update has this command. Message is automatically parsed and # Message text is automatically parsed into method arguments.
# words are passed as method arguments. Be sure to use default values and # Be sure to use default values and
# splat arguments in every action method to not get errors, when user # splat arguments in every action method to not get errors, when user
# sends command without necessary args / with extra args. # sends command without necessary args / with extra args.
# #
# def start(token = nil, *) # def start!(token = nil, *)
# if token # if token
# # ... # # ...
# else # else
@@ -20,25 +21,21 @@ module Telegram
# end # end
# end # end
# #
# def help(*) # def help!(*)
# respond_with :message, text: # respond_with :message, text:
# end # end
# #
# To process plain text messages (without commands) or other updates just # To process plain text messages (without commands) or other updates just
# define public method with name of payload type. They will receive payload # define public method with name of payload type.
# as an argument. # By default they receive payload as an argument, but some of them are called
# with more usefuk args:
# #
# def message(message) # def message(message)
# respond_with :message, text: "Echo: #{message['text']}" # respond_with :message, text: "Echo: #{message['text']}"
# end # end
# #
# def inline_query(query) # def inline_query(query, offset)
# answer_inline_query results_for_query(query), is_personal: true # answer_inline_query results_for_query(query, offset), is_personal: true
# end
#
# # To process conflicting commands (`/message args`) just use `on_` prefix:
# def on_message(*args)
# # ...
# end # end
# #
# To process update run: # To process update run:
@@ -54,12 +51,14 @@ module Telegram
abstract! abstract!
%w[ %w[
instrumentation Commands
log_subscriber Instrumentation
reply_helpers LogSubscriber
rescue ReplyHelpers
session Rescue
].each { |file| require "telegram/bot/updates_controller/#{file}" } Session
Translation
].each { |name| require "telegram/bot/updates_controller/#{name.underscore}" }
%w[ %w[
CallbackQueryContext CallbackQueryContext
@@ -78,9 +77,12 @@ module Telegram
skip_after_callbacks_if_terminated: true skip_after_callbacks_if_terminated: true
end end
include AbstractController::Translation include Commands
include Rescue include Rescue
include ReplyHelpers include ReplyHelpers
include Translation
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
include Instrumentation include Instrumentation
extend Session::ConfigMethods extend Session::ConfigMethods
@@ -95,9 +97,11 @@ module Telegram
callback_query callback_query
shipping_query shipping_query
pre_checkout_query pre_checkout_query
poll
poll_answer
my_chat_member
chat_member
].freeze ].freeze
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
class << self class << self
# Initialize controller and process update. # Initialize controller and process update.
@@ -105,27 +109,6 @@ module Telegram
new(*args).dispatch new(*args).dispatch
end end
# Overrid it to filter or transform commands.
# Default implementation is to convert to downcase and add `on_` prefix
# for conflicting commands.
def action_for_command(cmd)
cmd.downcase!
cmd.match(CONFLICT_CMD_REGEX) ? "on_#{cmd}" : cmd
end
# Fetches command from text message. All subsequent words are returned
# as arguments.
# If command has mention (eg. `/test@SomeBot`), it returns commands only
# for specified username. Set `username` to `true` to accept
# any commands.
def command_from_text(text, username = nil)
return unless text
match = text.match(CMD_REGEX)
return unless match
mention = match[3]
[match[1], text.split.drop(1)] if username == true || !mention || mention == username
end
def payload_from_update(update) def payload_from_update(update)
update && PAYLOAD_TYPES.find do |type| update && PAYLOAD_TYPES.find do |type|
item = update[type] item = update[type]
@@ -134,8 +117,7 @@ module Telegram
end end
end end
attr_internal_reader :update, :bot, :payload, :payload_type, :is_command attr_internal_reader :update, :bot, :payload, :payload_type
alias_method :command?, :is_command
delegate :username, to: :bot, prefix: true, allow_nil: true delegate :username, to: :bot, prefix: true, allow_nil: true
# Second argument can be either update object with hash access & string # Second argument can be either update object with hash access & string
@@ -170,59 +152,73 @@ module Telegram
# Accessor to `'from'` field of payload. Can be overriden with `from` option # Accessor to `'from'` field of payload. Can be overriden with `from` option
# for #initialize. # for #initialize.
def from def from
@_from ||= payload && payload['from'] @_from ||= payload.is_a?(Hash) ? payload['from'] : payload.try(:from)
end end
# Processes current update. # Processes current update.
def dispatch def dispatch
@_is_command, action, args = action_for_payload action, args = action_for_payload
process(action, *args) process(action, *args)
end end
attr_internal_reader :action_options
# It provides support for passing array as action, where first vaule
# is action name and second is action metadata.
# This metadata is stored inside action_options
def process(action, *args)
action, options = action if action.is_a?(Array)
@_action_options = options || {}
super
end
# There are multiple ways how action name is calculated for update
# (see Commands, MessageContext, etc.). This method represents the
# way how action was calculated for current udpate.
#
# Some of possible values are `:payload, :command, :message_context`.
def action_type
action_options[:type] || :payload
end
# Calculates action name and args for payload. # Calculates action name and args for payload.
# Uses `action_for_#{payload_type}` methods. # Uses `action_for_#{payload_type}` methods.
# If this method doesn't return anything # If this method doesn't return anything
# it uses fallback with action same as payload type. # it uses fallback with action same as payload type.
# Returns array `[is_command?, action, args]`. # Returns array `[action, args]`.
def action_for_payload def action_for_payload
if payload_type if payload_type
send("action_for_#{payload_type}") || action_for_default_payload send("action_for_#{payload_type}") || action_for_default_payload
else else
[false, :unsupported_payload_type, []] [:unsupported_payload_type, []]
end end
end end
def action_for_default_payload def action_for_default_payload
[false, payload_type, [payload]] [payload_type, [payload]]
end end
# If payload is a message with command, then returned action is an
# action for this command.
# 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
cmd, args = self.class.command_from_text(payload['text'], bot_username)
cmd &&= self.class.action_for_command(cmd)
[true, cmd, args] if cmd
end
alias_method :action_for_channel_post, :action_for_message
def action_for_inline_query def action_for_inline_query
[false, payload_type, [payload['query'], payload['offset']]] [payload_type, [payload['query'], payload['offset']]]
end end
def action_for_chosen_inline_result def action_for_chosen_inline_result
[false, payload_type, [payload['result_id'], payload['query']]] [payload_type, [payload['result_id'], payload['query']]]
end end
def action_for_callback_query def action_for_callback_query
[false, payload_type, [payload['data']]] [payload_type, [payload['data']]]
end end
# Silently ignore unsupported messages. def action_for_poll_answer
# Params are `action, *args`. [payload_type, [payload['poll_id'], payload['option_ids']]]
def action_missing(*) end
# Silently ignore unsupported messages to not fail when user crafts
# an update with usupported command, callback query context, etc.
def action_missing(action, *_args)
logger.debug { "The action '#{action}' is not defined in #{self.class.name}" } if logger
nil
end end
PAYLOAD_TYPES.each do |type| PAYLOAD_TYPES.each do |type|

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

@@ -17,8 +17,12 @@ module Telegram
context, new_data = context_from_callback_query context, new_data = context_from_callback_query
if context if context
action_name = "#{context}_callback_query" action_name = "#{context}_callback_query"
[false, action_name, [new_data]] if action_method?(action_name) if action_method?(action_name)
end || super action_options = {type: :callback_query_context, context: context}
return [[action_name, action_options], [new_data]]
end
end
super
end end
def context_from_callback_query def context_from_callback_query

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

@@ -0,0 +1,44 @@
module Telegram
module Bot
class UpdatesController
# Support for parsing commands
module Commands
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
class << self
# Fetches command from text message. All subsequent words are returned
# as arguments.
# If command has mention (eg. `/test@SomeBot`), it returns commands only
# for specified username. Set `username` to `true` to accept
# any commands.
def command_from_text(text, username = nil)
return unless text
match = text.match(CMD_REGEX)
return unless match
mention = match[3]
[match[1], text.split.drop(1)] if username == true || !mention || mention == username
end
end
# Override it to filter or transform commands.
# Default implementation is to downcase and add `!` suffix.
def action_for_command(cmd)
"#{cmd.downcase}!"
end
# If payload is a message with command, then returned action is an
# action for this command.
# 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
cmd, args = Commands.command_from_text(payload['text'], bot_username)
return unless cmd
[[action_for_command(cmd), type: :command, command: cmd], args]
end
alias_method :action_for_channel_post, :action_for_message
end
end
end
end

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

@@ -49,7 +49,7 @@ module Telegram
private private
# A hook invoked every time a before callback is halted. # A hook invoked every time a before callback is halted.
def halted_callback_hook(filter) def halted_callback_hook(filter, *)
Instrumentation.instrument(:halted_callback, filter: filter) Instrumentation.instrument(:halted_callback, filter: filter)
end end

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

@@ -2,62 +2,42 @@ module Telegram
module Bot module Bot
class UpdatesController class UpdatesController
# Allows to store context in session and treat next message according to this context. # Allows to store context in session and treat next message according to this context.
#
# It provides `save_context` method to store method name
# to be used as action for next update:
#
# def set_location!(*)
# save_context(:set_location_from_message)
# respond_with :message, text: 'Where are you?'
# end
#
# def set_location_from_messge(city = nil, *)
# # update
# end
#
# # OR
# # This will support both `/set_location city_name`, and `/set_location`
# # with subsequent refinement.
# def set_location!(city = nil, *)
# if city
# # update
# else
# save_context(:set_location!)
# respond_with :message, text: 'Where are you?'
# end
# end
module MessageContext module MessageContext
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Session include Session
module ClassMethods
def context_handlers
@_context_handlers ||= {}
end
# Registers handler for context.
#
# context_handler :rename do |*|
# resource.update!(name: payload['text'])
# end
#
# # To run other action with all the callbacks:
# context_handler :rename do |*words|
# process(:rename, *words)
# end
#
# # Or just
# context_handler :rename, :your_action_to_call
# context_handler :rename # to call :rename
#
def context_handler(context = nil, action = nil, &block)
context &&= context.to_sym
if block
action = "_context_handler_#{context}"
define_method(action, &block)
end
context_handlers[context] = action || context
end
attr_reader :context_to_action
# Use it to use context value as action name for all contexts
# which miss handlers.
# For security reasons it supports only action methods and will
# raise AbstractController::ActionNotFound if context is invalid.
def context_to_action!
@context_to_action = true
end
end
# Action to clear context. # Action to clear context.
def cancel def cancel!
# Context is already cleared in action_for_message # Context is already cleared in action_for_message
end end
private private
# Context is read from the session to treat messages
# according to previous request.
attr_reader :context
# Controller may have multiple sessions, let it be possible # Controller may have multiple sessions, let it be possible
# to select session for message context. # to select session for message context.
def message_context_session def message_context_session
@@ -68,10 +48,11 @@ module Telegram
# it has higher priority than contextual action. # it has higher priority than contextual action.
def action_for_message def action_for_message
val = message_context_session.delete(:context) val = message_context_session.delete(:context)
@context = val && val.to_sym context = val && val.to_s
super || context && begin super || context && begin
handler = handler_for_context args = payload['text'].try!(:split) || []
[true, handler, payload['text'].try!(:split) || []] if handler action = action_for_message_context(context)
[[action, type: :message_context, context: context], args]
end end
end end
@@ -80,18 +61,16 @@ module Telegram
message_context_session[:context] = context message_context_session[:context] = context
end end
def handler_for_context # Returns action name for message context. By default it's the same as context name.
self.class.context_handlers[context] || self.class.context_to_action && begin # Raises AbstractController::ActionNotFound if action is not available.
action_name = context.to_s # This differs from other cases where invalid actions are silently ignored,
unless action_method?(action_name) # because message context is controlled by developer, and users are not able
raise AbstractController::ActionNotFound, # to construct update to run any specific context.
"The action '#{action_name}' could not be set from context " \ def action_for_message_context(context)
"for #{self.class.name}. " \ action = context.to_s
'context_to_action! supports only action methods for security reasons. ' \ return action if action_method?(action)
'If you need to call this action use context_handler for it.' raise AbstractController::ActionNotFound,
end "The context action '#{action}' is not found in #{self.class.name}"
action_name
end
end end
end end
end end

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

@@ -21,7 +21,7 @@ module Telegram
respond_with(type, params) respond_with(type, params)
end end
# Same as reply_with, but for inline queries. # Same as respond_with, but for inline queries.
def answer_inline_query(results, params = {}) def answer_inline_query(results, params = {})
params = params.merge( params = params.merge(
inline_query_id: payload['id'], inline_query_id: payload['id'],
@@ -30,7 +30,7 @@ module Telegram
bot.answer_inline_query(params) bot.answer_inline_query(params)
end end
# Same as reply_with, but for callback queries. # Same as respond_with, but for callback queries.
def answer_callback_query(text, params = {}) def answer_callback_query(text, params = {})
params = params.merge( params = params.merge(
callback_query_id: payload['id'], callback_query_id: payload['id'],
@@ -39,6 +39,23 @@ module Telegram
bot.answer_callback_query(params) bot.answer_callback_query(params)
end end
# Same as respond_with, but for pre checkout queries.
def answer_pre_checkout_query(ok, params = {})
params = params.merge(
pre_checkout_query_id: payload['id'],
ok: ok,
)
bot.answer_pre_checkout_query(params)
end
def answer_shipping_query(ok, params = {})
params = params.merge(
shipping_query_id: payload['id'],
ok: ok,
)
bot.answer_shipping_query(params)
end
# Edit message from callback query. # Edit message from callback query.
def edit_message(type, params = {}) def edit_message(type, params = {})
params = params =

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

@@ -1,37 +1,32 @@
require 'telegram/bot/updates_controller/testing' require 'telegram/bot/updates_controller/testing'
require 'telegram/bot/rspec/message_helpers'
require 'telegram/bot/rspec/callback_query_helpers'
RSpec.shared_context 'telegram/bot/updates_controller' do RSpec.shared_context 'telegram/bot/updates_controller' do
include Telegram::Bot::RSpec::ClientMatchers
include_context 'telegram/bot/message_helpers'
include_context 'telegram/bot/callback_query', :callback_query
let(:controller_class) { described_class } let(:controller_class) { described_class }
let(:controller) do let(:controller) do
controller_class.new(bot, update).tap do |x| controller_class.new(*controller_args).tap do |x|
x.extend Telegram::Bot::UpdatesController::Testing x.extend Telegram::Bot::UpdatesController::Testing
end end
end end
let(:update) { build_update(payload_type, payload) } let(:controller_args) { [bot, deep_stringify(update)] }
let(:update) { {payload_type => payload} }
let(:payload_type) { :some_type } let(:payload_type) { :some_type }
let(:payload) { double(:payload) } let(:payload) { double(:payload) }
let(:bot) { Telegram::Bot::ClientStub.new(bot_name) } let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
let(:bot_name) { 'bot' } let(:bot_name) { 'bot' }
let(:session) { controller.send(:session) } let(:session) { controller.send(:session) }
let(:from_id) { 123 }
let(:chat_id) { 456 }
let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} }
include Telegram::Bot::RSpec::ClientMatchers # Process update.
def dispatch(update = self.update, bot = self.bot)
def dispatch(bot = self.bot, update = self.update) controller.dispatch_again(bot, deep_stringify(update))
controller.dispatch_again(bot, update)
end
def dispatch_message(text, options = {})
update = build_update :message, default_message_options.merge(options).merge(text: text)
dispatch bot, update
end
def build_update(type, content)
deep_stringify type => content
end end
# Same as `.as_json` but mocks-friendly.
def deep_stringify(input) def deep_stringify(input)
case input case input
when Array then input.map(&method(__callee__)) when Array then input.map(&method(__callee__))
@@ -39,15 +34,8 @@ 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
RSpec.configure do |config| Telegram::Bot::RSpec.with_include_context do |config|
if config.respond_to?(:include_context) config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
end
end end

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

@@ -0,0 +1,47 @@
module Telegram
module Bot
class UpdatesController
# Provides helpers similar to AbstractController::Translation
# but by default uses `action_name_i18n_key` in lazy translation keys
# which strips `!` from action names by default. This makes translating
# strings for commands more convenient.
#
# To disable this behaviour use `alias_method :action_name_i18n_key, :action_name`.
module Translation
extend ActiveSupport::Concern
module ClassMethods
# Class-level helper for lazy translations.
def translate(key, **options)
key = "#{controller_path.tr('/', '.')}#{key}" if key.to_s.start_with?('.')
I18n.translate(key, **options)
end
alias :t :translate
end
# See toplevel description.
def translate(key, **options)
if key.to_s.start_with?('.')
path = controller_path.tr('/', '.')
defaults = [:"#{path}#{key}"]
defaults << options[:default] if options[:default]
options[:default] = defaults.flatten
key = "#{path}.#{action_name_i18n_key}#{key}"
end
I18n.translate(key, **options)
end
alias :t :translate
# Strips trailing `!` from action_name.
def action_name_i18n_key
action_name.chomp('!')
end
def localize(*args)
I18n.localize(*args)
end
alias :l :localize
end
end
end
end

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

@@ -50,7 +50,7 @@ module Telegram
ensure ensure
@running = false @running = false
end end
log { 'Stoped polling bot updates.' } log { 'Stopped polling bot updates.' }
end end
def run def run

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

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

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

@@ -1,6 +1,6 @@
require 'integration_helper' require 'integration_helper'
RSpec.describe DefaultBotController, :telegram_bot, type: :request do RSpec.describe DefaultBotController, telegram_bot: :rails do
describe '#start' do describe '#start' do
subject { -> { dispatch_command :start } } subject { -> { dispatch_command :start } }
it { should respond_with_message 'from default' } it { should respond_with_message 'from default' }

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

@@ -1,6 +1,6 @@
require 'integration_helper' require 'integration_helper'
RSpec.describe NamedBotController, :telegram_bot, type: :request do RSpec.describe NamedBotController, telegram_bot: :rails do
let(:bot) { Telegram.bots[:named] } let(:bot) { Telegram.bots[:named] }
describe '#start' do describe '#start' do
subject { -> { dispatch_command :start } } subject { -> { dispatch_command :start } }

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

@@ -1,6 +1,6 @@
require 'integration_helper' require 'integration_helper'
RSpec.describe OtherBotController, :telegram_bot, type: :request do RSpec.describe OtherBotController, telegram_bot: :rails do
let(:bot) { Telegram.bots[:other] } let(:bot) { Telegram.bots[:other] }
describe '#start' do describe '#start' do
subject { -> { dispatch_command :start } } subject { -> { dispatch_command :start } }

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

@@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'integration_helper'
RSpec.describe Telegram::Bot::Tasks, type: :request do
include Telegram::Bot::RSpec::ClientMatchers
def stub_env(new_values)
new_values.stringify_keys!
old_values = ENV.to_h.slice(new_values.keys)
new_values.each { |k, v| ENV[k] = v.to_s }
yield
ensure
new_values.each_key { |k| ENV[k] = old_values[k] }
end
shared_examples 'uses BOT envar' do |bot_matcher|
it 'runs for all bots by default' do
matcher = Telegram.bots.values.map { |x| instance_exec(x, &bot_matcher) }.reduce(&:and)
expect(subject).to matcher
end
context 'when BOT specified' do
around { |ex| stub_env(BOT: :other) { ex.run } }
it 'runs for this bot only' do
expect(subject).to instance_exec(Telegram.bots[:other], &bot_matcher)
end
end
end
describe '#log_out' do
subject { -> { described_class.log_out } }
include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :logOut) }
end
describe '#close' do
subject { -> { described_class.close } }
include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :close) }
end
describe '#set_webhook' do
subject { -> { described_class.set_webhook } }
include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :setWebhook) }
context 'with options in env' do
around do |ex|
stub_env(IP_ADDRESS: '1.2.3.4', DROP_PENDING_UPDATES: 'TrUe') do
ex.run
end
end
include_examples 'uses BOT envar', ->(bot) {
make_telegram_request(bot, :setWebhook).with(hash_including(
ip_address: '1.2.3.4',
drop_pending_updates: true,
))
}
end
end
describe '#delete_webhook' do
subject { -> { described_class.delete_webhook } }
include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :deleteWebhook) }
context 'with options in env' do
around { |ex| stub_env(DROP_PENDING_UPDATES: 'TrUe') { ex.run } }
include_examples 'uses BOT envar', ->(bot) {
make_telegram_request(bot, :deleteWebhook).with(hash_including(
drop_pending_updates: true,
))
}
end
end
end

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

@@ -1,7 +1,6 @@
require 'telegram/bot/rspec/integration' require 'telegram/bot/rspec/integration/rails'
require 'action_controller' require 'action_controller'
require 'action_dispatch' require 'action_dispatch'
require 'action_dispatch/testing/integration'
require 'rails' require 'rails'
require 'telegram/bot/railtie' require 'telegram/bot/railtie'
@@ -15,6 +14,7 @@ class TestApplication < Rails::Application
config.eager_load = false config.eager_load = false
config.log_level = :debug config.log_level = :debug
config.action_dispatch.show_exceptions = false config.action_dispatch.show_exceptions = false
routes.default_url_options = {host: 'test.rpsec'}
telegram_config = { telegram_config = {
bot: 'default_token', bot: 'default_token',
@@ -36,7 +36,7 @@ Rails.application.initialize!
# # Controllers # # Controllers
%w[default other named].each do |bot_name| %w[default other named].each do |bot_name|
controller = Class.new(Telegram::Bot::UpdatesController) do controller = Class.new(Telegram::Bot::UpdatesController) do
define_method :start do |*| define_method :start! do |*|
respond_with :message, text: "from #{bot_name}" respond_with :message, text: "from #{bot_name}"
end end
end end
@@ -47,7 +47,7 @@ end
klass.class_eval do klass.class_eval do
use_session! use_session!
define_method :load_session do |*| define_method :load_session! do |*|
session[:test] session[:test]
end end
end end

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

@@ -32,6 +32,14 @@ RSpec.configure do |config|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end end
# This config option will be enabled by default on RSpec 4,
# but for reasons of backwards compatibility, you have to
# set it on RSpec 3.
#
# It causes the host group and examples to inherit metadata
# from the shared context.
config.shared_context_metadata_behavior = :apply_to_host_groups
config.mock_with :rspec do |mocks| config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on # Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to # a real object. This is generally recommended, and will default to

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

@@ -1,4 +1,4 @@
RSpec.shared_examples 'async' do |request_args: -> {}| RSpec.shared_examples 'async' do |request_args:|
let(:instance) { described_class.new(token: token, id: id, async: async) } let(:instance) { described_class.new(token: token, id: id, async: async) }
let(:id) { :default_bot } let(:id) { :default_bot }
let(:async) { true } let(:async) { true }
@@ -103,7 +103,7 @@ RSpec.shared_examples 'async' do |request_args: -> {}|
end end
describe '#request' do describe '#request' do
subject { ->(*args) { instance.request(*(args.empty? ? self.args : args)) } } subject { ->(*args) { instance.request(*args) } }
let(:args, &request_args) let(:args, &request_args)
let(:result) { double(:result) } let(:result) { double(:result) }
@@ -112,7 +112,7 @@ RSpec.shared_examples 'async' do |request_args: -> {}|
expect(instance).to_not receive(:http_request) expect(instance).to_not receive(:http_request)
expect(described_class).to receive(:prepare_async_args).with(*args) { args } expect(described_class).to receive(:prepare_async_args).with(*args) { args }
expect(instance.async).to receive(:perform_later).with(id.to_s, *args) { result } expect(instance.async).to receive(:perform_later).with(id.to_s, *args) { result }
expect(subject.call).to eq result expect(subject.call(*args)).to eq result
end end
end end
@@ -131,12 +131,11 @@ RSpec.shared_examples 'async' do |request_args: -> {}|
context 'when async is disabled' do context 'when async is disabled' do
let(:async) { false } let(:async) { false }
let(:result) { double(status: 200, body: '{"test":"ok"}') } let(:result) { double(status: 200, body: '{"test":"ok"}') }
let(:args, &request_args)
it 'performs request immediately' do it 'performs request immediately' do
expect(instance).to receive(:request).with(*args).and_call_original expect(instance).to receive(:request).with(*args).and_call_original
expect(instance).to receive(:http_request) { result } expect(instance).to receive(:http_request) { result }
expect(subject[]).to eq 'test' => 'ok' expect(subject.call(*args)).to eq 'test' => 'ok'
end end
end end
end end

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

@@ -1,65 +0,0 @@
RSpec.shared_examples 'initializers' do |config_method = :bots|
describe '.wrap' do
subject { described_class.wrap(input, **options) }
let(:options) { {} }
let(:result) { double(:result) }
let(:username) { 'username' }
context 'when input is a string' do
let(:input) { token }
it 'treats string as token' do
expect(described_class).to receive(:new).with(token, {}) { result }
should eq result
end
context 'and additional options are given' do
let(:options) { {id: :test} }
it 'passes them to initializer' do
expect(described_class).to receive(:new).with(input, **options) { result }
should eq result
end
end
end
context 'when input is a hash' do
let(:input) { {token: token, 'username' => username, other: :options} }
it 'passes it with symbolized keys' do
expect(described_class).to receive(:new).with(**input.symbolize_keys) { result }
should eq result
end
context 'and additional options are given' do
let(:options) { {id: :test} }
it 'passes them to initializer' do
expect(described_class).to receive(:new).
with(**input.symbolize_keys, **options) { result }
should eq result
end
end
end
context 'when input is an instance of described_class' do
let!(:input) { instance }
it 'returns input' do
expect(described_class).to_not receive(:new)
should eq input
end
end
context 'when input is a Symbol' do
let(:input) { :client_1 }
before { allow(Telegram).to receive(config_method) { {client_1: instance} } }
it { should eq Telegram.send(config_method)[:client_1] }
context 'and there is no such bot' do
let(:input) { :invalid }
it { expect { subject }.to raise_error(/not configured/) }
end
end
end
end

24
spec/support/examples/integration.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
RSpec.shared_examples 'shared integration examples' do
let(:bot) { Telegram::Bot::ClientStub.new('token') }
let(:controller_class) do
Class.new(Telegram::Bot::UpdatesController) do
def start!(data = nil, *)
respond_with :message, text: "Hi #{data}"
end
def callback_query(data, *)
answer_callback_query "pong: #{data}"
end
end
end
describe '#start' do
subject { -> { dispatch_command(:start, :test_data) } }
it { should respond_with_message('Hi test_data') }
end
describe '#callback_query', :callback_query do
let(:data) { :test_data }
it { should answer_callback_query "pong: #{data}" }
end
end

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

@@ -1,33 +0,0 @@
RSpec.describe Telegram::Bot::Client do
let(:instance) { described_class.new(*client_args) }
let(:token) { 'token' }
let(:botan_token) { double(:botan_token) }
let(:client_id) { 'client_id' }
let(:client_args) { [token] }
describe '#botan' do
subject { instance.botan }
it { should eq nil }
context 'when botan token is set' do
let(:client_args) { [token, id: client_id, async: Class.new, botan: botan_token] }
it { should be_instance_of Telegram::Bot::Botan }
its(:token) { should eq botan_token }
its(:id) { should eq client_id }
it 'doesnt inherit async from client' do
expect(instance.async).to be
expect(subject.async).to_not be
end
end
context 'when botan is configured with hash' do
let(:client_args) { [token, id: client_id, async: Class.new, botan: botan_config] }
let(:botan_config) { {token: botan_token, async: Class.new} }
it { should be_instance_of Telegram::Bot::Botan }
its(:token) { should eq botan_token }
its(:id) { should eq client_id }
its(:async) { should eq botan_config[:async] }
end
end
end

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

@@ -1,54 +0,0 @@
RSpec.describe Telegram::Bot::Botan::ControllerHelpers do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
described_class = self.described_class
Class.new(Telegram::Bot::UpdatesController) do
include described_class
end
end
let(:botan) { double(:botan) }
let(:result) { double(:result) }
let(:payload) { {from: {id: user_id}} }
let(:payload_type) { :message }
let(:user_id) { double(:user_id) }
before { allow(bot).to receive(:botan) { botan } }
shared_examples 'basic tracking' do
context 'when botan is not configured' do
let(:botan) {}
it { should eq nil }
end
context 'when `from` is empty' do
let(:payload) { {text: 'test'} }
it { expect { subject }.to raise_error described_class::MissingFrom }
end
end
describe '#botan_track' do
subject { controller.send(:botan_track, event, data) }
let(:event) { double(:event) }
let(:data) { double(:data) }
it 'calls botan#track' do
expect(botan).to receive(:track).with(event, user_id, data) { result }
should eq result
end
include_examples 'basic tracking'
end
describe '#botan_track_action' do
subject { controller.send(:botan_track_action) }
let(:action_name) { double(:action_name) }
it 'calls botan#track with current action and payload' do
expect(controller).to receive(:action_name) { action_name }
expect(botan).to receive(:track).
with(action_name, user_id, deep_stringify(payload)) { result }
should eq result
end
include_examples 'basic tracking'
end
end

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

@@ -1,27 +0,0 @@
RSpec.describe Telegram::Bot::Botan do
let(:instance) { described_class.new 'token' }
let(:token) { 'token' }
include_examples 'initializers', :botans
it_behaves_like 'async', request_args: -> { [double(:method), double(:url)] }
describe '.new' do
subject { described_class.new(*args) }
context 'when usual args are given' do
let(:args) { ['secret'] }
its(:token) { should eq args[0] }
end
context 'when options are given' do
let(:args) { [token: 'secret'] }
its(:token) { should eq args[0][:token] }
end
end
describe '.prepare_async_args' do
subject { described_class.prepare_async_args(*input) }
let(:input) { [:post, :uri, {a: 1, b: :sym, 'd' => 'str'}, 'body'] }
it { should eq ['post', 'uri', {a: 1, b: 'sym', 'd' => 'str'}, 'body'] }
end
end

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

@@ -0,0 +1,92 @@
RSpec.describe Telegram::Bot::Client::RequestBodyFormatter do
describe '.format' do
subject { described_class.format(input, action) }
let(:action) { :sendMessage }
context 'when plain hash is given' do
let(:input) { {a: 1, b: '2', c: nil} }
it { should eq input }
end
context 'when nested hash is given' do
let(:input) { {a: 1, b: '2', c: [1, 2], d: {a: 1}, e: {b: []}} }
it 'encodes nested hashes to json' do
expected = input.dup
%i[c d e].each { |x| expected[x] = expected[x].to_json }
should eq expected
end
end
context 'with sendMediaGroup action' do
let(:action) { :sendMediaGroup }
let(:input) { {media: [{a: file_1}, {'b' => file_2, c: 123}, {d: 456}], x: 789} }
let(:file_1) { File.new(__FILE__) }
let(:file_2) { File.new(__FILE__) }
it 'extracts files to the top-level' do
should eq(
media: [
{a: 'attach://_file0'},
{b: 'attach://_file1', c: 123},
{d: 456},
].to_json,
x: 789,
'_file0' => file_1,
'_file1' => file_2,
)
end
context 'and input has string keys' do
let(:input) { super().stringify_keys }
it 'extracts files to the top-level' do
should eq(
'media' => [
{a: 'attach://_file0'},
{b: 'attach://_file1', c: 123},
{d: 456},
].to_json,
'x' => 789,
'_file0' => file_1,
'_file1' => file_2,
)
end
end
context 'without media' do
let(:input) { {a: 1, b: '2', c: nil} }
it { should eq input }
end
end
context 'with editMessageMedia action' do
let(:action) { :editMessageMedia }
let(:input) { {media: {a: file, b: 123}, x: 789} }
let(:file) { File.new(__FILE__) }
it 'extracts files to the top-level' do
should eq(
media: {a: 'attach://_file0', b: 123}.to_json,
x: 789,
'_file0' => file,
)
end
context 'and input has string keys' do
let(:input) { super().stringify_keys }
it 'extracts files to the top-level' do
should eq(
'media' => {a: 'attach://_file0', b: 123}.to_json,
'x' => 789,
'_file0' => file,
)
end
end
context 'without media' do
let(:input) { {a: 1, b: '2', c: nil} }
it { should eq input }
end
end
end
end

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

@@ -1,26 +1,72 @@
RSpec.describe Telegram::Bot::Client do RSpec.describe Telegram::Bot::Client do
let(:instance) { described_class.new 'token' } let(:instance) { described_class.new 'token' }
let(:token) { 'token' } let(:token) { 'token' }
let(:botan_token) { double(:botan_token) }
include_examples 'initializers'
it_behaves_like 'async', request_args: -> { [double(:action), {body: :content}] } it_behaves_like 'async', request_args: -> { [double(:action), {body: :content}] }
describe '.prepare_body' do describe '.wrap' do
subject { described_class.prepare_body(input) } subject { described_class.wrap(input, **options) }
let(:options) { {} }
let(:result) { double(:result) }
let(:username) { 'username' }
context 'when plain hash is given' do context 'when input is a string' do
let(:input) { {a: 1, b: '2', c: nil} } let(:input) { token }
it { should eq input }
it 'treats string as token' do
expect(described_class).to receive(:new) do |*args, **kwargs|
expect([*args, kwargs]).to eq([token, {}]) # not using .with to support ruby 2.x and 3.x
result
end
should eq result
end
context 'and additional options are given' do
let(:options) { {id: :test} }
it 'passes them to initializer' do
expect(described_class).to receive(:new).with(input, **options) { result }
should eq result
end
end
end end
context 'when nested hash is given' do context 'when input is a hash' do
let(:input) { {a: 1, b: '2', c: [1, 2], d: {a: 1}, e: {b: []}} } let(:input) { {token: token, 'username' => username, other: :options} }
it 'encodes nested hashes to json' do it 'passes it with symbolized keys' do
expected = input.dup expect(described_class).to receive(:new).with(**input.symbolize_keys) { result }
%i[c d e].each { |x| expected[x] = expected[x].to_json } should eq result
should eq expected end
context 'and additional options are given' do
let(:options) { {id: :test} }
it 'passes them to initializer' do
expect(described_class).to receive(:new).
with(**input.symbolize_keys, **options) { result }
should eq result
end
end
end
context 'when input is an instance of described_class' do
let!(:input) { instance }
it 'returns input' do
expect(described_class).to_not receive(:new)
should eq input
end
end
context 'when input is a Symbol' do
let(:input) { :client_1 }
before { allow(Telegram).to receive(:bots) { {client_1: instance} } }
it { should eq Telegram.bots[:client_1] }
context 'and there is no such bot' do
let(:input) { :invalid }
it { expect { subject }.to raise_error(/not configured/) }
end end
end end
end end
@@ -32,27 +78,49 @@ RSpec.describe Telegram::Bot::Client do
end end
describe '.new' do describe '.new' do
subject { described_class.new(*args) } subject { described_class.new(*args, **kwargs) }
let(:args) { [] }
let(:kwargs) { {} }
let(:token) { 'secret' }
let(:username) { 'superbot' }
context 'when multiple args are given' do context 'when multiple args are given' do
let(:args) { %w[secret superbot] } let(:args) { [token, username] }
its(:token) { should eq args[0] } its(:token) { should eq token }
its(:username) { should eq args[1] } its(:username) { should eq username }
its(:base_uri) { should include args[0] } its(:base_uri) { should eq "#{described_class::SERVER}/bot#{token}/" }
end end
context 'when hash is given' do context 'when hash is given' do
let(:args) { [token: 'secret', username: 'superbot'] } let(:kwargs) { {token: 'secret', username: 'superbot'} }
its(:token) { should eq args[0][:token] } its(:token) { should eq token }
its(:username) { should eq args[0][:username] } its(:username) { should eq username }
its(:base_uri) { should include args[0][:token] } its(:base_uri) { should eq "#{described_class::SERVER}/bot#{token}/" }
end
context 'with custom server' do
let(:server) { 'http://my.server' }
let(:args) { [token, username] }
let(:kwargs) { {server: server} }
its(:base_uri) { should eq "#{server}/bot#{token}/" }
context 'and hash options' do
let(:kwargs) { {token: token, username: username, server: server} }
its(:base_uri) { should eq "#{server}/bot#{token}/" }
end
end end
end end
describe '#request' do describe '#request' do
subject { -> { instance.request(action, request_body) } } subject { -> { instance.request(action, request_body) } }
let(:action) { :some_action } let(:action) { :some_action }
let(:url) { "#{format(described_class::URL_TEMPLATE, token: token)}#{action}" } let(:url) do
base_uri = format(described_class::URL_TEMPLATE,
server: described_class::SERVER,
token: token,
)
"#{base_uri}#{action}"
end
let(:request_body) { double(:body) } let(:request_body) { double(:body) }
let(:prepared_body) { double(:prepared_body) } let(:prepared_body) { double(:prepared_body) }
let(:response) { HTTP::Message.new_response(body).tap { |x| x.status = status } } let(:response) { HTTP::Message.new_response(body).tap { |x| x.status = status } }
@@ -61,7 +129,8 @@ RSpec.describe Telegram::Bot::Client do
let(:body) { body_json.to_json } let(:body) { body_json.to_json }
let(:body_json) { {'param' => 'val', 'description' => 'some description'} } let(:body_json) { {'param' => 'val', 'description' => 'some description'} }
before do before do
expect(described_class).to receive(:prepare_body).with(request_body) { prepared_body } expect(Telegram::Bot::Client::RequestBodyFormatter).
to receive(:format).with(request_body, action) { prepared_body }
expect(instance).to receive(:http_request).with(url, prepared_body) { response } expect(instance).to receive(:http_request).with(url, prepared_body) { response }
end end
around { |ex| Telegram::Bot::ClientStub.stub_all!(false) { ex.run } } around { |ex| Telegram::Bot::ClientStub.stub_all!(false) { ex.run } }

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

@@ -37,11 +37,13 @@ RSpec.describe Telegram::Bot::ClientStub do
end end
describe '#new' do describe '#new' do
subject { described_class.new(*args) } subject { described_class.new(*args, **kwargs) }
let(:args) { [] }
let(:kwargs) { {} }
context 'when only username is given' do context 'when only username is given' do
let(:args) { 'superbot' } let(:args) { ['superbot'] }
its(:username) { should eq args } its(:username) { should eq args[0] }
end end
context 'when username and token are given' do context 'when username and token are given' do
@@ -51,9 +53,9 @@ RSpec.describe Telegram::Bot::ClientStub do
end end
context 'when hash config is given' do context 'when hash config is given' do
let(:args) { [token: 'token', username: 'superbot'] } let(:kwargs) { {token: 'token', username: 'superbot'} }
its(:token) { should eq args[0][:token] } its(:token) { should eq kwargs[:token] }
its(:username) { should eq args[0][:username] } its(:username) { should eq kwargs[:username] }
end end
end end
end end

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

@@ -11,12 +11,10 @@ RSpec.describe Telegram::Bot::ConfigMethods do
chat: { chat: {
token: 'chat_token', token: 'chat_token',
username: 'Chat', username: 'Chat',
botan: 'chat_botan_token',
}, },
other_chat: { other_chat: {
'token' => 'other_chat_token', 'token' => 'other_chat_token',
'username' => 'OtherChat', 'username' => 'OtherChat',
'botan' => 'other_chat_botan_token',
}, },
} }
end end
@@ -43,7 +41,6 @@ RSpec.describe Telegram::Bot::ConfigMethods do
its(:id) { should eq :chat } its(:id) { should eq :chat }
its(:token) { should eq config[:chat][:token] } its(:token) { should eq config[:chat][:token] }
its(:username) { should eq config[:chat][:username] } its(:username) { should eq config[:chat][:username] }
its('botan.token') { should eq config[:chat][:botan] }
end end
context 'configured by hash with stringified keys' do context 'configured by hash with stringified keys' do
@@ -51,18 +48,6 @@ RSpec.describe Telegram::Bot::ConfigMethods do
its(:id) { should eq :other_chat } its(:id) { should eq :other_chat }
its(:token) { should eq config[:other_chat]['token'] } its(:token) { should eq config[:other_chat]['token'] }
its(:username) { should eq config[:other_chat]['username'] } its(:username) { should eq config[:other_chat]['username'] }
its('botan.token') { should eq config[:other_chat]['botan'] }
end
end
describe '#botans' do
subject { registry.botans }
it do
should eq(
default: nil,
chat: registry.bots[:chat].botan,
other_chat: registry.bots[:other_chat].botan,
)
end end
end end
@@ -76,13 +61,14 @@ RSpec.describe Telegram::Bot::ConfigMethods do
it { should eq({}) } it { should eq({}) }
context 'in rails environment' do context 'in rails environment' do
before { stub_const('Rails', double(application: double(secrets: secrets))) } before { stub_const('Rails', double(application: double(app_stub))) }
let(:app_stub) { {secrets: secrets} }
let(:secrets) { {} } let(:secrets) { {} }
it { should eq({}) } it { should eq({}) }
context 'when there is telegram section in secrets' do context 'when there is telegram section in secrets' do
let(:secrets) { {telegram: config.stringify_keys} } let(:secrets) { {telegram: secrets_config} }
let(:config) do let(:secrets_config) do
{ {
bot: double(:bot_config), bot: double(:bot_config),
bots: { bots: {
@@ -91,13 +77,21 @@ RSpec.describe Telegram::Bot::ConfigMethods do
}, },
} }
end end
it { should include default: config[:bot] } it { should include default: secrets_config[:bot] }
it { should include config[:bots] } it { should include secrets_config[:bots] }
context 'on rails >5.1 (deep symbolized keys)' do context 'on rails >5.1 (deep symbolized keys)' do
let(:secrets) { {telegram: config.deep_symbolize_keys} } let(:secrets) { super().deep_symbolize_keys }
it { should include default: config[:bot] } it { should include default: secrets_config[:bot] }
it { should include config[:bots] } it { should include secrets_config[:bots] }
end
context 'and credentials (>= 5.2)' do
let(:app_stub) { super().merge(credentials: credentials) }
let(:credentials) { {telegram: credentials_config} }
let(:credentials_config) { {bot: double(:credentials_bot_config)} }
it { should include default: credentials_config[:bot] }
it { should_not include secrets[:bots] }
end end
end end
end end

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

@@ -7,7 +7,6 @@ RSpec.describe Telegram::Bot::Middleware do
describe '#call' do describe '#call' do
subject { instance.call(env) } subject { instance.call(env) }
let(:env) { {'action_dispatch.request.request_parameters' => json_body} }
let(:update) { {'message' => {'id' => 1}} } let(:update) { {'message' => {'id' => 1}} }
let(:env) do let(:env) do
Rack::MockRequest.env_for('/', Rack::MockRequest.env_for('/',
@@ -21,7 +20,6 @@ RSpec.describe Telegram::Bot::Middleware do
if ActionPack::VERSION::MAJOR < 5 if ActionPack::VERSION::MAJOR < 5
# Before Rails 5, params are parsed in middleware. # Before Rails 5, params are parsed in middleware.
# In Rails 5, they are parsed in Request#request_parameters. # In Rails 5, they are parsed in Request#request_parameters.
require 'action_dispatch/middleware/params_parser'
let(:instance) { ActionDispatch::ParamsParser.new(super()) } let(:instance) { ActionDispatch::ParamsParser.new(super()) }
end end

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

@@ -41,162 +41,62 @@ RSpec.describe Telegram::Bot::RoutesHelper do
end end
describe '#telegram_webhook' do describe '#telegram_webhook' do
subject { mapper.telegram_webhook(*input) } subject { ->(*args, **kwargs) { mapper.telegram_webhook(*args, **kwargs) } }
let(:mapper) { double(:mapper).tap { |x| x.extend described_class } } let(:mapper) { double(:mapper).tap { |x| x.extend described_class } }
let(:bots) { {default: bot, other: other_bot} }
let(:controller) { double(:controller, name: :controller) } let(:controller) { double(:controller, name: :controller) }
let(:other_controller) { double(:other_controller, name: :other_controller) }
before { allow(Telegram).to receive(:bots) { bots } } before { allow(Telegram).to receive(:bots) { bots } }
def assert_routes(bot, controller, route_name, options) # rubocop:disable AbcSize def assert_route(bot, controller, path: nil, **expected_options) # rubocop:disable AbcSize
expected_path = options.delete(:path) || "telegram/#{bot.token}" path ||= "telegram/#{described_class.token_hash(bot.token)}"
expect(mapper).to receive(:post) do |path, params| expect(mapper).to receive(:post) do |actual_path, actual_options|
expect(path).to eq expected_path expect(actual_path).to eq(path)
middleware = params[:to] middleware = actual_options[:to]
expect(middleware.controller).to eq(controller) expect(middleware.controller).to eq(controller)
expect(middleware.bot.token).to eq(bot.token) expect(middleware.bot.token).to eq(bot.token)
expect(middleware.bot.username).to eq(bot.username) expect(middleware.bot.username).to eq(bot.username)
expect(params[:as]).to eq route_name expect(actual_options).to include(expected_options)
expect(params).to include(options) if options
end end
subject yield
end end
context 'when called with controller' do it 'creates routes for default bot' do
let(:input) { [controller, option: :val] } assert_route(bot, controller, as: 'default_telegram_webhook') do
subject[controller]
it 'creates routes for default bot and this controller' do
assert_routes bot, controller, 'default_telegram_webhook', option: :val
end end
context 'and bot does not have configured token' do other_controller = double(:other_controller, name: :other_controller)
let(:bot) { create_bot(nil) } assert_route(bot, other_controller, as: 'custom_route') do
it 'creates routes for default bot and this controller' do subject[other_controller, as: 'custom_route']
assert_routes bot, controller, 'default_telegram_webhook', option: :val
end
end end
end
context 'and bot has colon in token' do it 'passes extra options' do
let(:bot) { create_bot('some:token') } assert_route(bot, controller, as: 'default_telegram_webhook', option: :val, other: 2) do
it 'replaces colon with underscore' do subject[controller, option: :val, other: 2]
assert_routes bot, controller, 'default_telegram_webhook', end
option: :val, end
path: 'telegram/some_token'
it 'uses :path param to override default path' do
assert_route(bot, controller, as: 'default_telegram_webhook', path: 'custom/path') do
subject[controller, path: 'custom/path']
end
end
context 'when bot does not have configured token' do
let(:bot) { create_bot(nil) }
it 'creates route anyway' do
assert_route(bot, controller, as: 'default_telegram_webhook', path: 'telegram/') do
subject[controller]
end end
end end
end end
context 'when called with controller and smth castable to bot' do 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 it 'creates routes for every created bot and controller' do
assert_routes create_bot('custom_token'), controller, 'telegram_webhook', assert_route(create_bot('custom_token'), controller, as: 'telegram_webhook') do
option: :val subject[controller, 'custom_token']
end
end
end
describe '#telegram_webhooks' do
subject { mapper.telegram_webhooks(*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 } }
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)|
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 end
end end
subject
end
context 'when called with controller' do
let(:input) { [controller, option: :val] }
it 'creates routes for every bot and this controller' do
assert_routes [bot, controller, 'default_telegram_webhook', option: :val],
[other_bot, controller, 'other_telegram_webhook', option: :val]
end
context 'and bot does not have configured token' do
let(:bot) { create_bot(nil) }
it 'creates routes for every bot and this controller' do
assert_routes [bot, controller, 'default_telegram_webhook', option: :val],
[other_bot, controller, 'other_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',
], [other_bot, controller, 'other_telegram_webhook', option: :val]
end
end
end
context 'when called with hash' do
let(:input) do
[
{
bot => controller,
'custom_token' => [other_controller, as: :custom_route, option: :other_val],
other: controller,
},
option: :val,
]
end
it 'creates routes for every bot and its controller' do
assert_routes [bot, controller, 'default_telegram_webhook', option: :val],
[
create_bot('custom_token'),
other_controller,
:custom_route,
option: :other_val,
],
[Telegram.bots[:other], controller, 'other_telegram_webhook', option: :val]
end
end
context 'when called with controller and smth castable to bot' do
let(:input) do
[
controller,
['custom_token', token: bot.token, username: 'new_name'],
option: :val,
]
end
it 'creates routes for every created bot and controller' do
assert_routes [
create_bot('custom_token'),
controller,
'telegram_webhook',
option: :val,
], [
create_bot(bot.token, 'new_name'),
controller,
'telegram_webhook',
option: :val,
]
end
end end
end end
end end

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

@@ -0,0 +1,42 @@
require 'telegram/bot/rspec/integration/poller'
RSpec.describe 'Integration spec helpers', telegram_bot: :poller do
let(:bot) { Telegram::Bot::ClientStub.new('token') }
let(:controller_class) 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', :callback_query do
let(:data) { 'unknown:command' }
it { should answer_callback_query("data: #{data}") }
end
describe '#context_callback_query', :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', :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

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

@@ -0,0 +1,5 @@
require 'telegram/bot/rspec/integration/poller'
RSpec.describe 'Poller integration spec', telegram_bot: :poller do
include_examples 'shared integration examples'
end

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

@@ -0,0 +1,20 @@
require 'telegram/bot/rspec/integration/rack'
RSpec.describe 'Rack integration spec', telegram_bot: :rack do
include_examples 'shared integration examples'
let(:request_path) { '/bot' }
let(:app) do
path = request_path
bot_app = Telegram::Bot::Middleware.new(bot, controller_class)
app = Rack::Builder.new do
map(path) { run bot_app }
run ->(env) { raise "Route is not mapped: #{env['PATH_INFO']}" }
end
if ActionPack::VERSION::MAJOR >= 5
app
else
require 'action_dispatch/middleware/params_parser'
ActionDispatch::ParamsParser.new(app)
end
end
end

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

@@ -0,0 +1 @@
# Tested in spec/integration

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

@@ -1,112 +0,0 @@
require 'telegram/bot/rspec/integration'
require 'action_controller'
require 'action_dispatch'
require 'action_dispatch/testing/integration'
RSpec.describe 'Integrations helper', :telegram_bot do
include ActionDispatch::Integration::Runner
def reset_template_assertion
end
let(:app) do
app = Telegram::Bot::Middleware.new(bot, controller)
if ActionPack::VERSION::MAJOR >= 5
app
else
require 'action_dispatch/middleware/params_parser'
ActionDispatch::ParamsParser.new(app)
end
end
let(:bot) { Telegram::Bot::ClientStub.new('token') }
let(:controller_path) { '/' }
let(:controller) do
Class.new(Telegram::Bot::UpdatesController) do
def start(*args)
respond_with :message, text: "Start: #{args.inspect}, option: #{payload[:option]}"
end
end
end
describe '#default_message_options' do
subject { default_message_options }
it { should eq from: {id: from_id}, chat: {id: chat_id} }
end
describe '#dispatch' do
subject { -> { dispatch message: {text: '/start', **default_message_options} } }
it { should respond_with_message 'Start: [], option: ' }
end
describe '#dispatch_message' do
subject { -> { dispatch_message "/start #{args.join ' '}", options } }
let(:args) { %w[asd qwe] }
let(:options) { {} }
it { should respond_with_message "Start: #{args.inspect}, option: " }
context 'with options' do
let(:options) { {option: 1} }
it { should respond_with_message "Start: #{args.inspect}, option: 1" }
context 'and chat_id is not set' do
let(:options) { super().merge(chat: nil) }
it { should raise_error(/chat is not present/) }
end
end
end
describe '#dispatch_command' do
subject { -> { dispatch_command :start, *args } }
let(:args) { [] }
it { should respond_with_message "Start: #{args.inspect}, option: " }
context 'with args' do
let(:args) { %w[asd qwe] }
it { should respond_with_message "Start: #{args.inspect}, option: " }
end
context 'with options' do
let(:args) { ['asd', 'qwe', option: 1] }
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,115 @@
require 'telegram/bot/rspec/integration/poller'
RSpec.describe 'Integration: message helpers', telegram_bot: :poller do
describe '#default_message_options' do
subject { default_message_options }
it { should eq from: {id: from_id}, chat: {id: chat_id} }
end
describe '#dispatch_message' do
subject { -> { dispatch_message text, options } }
let(:text) { '/start asd qwe' }
let(:options) { {} }
let(:result) { double(:result) }
it 'invokes dispatch' do
expect(self).to receive(:dispatch).with(
message: hash_including(default_message_options.merge(
text: text,
)),
) { result }
expect(subject.call).to eq result
end
context 'with options' do
let(:options) { {option: 1} }
it 'invokes dispatch' do
expect(self).to receive(:dispatch).with(
message: hash_including(default_message_options.merge(
text: text,
).merge(options)),
) { result }
expect(subject.call).to eq result
end
end
end
describe '#dispatch_command' do
subject { -> { dispatch_command :start, *args } }
let(:args) { [] }
let(:result) { double(:result) }
it 'invokes dispatch' do
expect(self).to receive(:dispatch).with(
message: hash_including(default_message_options.merge(
text: '/start',
)),
) { result }
expect(subject.call).to eq result
end
context 'with args & options' do
let(:args) { [*params, options] }
let(:params) { %w[qwe asd] }
let(:options) { {option: 1} }
it 'invokes dispatch' do
expect(self).to receive(:dispatch).with(
message: hash_including(default_message_options.merge(
text: "/start #{params.join(' ')}",
).merge(options)),
) { result }
expect(subject.call).to eq result
end
end
end
end
# Old specs
RSpec.describe 'Integration: message helpers', telegram_bot: :poller do
let(:bot) { Telegram::Bot::ClientStub.new('token') }
let(:controller_class) do
Class.new(Telegram::Bot::UpdatesController) do
def start!(*args)
respond_with :message, text: "Start: #{args.inspect}, option: #{payload['option']}"
end
end
end
describe '#default_message_options' do
subject { default_message_options }
it { should eq from: {id: from_id}, chat: {id: chat_id} }
end
describe '#dispatch_message' do
subject { -> { dispatch_message "/start #{args.join ' '}", options } }
let(:args) { %w[asd qwe] }
let(:options) { {} }
it { should respond_with_message "Start: #{args.inspect}, option: " }
context 'with options' do
let(:options) { {option: 1} }
it { should respond_with_message "Start: #{args.inspect}, option: 1" }
context 'and chat_id is not set' do
let(:options) { super().merge(chat: nil) }
it { should raise_error(/chat is not present/) }
end
end
end
describe '#dispatch_command' do
subject { -> { dispatch_command :start, *args } }
let(:args) { [] }
it { should respond_with_message "Start: #{args.inspect}, option: " }
context 'with args' do
let(:args) { %w[asd qwe] }
it { should respond_with_message "Start: #{args.inspect}, option: " }
end
context 'with options' do
let(:args) { ['asd', 'qwe', option: 1] }
it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" }
end
end
end

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

@@ -27,7 +27,7 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
describe '#dispatch' do describe '#dispatch' do
subject { -> { dispatch } } subject { -> { dispatch } }
let(:payload_type) { :callback_query } let(:payload_type) { :callback_query }
let(:payload) { {'data' => data} } let(:payload) { {data: data} }
let(:data) { text } let(:data) { text }
let(:text) { 'asd qwe zxc' } let(:text) { 'asd qwe zxc' }

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

@@ -0,0 +1,111 @@
RSpec.describe Telegram::Bot::UpdatesController::Commands do
describe '#action_for_command' do
subject { ->(*args) { object.action_for_command(*args) } }
let(:object) { Object.new.tap { |x| x.extend described_class } }
def assert_subject(input, expected)
expect(subject.call input).to eq expected
end
it 'bypasses and downcases not conflictint commands' do
assert_subject 'test', 'test!'
assert_subject 'TeSt', 'test!'
assert_subject '_Te1St', '_te1st!'
end
end
describe '.command_from_text' do
subject { ->(*args) { described_class.command_from_text(*args) } }
def assert_subject(input, cmd, *args)
expected = cmd ? [cmd, args] : cmd
expect(subject.call(*input)).to eq expected
end
let(:max_cmd_size) { 32 }
let(:long_cmd) { 'a' * (max_cmd_size - 1) }
let(:too_long_cmd) { 'a' * max_cmd_size }
it 'works for simple commands' do
assert_subject '/test', 'test'
assert_subject '/tE_2_St', 'tE_2_St'
assert_subject '/123', '123'
assert_subject "/#{long_cmd}", long_cmd
end
it 'works for simple messages' do
assert_subject 'text', nil
assert_subject ' ', nil
assert_subject ' text', nil
assert_subject ' 1', nil
assert_subject ' /text', nil
assert_subject '/te-xt', nil
assert_subject 'text /cmd ', nil
assert_subject "/#{too_long_cmd}", nil
end
it 'works for mentioned commands' do
assert_subject ['/test@bot', 'bot'], 'test'
assert_subject ['/test@otherbot', 'bot'], nil
assert_subject ['/test@Bot', 'bot'], nil
assert_subject '/test@bot', nil
assert_subject ['/test@bot', true], 'test'
assert_subject ['/test@otherbot', true], 'test'
end
it 'works for commands with args' do
assert_subject '/test arg', 'test', 'arg'
assert_subject '/test arg 1 2', 'test', 'arg', '1', '2'
assert_subject ['/test@bot arg', 'bot'], 'test', 'arg'
assert_subject ['/test@otherbot arg', 'bot'], nil
assert_subject '/test@bot arg', nil
end
it 'works for commands with multiline args' do
assert_subject "/test arg\nother", 'test', 'arg', 'other'
assert_subject "/test one\ntwo\n\nthree", 'test', 'one', 'two', 'three'
end
end
describe '#action_for_payload' do
include_context 'telegram/bot/updates_controller'
let(:controller_class) { Telegram::Bot::UpdatesController }
subject { controller.action_for_payload }
%w[message channel_post].each do |type|
context "when payload is edited_#{type}" do
let(:payload_type) { "edited_#{type}" }
it { should eq [payload_type, [payload]] }
end
context 'when payload is message' do
let(:payload_type) { type }
let(:payload) { {'text' => text} }
let(:text) { 'test' }
it { should eq [payload_type, [payload]] }
context 'with command' do
let(:text) { "/test#{"@#{mention}" if mention} arg 1 2" }
let(:mention) {}
it { should eq [['test!', type: :command, command: 'test'], %w[arg 1 2]] }
context 'with mention' do
let(:mention) { bot.username }
it { should eq [['test!', type: :command, command: 'test'], %w[arg 1 2]] }
end
context 'with mention for other bot' do
let(:mention) { 'other_bot_name' }
it { should eq [payload_type, [payload]] }
end
end
context 'without text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
it { should eq [payload_type, [payload]] }
end
end
end
end
end

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

@@ -2,13 +2,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do
include_context 'telegram/bot/updates_controller' include_context 'telegram/bot/updates_controller'
subject { -> { dispatch } } subject { -> { dispatch } }
let(:update) do let(:update) { {message: default_message_options.merge(text: '/start')} }
build_update :message, default_message_options.merge(text: '/start')
end
let(:controller_class) do let(:controller_class) do
Class.new(Telegram::Bot::UpdatesController) do Class.new(Telegram::Bot::UpdatesController) do
def start(*) def start!(*)
end end
end end
end end
@@ -36,11 +34,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do
action = action_name(:start_processing) action = action_name(:start_processing)
expect(events[action].size).to eq(1) expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(update: update) expect(events[action][0].last).to include(update: deep_stringify(update))
action = action_name(:process_action) action = action_name(:process_action)
expect(events[action].size).to eq(1) expect(events[action].size).to eq(1)
expect(events[action][0].last).to include(update: update) expect(events[action][0].last).to include(update: deep_stringify(update))
end end
end end
@@ -74,7 +72,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do
describe '#respond_with' do describe '#respond_with' do
before do before do
def controller.start(*) def controller.start!(*)
respond_with :message, text: 'sample response' respond_with :message, text: 'sample response'
end end
end end

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

@@ -6,7 +6,7 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
include described_class include described_class
attr_accessor :filter_done attr_accessor :filter_done
before_action only: :redirect do before_action only: :context_with_filter do
self.filter_done = true self.filter_done = true
end end
@@ -17,19 +17,16 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
[:no_context, *args] [:no_context, *args]
end end
context_handler :block do |*args| def handler_method(*args)
[:block_result, *args] [:method_result_1, *args]
end end
context_handler :redirect def context_with_filter(*args)
context_handler :other_redirect, :redirect [:method_result_2, *args]
def redirect(*args)
[:method_result, *args]
end end
def action(*args) def action!(*args)
[:action_result, *args] [:command_result, *args]
end end
private private
@@ -52,85 +49,59 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
it { should_not change { session[:context] } } it { should_not change { session[:context] } }
end end
context 'when context is handled by block' do context 'when context is handled by handler_method' do
before { session[:context] = :block } before { session[:context] = :handler_method }
its(:call) { should eq [:block_result, *text.split] } its(:call) { should eq [:method_result_1, *text.split] }
it { should_not change(controller, :filter_done) } it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
context 'when message has no text' do context 'when message has no text' do
let(:payload) { {'audio' => {'file_id' => 123}} } let(:payload) { {'audio' => {'file_id' => 123}} }
its(:call) { should eq [:block_result] } its(:call) { should eq [:method_result_1] }
end end
context 'when message has new command' do context 'when message has new command' do
let(:text) { '/action a s d' } let(:text) { '/action a s d' }
its(:call) { should eq [:action_result, 'a', 's', 'd'] } its(:call) { should eq [:command_result, 'a', 's', 'd'] }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
end end
end end
context 'when context is handled by short redirect' do context 'when context is handled by short context_with_filter' do
before { session[:context] = :redirect } before { session[:context] = :context_with_filter }
its(:call) { should eq [:method_result, *text.split] } its(:call) { should eq [:method_result_2, *text.split] }
it { should change(controller, :filter_done).to true } it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
it { should change(controller, :callbacks_runs).to 1 } it { should change(controller, :callbacks_runs).to 1 }
context 'when message has no text' do context 'when message has no text' do
let(:payload) { {'audio' => {'file_id' => 123}} } let(:payload) { {'audio' => {'file_id' => 123}} }
its(:call) { should eq [:method_result] } its(:call) { should eq [:method_result_2] }
it { should change(controller, :filter_done).to true } it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
end end
end end
context 'when context is handled by custom redirect' do context 'when context is command-action`s name' do
before { session[:context] = :other_redirect } before { session[:context] = :action! }
its(:call) { should eq [:method_result, *text.split] } its(:call) { should eq [:command_result, *text.split] }
it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil }
end
context 'when context is action`s name but not mapped' do
before { session[:context] = :action }
its(:call) { should eq [:no_context, payload] }
it { should_not change(controller, :filter_done) } it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
end end
context 'when context_to_action is true' do context 'when context is not an action`s name' do
before { controller_class.context_to_action! } before { session[:context] = :not_action }
it do
context 'when context is not set' do should raise_error(AbstractController::ActionNotFound).
its(:call) { should eq [:no_context, payload] } and change { session[:context] }.to nil
it { should_not change(controller, :filter_done) }
it { should_not change { session[:context] } }
end end
end
context 'when context is action`s name but not mapped' do context 'when context is invalid name' do
before { session[:context] = :action } before { session[:context] = :invalid }
its(:call) { should eq [:action_result, *text.split] } it do
it { should_not change(controller, :filter_done) } should raise_error(AbstractController::ActionNotFound).
it { should change { session[:context] }.to nil } and change { session[:context] }.to nil
end
context 'when context is invalid' do
before { session[:context] = :invalid }
it 'raises error and clears context' do
expect do
should raise_error AbstractController::ActionNotFound
end.to change { session[:context] }.to nil
end
end
context 'when context is private method`s name' do
before { session[:context] = :not_action }
it 'raises error and clears context' do
expect do
should raise_error AbstractController::ActionNotFound
end.to change { session[:context] }.to nil
end
end end
end end
end end

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

@@ -4,8 +4,8 @@ RSpec.describe Telegram::Bot::UpdatesController do
let(:respond_type) { :photo } let(:respond_type) { :photo }
let(:result) { double(:result) } let(:result) { double(:result) }
let(:payload_type) { :message } let(:payload_type) { :message }
let(:payload) { {message_id: double(:message_id)} } let(:payload) { {message_id: double(:message_id), chat: chat} }
let(:chat) { {'id' => double(:chat_id)} } let(:chat) { {id: double(:chat_id)} }
shared_examples 'missing chat' do shared_examples 'missing chat' do
context 'when chat is missing' do context 'when chat is missing' do
@@ -19,9 +19,8 @@ RSpec.describe Telegram::Bot::UpdatesController do
include_examples 'missing chat' include_examples 'missing chat'
it 'sets chat_id & reply_to_message_id' do it 'sets chat_id & reply_to_message_id' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{respond_type}"). expect(bot).to receive("send_#{respond_type}").
with(params.merge(chat_id: chat['id'])) { result } with(params.merge(chat_id: chat[:id])) { result }
should eq result should eq result
end end
end end
@@ -31,19 +30,18 @@ RSpec.describe Telegram::Bot::UpdatesController do
include_examples 'missing chat' include_examples 'missing chat'
it 'sets chat_id & reply_to_message_id' do it 'sets chat_id & reply_to_message_id' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{respond_type}").with(params.merge( expect(bot).to receive("send_#{respond_type}").with(params.merge(
chat_id: chat['id'], chat_id: chat[:id],
reply_to_message_id: payload[:message_id], reply_to_message_id: payload[:message_id],
)) { result } )) { result }
should eq result should eq result
end end
context 'when update is not set' do context 'when update is not set' do
let(:update) { {chat: chat} } let(:controller_args) { [bot, chat: deep_stringify(chat)] }
it 'sets chat_id' do it 'sets chat_id' do
expect(bot).to receive("send_#{respond_type}"). expect(bot).to receive("send_#{respond_type}").
with(params.merge(chat_id: chat['id'])) { result } with(params.merge(chat_id: chat[:id])) { result }
should eq result should eq result
end end
end end
@@ -70,7 +68,7 @@ RSpec.describe Telegram::Bot::UpdatesController do
it 'sets chat_id & message_id' do it 'sets chat_id & message_id' do
expect(bot).to receive("edit_message_#{type}").with(params.merge( expect(bot).to receive("edit_message_#{type}").with(params.merge(
message_id: payload[:message][:message_id], message_id: payload[:message][:message_id],
chat_id: payload[:message][:chat]['id'], chat_id: payload[:message][:chat][:id],
)) { result } )) { result }
should eq result should eq result
end end

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

@@ -8,11 +8,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Rescue do
Class.new(Telegram::Bot::UpdatesController) do Class.new(Telegram::Bot::UpdatesController) do
rescue_from ArgumentError, with: -> { respond_with :message, text: 'Rescued' } rescue_from ArgumentError, with: -> { respond_with :message, text: 'Rescued' }
def rescuable(*) def rescuable!(*)
raise ArgumentError, 'rescuable' raise ArgumentError, 'rescuable'
end end
def not_rescuable(*) def not_rescuable!(*)
raise 'not_rescuable' raise 'not_rescuable'
end end
end end

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

@@ -19,11 +19,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
controller_class.class_eval do controller_class.class_eval do
self.session_store = :memory_store self.session_store = :memory_store
def write(text) def write!(text)
session[:text] = text session[:text] = text
end end
def read def read!
session[:text] session[:text]
end end
@@ -36,7 +36,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
end end
def build_message(text, from) def build_message(text, from)
{'message' => {'text' => text, 'from' => from.stringify_keys}} deep_stringify(message: {text: text, from: from})
end end
it 'stores session between requests' do it 'stores session between requests' do

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

@@ -0,0 +1,80 @@
RSpec.describe Telegram::Bot::UpdatesController::Translation do
def self.stub_translations(translations)
around do |ex|
backend = I18n.backend
I18n.backend = I18n::Backend::Simple.new
begin
I18n.backend.store_translations 'en', telegram: {webhooks: translations}
ex.run
ensure
I18n.backend = backend
end
end
end
let(:result) { double(:result) }
describe '#translate' do
stub_translations(
start: {
hello: 'Hello from start!',
hi: 'Hi %{name} from start', # rubocop:disable Style/FormatStringToken
},
)
let(:controller) do
double(
controller_path: 'telegram/webhooks',
action_name: 'start!',
).tap { |x| x.extend(described_class) }
end
it 'uses action_name without ! for lazy translations' do
expect(I18n).to receive(:translate).with('telegram.webhooks.start.hello',
default: [:'telegram.webhooks.hello'],
) { result }
expect(controller.t('.hello')).to eq(result)
expect(I18n).to receive(:translate).with('telegram.webhooks.start.hi',
default: [:'telegram.webhooks.hi'],
name: 'Dude',
) { result }
expect(controller.t('.hi', name: 'Dude')).to eq(result)
end
it 'translates' do
expect(controller.t('.hello')).to eq('Hello from start!')
expect(controller.t('.hi', name: 'Dude')).to eq('Hi Dude from start')
end
end
describe described_class::ClassMethods do
stub_translations(
hello: 'Hello!',
hi: 'Hi %{name}', # rubocop:disable Style/FormatStringToken
)
describe '#translate' do
let(:controller_class) do
double(controller_path: 'telegram/webhooks').
tap { |x| x.extend(described_class) }
end
it 'uses controller_path for lazy translations' do
expect(I18n).to receive(:translate) do |*args, **kwargs|
# not using .with to support ruby 2.x and 3.x
expect([*args, kwargs]).to eq(['telegram.webhooks.hello', {}])
result
end
expect(controller_class.t('.hello')).to eq(result)
expect(I18n).to receive(:translate).with('telegram.webhooks.hi', name: 'Dude') { result }
expect(controller_class.t('.hi', name: 'Dude')).to eq(result)
end
it 'translates' do
expect(controller_class.t('.hello')).to eq('Hello!')
expect(controller_class.t('.hi', name: 'Dude')).to eq('Hi Dude')
end
end
end
end

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

@@ -9,14 +9,19 @@ RSpec.describe Telegram::Bot::UpdatesController::TypedUpdate do
context 'when `update` is a virtus model' do context 'when `update` is a virtus model' do
subject { controller } subject { controller }
unique_types = Telegram::Bot::UpdatesController::PAYLOAD_TYPES - %w[ unique_types = (Telegram::Bot::UpdatesController::PAYLOAD_TYPES - %w[
edited_message edited_message
channel_post channel_post
edited_channel_post edited_channel_post
] my_chat_member
unique_types.each do |type| chat_member
]).
map { |x| [x, Telegram::Bot::Types.const_get(x.camelize)] }.to_h.
merge(
'chat_member' => Telegram::Bot::Types::ChatMemberUpdated
)
unique_types.each do |type, type_class|
context "with #{type}" do context "with #{type}" do
type_class = Telegram::Bot::Types.const_get(type.camelize)
let(:payload_type) { type } let(:payload_type) { type }
let(:payload) do let(:payload) do
{}.tap do |result| {}.tap do |result|

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

@@ -1,81 +1,5 @@
RSpec.describe Telegram::Bot::UpdatesController do RSpec.describe Telegram::Bot::UpdatesController do
include_context 'telegram/bot/updates_controller' include_context 'telegram/bot/updates_controller'
let(:other_bot_name) { 'other_bot' }
describe '.action_for_command' do
subject { ->(*args) { described_class.action_for_command(*args) } }
def assert_subject(input, expected)
expect(subject.call input).to eq expected
end
it 'bypasses and downcases not conflictint commands' do
assert_subject 'test', 'test'
assert_subject 'TeSt', 'test'
assert_subject '_Te1St', '_te1st'
end
it 'adds _on to conflicting commands' do
described_class::PAYLOAD_TYPES.each do |x|
assert_subject x, "on_#{x}"
assert_subject x.upcase, "on_#{x}"
end
assert_subject '1TeSt', 'on_1test'
end
end
describe '.command_from_text' do
subject { ->(*args) { described_class.command_from_text(*args) } }
def assert_subject(input, cmd, *args)
expected = cmd ? [cmd, args] : cmd
expect(subject.call(*input)).to eq expected
end
let(:max_cmd_size) { 32 }
let(:long_cmd) { 'a' * (max_cmd_size - 1) }
let(:too_long_cmd) { 'a' * max_cmd_size }
it 'works for simple commands' do
assert_subject '/test', 'test'
assert_subject '/tE_2_St', 'tE_2_St'
assert_subject '/123', '123'
assert_subject "/#{long_cmd}", long_cmd
end
it 'works for simple messages' do
assert_subject 'text', nil
assert_subject ' ', nil
assert_subject ' text', nil
assert_subject ' 1', nil
assert_subject ' /text', nil
assert_subject '/te-xt', nil
assert_subject 'text /cmd ', nil
assert_subject "/#{too_long_cmd}", nil
end
it 'works for mentioned commands' do
assert_subject ['/test@bot', 'bot'], 'test'
assert_subject ['/test@otherbot', 'bot'], nil
assert_subject ['/test@Bot', 'bot'], nil
assert_subject '/test@bot', nil
assert_subject ['/test@bot', true], 'test'
assert_subject ['/test@otherbot', true], 'test'
end
it 'works for commands with args' do
assert_subject '/test arg', 'test', 'arg'
assert_subject '/test arg 1 2', 'test', 'arg', '1', '2'
assert_subject ['/test@bot arg', 'bot'], 'test', 'arg'
assert_subject ['/test@otherbot arg', 'bot'], nil
assert_subject '/test@bot arg', nil
end
it 'works for commands with multiline args' do
assert_subject "/test arg\nother", 'test', 'arg', 'other'
assert_subject "/test one\ntwo\n\nthree", 'test', 'one', 'two', 'three'
end
end
describe '#action_for_payload' do describe '#action_for_payload' do
subject { controller.action_for_payload } subject { controller.action_for_payload }
@@ -87,60 +11,30 @@ RSpec.describe Telegram::Bot::UpdatesController do
context 'when payload is inline_query' do context 'when payload is inline_query' do
let(:payload_type) { 'inline_query' } let(:payload_type) { 'inline_query' }
let(:payload) { stub_payload(:id, :from, :location, :query, :offset) } let(:payload) { stub_payload(:id, :from, :location, :query, :offset) }
it { should eq [false, payload_type, payload.values_at(:query, :offset)] } it { should eq [payload_type, payload.values_at(:query, :offset)] }
end end
context 'when payload is chosen_inline_result' do context 'when payload is chosen_inline_result' do
let(:payload_type) { 'chosen_inline_result' } let(:payload_type) { 'chosen_inline_result' }
let(:payload) { stub_payload(:result_id, :from, :location, :inline_message_id, :query) } let(:payload) { stub_payload(:result_id, :from, :location, :inline_message_id, :query) }
it { should eq [false, payload_type, payload.values_at(:result_id, :query)] } it { should eq [payload_type, payload.values_at(:result_id, :query)] }
end end
context 'when payload is callback_query' do context 'when payload is callback_query' do
let(:payload_type) { 'callback_query' } let(:payload_type) { 'callback_query' }
let(:payload) { stub_payload(:id, :from, :message, :inline_message_id, :data) } let(:payload) { stub_payload(:id, :from, :message, :inline_message_id, :data) }
it { should eq [false, payload_type, payload.values_at(:data)] } it { should eq [payload_type, payload.values_at(:data)] }
end
context 'when payload is poll_answer' do
let(:payload_type) { 'poll_answer' }
let(:payload) { stub_payload(:poll_id, :user, :option_ids) }
it { should eq [payload_type, payload.values_at(:poll_id, :option_ids)] }
end end
context 'when payload is not supported' do context 'when payload is not supported' do
let(:payload_type) { '_unsupported_' } let(:payload_type) { '_unsupported_' }
it { should eq [false, :unsupported_payload_type, []] } it { should eq [:unsupported_payload_type, []] }
end
%w[message channel_post].each do |type|
context "when payload is edited_#{type}" do
let(:payload_type) { "edited_#{type}" }
it { should eq [false, payload_type, [payload]] }
end
context 'when payload is message' do
let(:payload_type) { type }
let(:payload) { {'text' => text} }
let(:text) { 'test' }
it { should eq [false, payload_type, [payload]] }
context 'with command' do
let(:text) { "/test#{"@#{mention}" if mention} arg 1 2" }
let(:mention) {}
it { should eq [true, 'test', %w[arg 1 2]] }
context 'with mention' do
let(:mention) { bot.username }
it { should eq [true, 'test', %w[arg 1 2]] }
end
context 'with mention for other bot' do
let(:mention) { other_bot_name }
it { should eq [false, payload_type, [payload]] }
end
end
context 'without text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
it { should eq [false, payload_type, [payload]] }
end
end
end end
custom_payload_types = %w[ custom_payload_types = %w[
@@ -151,11 +45,12 @@ RSpec.describe Telegram::Bot::UpdatesController do
inline_query inline_query
chosen_inline_result chosen_inline_result
callback_query callback_query
poll_answer
] ]
(described_class::PAYLOAD_TYPES - custom_payload_types).each do |type| (described_class::PAYLOAD_TYPES - custom_payload_types).each do |type|
context "when payload is #{type}" do context "when payload is #{type}" do
let(:payload_type) { type } let(:payload_type) { type }
it { should eq [false, payload_type, [payload]] } it { should eq [payload_type, [payload]] }
end end
end end
end end
@@ -278,11 +173,13 @@ RSpec.describe Telegram::Bot::UpdatesController do
instance_eval(&block) instance_eval(&block)
context 'when re-initialized' do context 'when re-initialized' do
let(:controller) do let(:controller) do
described_class.new(double(:other_bot), build_update(:message, initial_update = deep_stringify message: {
text: 'original message', text: 'original message',
from: double(:original_from), from: double(:original_from),
chat: double(:original_chat), chat: double(:original_chat),
)).tap { |x| x.send(:initialize, bot, update) } }
described_class.new(double(:other_bot), initial_update).
tap { |x| x.send(:initialize, *controller_args) }
end end
instance_eval(&block) instance_eval(&block)
end end
@@ -300,7 +197,7 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
context 'when options hash is given' do context 'when options hash is given' do
let(:update) { {from: from, chat: chat} } let(:controller_args) { [bot, from: from, chat: chat] }
with_reinitialize do with_reinitialize do
its(:bot) { should eq bot } its(:bot) { should eq bot }
its(:update) { should eq nil } its(:update) { should eq nil }

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

@@ -17,12 +17,12 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib'] spec.require_paths = ['lib']
spec.required_ruby_version = '~> 2.0' spec.required_ruby_version = '>= 2.4'
spec.add_dependency 'actionpack', '>= 4.0', '< 6.0' spec.add_dependency 'actionpack', '>= 4.0', '< 7.1'
spec.add_dependency 'activesupport', '>= 4.0', '< 6.0' spec.add_dependency 'activesupport', '>= 4.0', '< 7.1'
spec.add_dependency 'httpclient', '~> 2.7' spec.add_dependency 'httpclient', '~> 2.7'
spec.add_development_dependency 'bundler', '~> 1.16' spec.add_development_dependency 'bundler', '> 1.16'
spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rake', '~> 13.0'
end end