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

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

...

174 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
62c3e3c861 v0.16.2 2024-05-12 16:55:41 -04:00
Max Melentiev
da22ecbe79 Update to Bot API 7.3 2024-05-12 16:54:41 -04:00
Max Melentiev
ffd17ec933 Fix frozen string issue for ruby < 3 (#235) 2024-05-12 21:42:33 +01:00
Max Melentiev
ad1eb5a47f Use debug gem instead of pry (#234) 2024-05-12 21:41:03 +01:00
Roman Novoselov
b05f17b02b Treat callback_query_id as string in spec helper (#231) 2024-04-16 18:15:15 +01:00
Max Melentiev
6140f7a68a Bot api 7.0 (#229) 2023-12-30 16:23:57 +00:00
Max Melentiev
57b17f068c Update rubopcop (#227)
* Update rubopcop [WIP]

* Further rubocop fixes

* Fix rubocop-rails cops

* Fix spec

* Disable rubocop suggestions

* Run rubocop in github actions rather than in codeclimate
2023-12-30 15:20:27 +00:00
Max Melentiev
7bcb0caded v0.16.1 2023-12-30 18:18:35 +06:00
Max Melentiev
d048f42c6c Merge pull request #225 from mario-amazing/master
Fix rails 7.1 broadcast error
2023-12-18 20:58:13 +00:00
mario_amazing
f54b36d9b7 Fix rails 7.1 broadcast error 2023-12-18 18:29:04 +03:00
Max Melentiev
cd2d3d8d87 v0.16.0 2023-11-09 21:33:24 -08:00
Max Melentiev
ee666f0e90 Merge pull request #207 from telegram-bot-rb/webhook_request
Add UpdatesController#webhook_request
2023-11-09 21:27:39 -08:00
Max Melentiev
759b125d82 Merge branch 'master' into webhook_request 2023-11-09 21:16:12 -08:00
Max Melentiev
21ca16f613 Merge pull request #209 from anko20094/added-chat-join-request-to-payload-types
Added chat_join_request to PAYLOAD_TYPES + update API
2023-11-09 21:14:23 -08:00
Max Melentiev
7cfceb7439 Update CHANGELOG.md 2023-11-09 21:01:07 -08:00
Max Melentiev
29603e5b60 Update .gitignore 2023-11-09 21:00:36 -08:00
Max Melentiev
5822eff988 Merge branch 'master' into added-chat-join-request-to-payload-types 2023-11-09 20:59:43 -08:00
Max Melentiev
cbe7186c44 Add UpdatesController#webhook_request 2023-11-09 20:56:45 -08:00
Max Melentiev
ca20bb0533 Merge pull request #221 from epergo/ep/fix-pipeline
Update testing matrix
2023-11-05 06:50:16 -08:00
Eloy Pérez
6f8565fbd5 Update testing matrix 2023-11-05 10:23:40 +01:00
Max Melentiev
a16928267a Merge pull request #217 from epergo/ep/rails-7-1
Allow installation in Rails 7.1
2023-10-28 07:59:03 +01:00
Eloy Pérez
83bc13bac6 Allow installation in Rails 7.1 2023-10-27 19:43:17 +02:00
Max Melentiev
1e1814a26d Merge pull request #213 from Nmerey/add-login-url-method
Add new commands available at API 6.8
2023-08-28 11:56:39 +01:00
Nmerey
ae9a6a501d Add new methods available at API 6.8 2023-08-28 14:51:15 +06:00
Danyil Shkoropad
74c2c28ab5 Added chat_join_request to PAYLOAD_TYPES + update API 2023-06-28 23:54:47 +03:00
Max Melentiev
af85966939 Fix status badge 2023-06-28 21:23:43 +01:00
Max Melentiev
5145c31d73 Merge pull request #210 from telegram-bot-rb/gh-actions
Move to github actions from travis-ci
2023-06-28 21:15:26 +01:00
Max Melentiev
cbb3a85757 Move to github actions from travis-ci 2023-06-28 20:55:51 +01:00
Max Melentiev
f0c5222c8f Merge pull request #206 from marckohlbrugge/master
Add commands upto Bot API 6.6
2023-04-16 19:18:51 +01:00
Marc Köhlbrugge
b4e5b056e0 Add commands upto Bot API 6.6 2023-04-06 23:11:31 +08:00
Max Melentiev
be74094c90 Merge pull request #200 from ceritium/patch-1
Fix typo on comment
2023-02-13 08:47:48 +01:00
Jose Galisteo
e53c31b79f Fix typo on comment 2023-02-11 14:56:39 +01:00
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
Max Melentiev
2eb2670ed2 v0.13.1 2018-05-28 13:27:35 +03:00
Max Melentiev
6a5e642470 Fix #chat for non-message updates with TypedUpdates enabled
Fixes #82
2018-05-19 12:53:15 +03:00
printercu
4ff5a58b98 Merge pull request #79 from telegram-bot-rb/fix-travis
Fix travis build
2018-05-14 13:20:35 +03:00
printercu
ffb13321b4 Fix travis build 2018-05-14 12:18:14 +03:00
printercu
3bdc4f5ae0 Merge pull request #72 from WA9ACE/patch-1
Spelling correction
2018-04-25 11:15:23 +03:00
Max Melentiev
5603016e4a Extracted typed response mappings to telegram-bot-types gem
Fixes #75
2018-04-25 11:07:34 +03:00
Caleb Albritton
fa6b9a825e Spelling correction 2018-04-13 11:50:05 -04:00
printercu
bdff6b2e1c Merge pull request #65 from telegram-bot-rb/rails-6
Prepare for rails 6
2018-02-21 21:34:49 +03:00
Max Melentiev
f39a451192 Add rails-6 to build matrix 2018-02-21 20:35:03 +03:00
Max Melentiev
9af6de8d36 Avoid Hash#transform_values. Fixes #63 2018-02-21 19:49:09 +03:00
Max Melentiev
c86b2ef09b v0.13.0 2018-02-02 11:03:14 +03:00
Max Melentiev
bfcb1c5f41 Deleted page from wiki with old routes examples, remove this link from readme 2018-02-02 11:01:22 +03:00
Max Melentiev
88c8d4fd4e Deprecate telegram_webhooks in favor of telegram_webhook 2018-02-02 10:52:48 +03:00
Max Melentiev
06253c940e Extract methods in UpdatesPoller 2018-02-02 10:37:45 +03:00
printercu
eb24816e1e Merge pull request #61 from telegram-bot-rb/test-cc
Setup codeclimate integration
2018-01-18 14:12:11 +03:00
Max Melentiev
a67a643c7b Build new rubies on travis 2018-01-18 14:04:39 +03:00
Max Melentiev
28db729320 Update changelog 2018-01-18 12:43:10 +03:00
Max Melentiev
7f1f2bb884 Fix readme 2018-01-18 12:42:11 +03:00
Max Melentiev
88ae24b7e8 Fix codeclimate issues 2018-01-18 12:42:04 +03:00
Max Melentiev
3a876a42df Setup codeclimate 2018-01-18 12:31:32 +03:00
Yuri S
66991d3653 Support for :credentials on Rails v5.2.x. (#60) 2018-01-17 10:40:24 +03:00
Max Melentiev
5407c5028f Fix specs for rails 4 2018-01-09 12:00:49 +03:00
Max Melentiev
b41032eee9 Update rubocop 2018-01-05 14:21:08 +03:00
Max Melentiev
707e532963 Specs for instrumentation 2018-01-05 14:20:19 +03:00
Max Melentiev
45e8a7283f rescue_from feature 2018-01-05 13:19:44 +03:00
Max Melentiev
70a1ed8a8f Compact requires 2018-01-05 12:52:46 +03:00
Max Melentiev
384d203b59 Fix spec helpers for callback queries 2017-12-04 09:47:17 +03:00
Max Melentiev
9e396cabc4 New methods from bot api v3.5
Collect all api helper-methods in Client::ApiHelper module.
Add bin/fetch-telegram-methods to update methods list from website.
2017-11-24 11:08:12 +03:00
Max Melentiev
81c18034a5 Add some new files after appraisal install 2017-11-24 11:04:38 +03:00
printercu
f332d27ec8 Update README.md 2017-11-18 11:27:24 +03:00
printercu
fa9b01d9d2 Merge pull request #49 from telegram-bot-rb/update_rubocop
Update rubocop
2017-11-17 09:59:43 +03:00
Max Melentiev
e06882f7af Release rails dependencies 2017-11-17 09:53:10 +03:00
Max Melentiev
1a30b430df Update rubocop, add appraisal gem 2017-11-17 09:46:49 +03:00
Max Melentiev
da93abaaf7 v0.12.1 2017-10-06 11:23:56 +03:00
printercu
0ae17fb34e Merge pull request #45 from telegram-bot-rb/update_sessions
Update sessions
2017-10-06 11:19:01 +03:00
Max Melentiev
7e41bd4f9b Allow use different sessions for MessageContext 2017-10-06 11:10:05 +03:00
Max Melentiev
e529a79339 Don't use Rails.cache as fallback for session_store
It's NullStore in development by default so session does not work,
and indeed should not be dependent.
2017-10-06 11:07:11 +03:00
Max Melentiev
ef4f3caa5f Make #session raise error when store is not configured
Add more explanation on sessions in readme
2017-10-06 11:05:25 +03:00
Max Melentiev
df7039326d Run set_webhook task always in sync way
Fixes telegram-bot-rb/telegram_bot_app#7
2017-10-05 22:11:22 +03:00
Max Melentiev
4f47506e22 Fix typo in readme 2017-08-11 15:10:05 +03:00
Max Melentiev
6abbe5489d v0.12.0 2017-08-11 15:04:41 +03:00
Max Melentiev
ff7de976d0 Add specs for #edit_message 2017-08-11 15:04:19 +03:00
Max Melentiev
aaba27eb70 New API methods and payload types (up to Bot API 3.2) 2017-08-11 15:04:02 +03:00
printercu
88a4a9aab3 Merge pull request #37 from zuf/master
update rails dependency (<5.2 instead <=5.1)
2017-08-11 13:43:13 +03:00
Egor
9387043ec1 fix rails dependency (<5.2 instead <=5.1) 2017-08-10 18:07:21 +03:00
Max Melentiev
3dca35ba9a callback_query context for RSpec 2017-07-25 10:58:18 +03:00
Max Melentiev
6879618c00 edit_message helper 2017-07-25 10:49:39 +03:00
Max Melentiev
6e4a69b6b1 Fix storing token in ClientStub when config hash is given 2017-05-17 17:45:21 +06:00
printercu
b8db576452 Merge pull request #31 from telegram-bot-rb/integration_test
Integration test with rails app and two bots
2017-05-15 22:20:26 +06:00
Max Melentiev
2fffdeb475 Integration test with rails app and two bots 2017-05-15 22:11:52 +06:00
Max Melentiev
9defa42f40 Add notes on testing multiple bots to readme
Fixes #29
2017-05-11 14:01:42 +06:00
Max Melentiev
0320507abe Telegram.bot raises error when there is no default config 2017-05-11 13:57:29 +06:00
Max Melentiev
cd3d3739a4 v0.11.3 2017-05-05 17:30:49 +06:00
Max Melentiev
a054ac704c Build with rails 5.1 on travis 2017-05-05 17:28:59 +06:00
printercu
23aa92e614 Merge pull request #28 from ironbrains/master
Increase dependencies version
2017-05-05 17:19:05 +06:00
Bulat F
a49dca66fa Increase dependencies version
- Add support `activesupport 5.1`
- Add support `actionpack 5.1`
2017-05-05 10:16:30 +03:00
Max Melentiev
2d5ca981ce Rails 5.1 deep symbolized secrets support
Fixes #26
2017-04-22 12:02:59 +03:00
Max Melentiev
cf9bd87cf2 Add notice about how to require gem to Readme
Fixes #24
2017-03-11 21:33:20 +03:00
Max Melentiev
48ff7a4f43 Fix poller for typed responses
Fixes: #22
2017-03-05 17:56:32 +03:00
Max Melentiev
17260cb5a9 v0.11.0 2017-02-10 09:34:15 +03:00
Max Melentiev
817af95369 Remove Bot::StaleChat in favor of Bot::Forbidden 2017-02-10 09:33:41 +03:00
Max Melentiev
99fbe41b16 Fixed support for *channel_post updates 2016-12-09 13:33:55 +03:00
104 изменённых файлов: 3363 добавлений и 1501 удалений

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

@@ -0,0 +1,4 @@
checks:
method-complexity:
config:
threshold: 6 # should be just fine

43
.github/workflows/ci.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
name: Lint & Test
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- run: bundle exec rubocop
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby:
- '2.7'
gemfile:
- gemfiles/rails_61.gemfile
- gemfiles/rails_60.gemfile
- gemfiles/rails_52.gemfile
- gemfiles/rails_51.gemfile
include:
# ruby 3.x
- ruby: '3.2'
gemfile: gemfiles/rails_71.gemfile
- ruby: '3.0'
gemfile: gemfiles/rails_61.gemfile
- ruby: '3.0'
gemfile: gemfiles/rails_60.gemfile
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake spec

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

@@ -1,9 +1,12 @@
/.bundle/
/.yardoc
/Gemfile.lock
/gemfiles/*.lock
/gemfiles/.bundle
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/log/
/tmp/

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

@@ -1,29 +1,75 @@
Rails: {Enabled: true}
require: rubocop-rails
AllCops:
TargetRubyVersion: 2.4
NewCops: enable
SuggestExtensions: false
Exclude:
- gemfiles/*
- tmp/*
- vendor/**/* # for github actions
Gemspec/DevelopmentDependencies: {Enabled: false}
Layout/ArgumentAlignment: {EnforcedStyle: with_fixed_indentation}
Layout/ParameterAlignment: {EnforcedStyle: with_fixed_indentation}
# Breaks
#
# I18n.t(key,
# param: val,
# # ...
# )
Layout/ClosingParenthesisIndentation: {Enabled: false}
Layout/DotPosition: {EnforcedStyle: trailing}
Layout/EmptyLineAfterGuardClause: {Enabled: false}
Layout/FirstParameterIndentation: {EnforcedStyle: consistent}
Layout/LineContinuationLeadingSpace: {Enabled: false}
Layout/LineEndStringConcatenationIndentation: {EnforcedStyle: indented}
Layout/LineLength: {Max: 100}
# Same as Layout/ClosingParenthesisIndentation
Layout/MultilineMethodCallBraceLayout: {Enabled: false}
Layout/MultilineMethodCallIndentation: {EnforcedStyle: indented}
Layout/MultilineOperationIndentation: {EnforcedStyle: indented}
Layout/SpaceInsideHashLiteralBraces: {EnforcedStyle: no_space}
# Offences named scopes and `expect {}.to change {}`.
Lint/AmbiguousBlockAssociation: {Enabled: false}
Lint/EmptyBlock:
Exclude:
- spec/**/*
Naming/FileName:
Exclude:
- lib/tasks/telegram-bot.rake
Naming/MethodParameterName: {MinNameLength: 2}
Naming/PredicateName: {Enabled: false}
Naming/RescuedExceptionsVariableName: {PreferredName: exception}
Naming/VariableNumber: {EnforcedStyle: snake_case}
Style/Alias: {Enabled: false}
Style/AlignParameters:
# Disable, till rubocop supports combination of styles.
# Use one of this styles where appropriate, keep it clean, compact and readable.
Enabled: false
# EnforcedStyle:
# - with_first_parameter
# - with_fixed_indentation
Style/AndOr: {EnforcedStyle: conditionals}
Style/ClosingParenthesisIndentation: {Enabled: false}
Style/Documentation: {Enabled: false}
Style/DotPosition: {EnforcedStyle: trailing}
Style/FirstParameterIndentation: {EnforcedStyle: consistent}
Style/IfUnlessModifier: {Enabled: false}
# Consistent to other definitions.
Style/EmptyMethod: {EnforcedStyle: expanded}
Style/Lambda: {EnforcedStyle: literal}
Style/ModuleFunction: {Enabled: false}
Style/MultilineOperationIndentation: {EnforcedStyle: indented}
Style/NestedParenthesizedCalls: {Enabled: false}
Style/PredicateName: {Enabled: false}
Style/SignalException: {EnforcedStyle: only_raise}
Style/SpaceInsideHashLiteralBraces: {EnforcedStyle: no_space}
Style/TrailingCommaInArguments: {Enabled: false}
Style/TrailingCommaInLiteral: {EnforcedStyleForMultiline: comma}
Style/TrailingCommaInArrayLiteral: {EnforcedStyleForMultiline: comma}
Style/TrailingCommaInHashLiteral: {EnforcedStyleForMultiline: comma}
Metrics/AbcSize: {Max: 21}
Metrics/LineLength: {Max: 100}
# Other metrics are just enough.
# This one offences all specs, routes and some initializers.
Metrics/BlockLength: {Enabled: false}
Metrics/MethodLength: {Max: 30}
Metrics/CyclomaticComplexity: {Max: 8}
Rails/IndexWith: {Enabled: false}
Rails/RakeEnvironment: {Enabled: false}
Rails/ShortI18n: {Enabled: false}

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

@@ -1,9 +0,0 @@
language: ruby
cache: bundler
rvm:
- 2.2.3
env:
- RAILS=4
- RAILS=5
notifications:
email: false

31
Appraisals Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
# frozen_string_literal: true
appraise 'rails-71' do
gem 'actionpack', '~> 7.1.0'
gem 'railties', '~> 7.1.0'
end
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
gem 'actionpack', '~> 6.0.0'
gem 'railties', '~> 6.0.0'
end
appraise 'rails-52' do
gem 'actionpack', '~> 5.2.0'
gem 'railties', '~> 5.2.0'
end
appraise 'rails-51' do
gem 'actionpack', '~> 5.1.0'
gem 'railties', '~> 5.1.0'
end

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

@@ -1,4 +1,154 @@
# 0.10.1
# Unreleased
# 0.16.2
- Update to Bot API 7.3
- Add `# frozen_string_literal: true` to all source files.
# 0.16.1
- Fix logger in poller mode in rails 7.1
# 0.16.0
- Add `UpdatesController#webhook_request` that is set to `ActionDispatch::Request`
in webhook mode.
- Update to Bot API 6.8
- Added `chat_join_request` to PAYLOAD_TYPES
# 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
- Extracted typed response mappings to telegram-bot-types gem.
It now provides definitions for all API v3.5 methods.
- Fix #chat for non-message updates with TypedUpdates enabled
# 0.13.0
- `rescue_from`.
- Support for `credentials` store in Rails 5.2.
- Deprecate `telegram_webhooks` in favor of `telegram_webhook`.
It was too complicated and such routes looked ugly.
# 0.12.4
- Fix spec helpers for callback queries.
# 0.12.3
- New methods from Bot API v3.5
- Collect all api helper-methods in Client::ApiHelper module.
- Add `bin/fetch-telegram-methods` to update API methods list from website.
# 0.12.1
- Fix `set_webhook` rake task for async bots with self-issued certificates.
- Make `#session` raise error when store is not configured,
don't use Rails.cache as fallback for session_store.
- Allow use different sessions for MessageContext.
# 0.12.0
- New API methods and payload types (up to Bot API 3.2).
- Fix rails 5.1.x support.
- RSpec context for callback_query updates.
- `edit_message` helper.
- ClientStub saves and returns token. Fixes testing multiple bots.
- Raise descriptive error when accessing not-configured bot.
# 0.11.3
- Release dependencies for Rails 5.1.
# 0.11.2
- Rails 5.1 deep symbolized secrets support.
# 0.11.1
- Fixed poller for typed response.
# 0.11.0
- Remove Bot::StaleChat in favor of Bot::Forbidden, as Telegram adds more
and more new descriptions.
Please open an issue if you face a problem.
# 0.10.2
- Support `(edited_)channel_post` updates.
- New methods from 2.3, 2.3.1 API updates.

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

@@ -1,24 +1,22 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec
case ENV['RAILS']
when '5'
gem 'actionpack', '5.0.0.rc1'
when '4'
gem 'actionpack', '~> 4.2'
end
group :development do
gem 'sdoc', '~> 0.4.1'
gem 'pry', '~> 0.10.1'
gem 'pry-byebug', '~> 3.2.0'
gem 'appraisal', '~> 2.2'
gem 'telegram-bot-types', '~> 0.3.0'
gem 'debug', '~> 1.9.2'
gem 'sdoc', '~> 2.0.3'
gem 'rspec', '~> 3.5.0'
gem 'rspec-its', '~> 1.1.0'
gem 'telegram-bot-types', '~> 0.6.3'
gem 'rubocop', '~> 0.37.0'
gem 'rspec', '~> 3.12.0'
gem 'rspec-its', '~> 1.3.0'
gem 'rspec-rails', '~> 4.0.2'
gem 'coveralls', '~> 0.8.2', require: false
gem 'rubocop', '~> 1.59.0'
gem 'rubocop-rails', '~> 2.23.1'
gem 'coveralls', '~> 0.8.23', require: false
end

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

@@ -2,30 +2,56 @@
[![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)
[![Build Status](https://travis-ci.org/telegram-bot-rb/telegram-bot.svg)](https://travis-ci.org/telegram-bot-rb/telegram-bot)
[![Lint & Test](https://github.com/telegram-bot-rb/telegram-bot/actions/workflows/ci.yml/badge.svg)](https://github.com/telegram-bot-rb/telegram-bot/actions/workflows/ci.yml)
Tools for developing bot for Telegram. Best used with Rails, but can be be used in
[standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Non-rails-application).
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).
Supposed to be used in webhook-mode in production, and poller-mode
in development, but you can use poller in production if you want.
Package contains:
- Ligthweight client for bot API (with fast and thread-safe
- Lightweight client for bot API (with fast and thread-safe
[httpclient](https://github.com/nahi/httpclient) under the hood).
- Controller with message parser. Allows to write separate methods for each command.
- Controller with message parser: define methods for commands, not `case` branches.
- Middleware and routes helpers for production env.
- Poller with automatic source-reloader for development env.
- 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!
Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app)
with session, keyboards and inline queries.
Run it on your local machine in 1 minute!
And here is [app teamplate](https://github.com/telegram-bot-rb/rails_template)
to generate clean app in seconds.
And here is [app template](https://github.com/telegram-bot-rb/rails_template)
to generate new rails app in seconds.
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
@@ -37,57 +63,115 @@ gem 'telegram-bot'
And then execute:
$ bundle
```
bundle
```
Or install it yourself as:
$ gem install telegram-bot
```
gem install telegram-bot
```
Require if necessary:
```ruby
require 'telegram/bot'
```
## Usage
### Configuration
Add `telegram` section into `secrets.yml`:
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`.
```yml
telegram:
bots:
# just set the token
chat: TOKEN_1
# or add username to support commands with mentions (/help@ChatBot)
auction:
token: TOKEN_2
username: ChatBot
```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
},
}
# Single bot can be specified like this
bot: TOKEN
# or
bot:
token: TOKEN
username: SomeBot
Telegram.bot.get_updates
Telegram.bot == Telegram.bots[:default] # true
Telegram.bots[:chat].send_message(...)
```
### Client
### Configuration in Rails app
In Rails app `Telegram.bots_config` is read from `secrets.yml` automatically
from `telegram` section:
```yml
development:
telegram:
# Single bot can be specified like this
bot: TOKEN
# or
bot:
token: TOKEN
username: SomeBot
server: http://local.bot.api.server
# For multiple bots in single app use hash of `internal_bot_id => settings`
bots:
# just set the bot token
chat: TOKEN_1
# or add username to support commands with mentions (/help@ChatBot)
auction:
token: TOKEN_2
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]`.
Single bot can be accessed with `Telegram.bot` or `Telegram.bots[:default]`.
You can create clients manually with `Telegram::Bot::Client.new(token, username)`.
### Client
Client is instantiated with `Telegram::Bot::Client.new(token, username)`.
Username is optional and used only to parse commands with mentions.
There is `request(path_suffix, body)` method to perform any query.
And there are also shortcuts for available queries in underscored style
(`answer_inline_query` instead of `answerInlineQuery`).
All this methods just post given params to specific URL.
And there are shortcuts for all available requests in underscored style
(`answer_inline_query(params)` instead of `answerInlineQuery`).
```ruby
bot.request(:getMe) or bot.get_me
bot.request(:getupdates, offset: 1) or bot.get_updates(offset: 1)
bot.send_message chat_id: chat_id, text: 'Test'
bot.send_message(chat_id: chat_id, text: 'Test')
```
There is no magic, they just pass params as is and set `path_suffix`.
See [list of available methods](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/client/api_methods.txt)
fetched from [API docs](https://core.telegram.org/bots/api).
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.
Special `Telegram::Bot::Forbidden` is raised when bot can't post messages to the chat anymore.
#### Typed responses
By default client will return parsed json responses. You can enable
response typecasting to virtus models using `telegram-bot-types` gem:
response typecasting to virtus models using
[`telegram-bot-types`](https://github.com/telegram-bot-rb/telegram-bot-types) gem:
```ruby
# Add to your gemfile:
gem 'telegram-bot-types', '~> x.x.x'
@@ -99,47 +183,56 @@ bot.extend Telegram::Bot::Client::TypedResponse
bot.get_me.class # => Telegram::Bot::Types::User
```
Any API request error will raise `Telegram::Bot::Error` with description in its message.
Special `Telegram::Bot::StaleChat` is raised when bot can't post messages to the chat anymore.
### Controller
Controller makes it easy to keep bot's code readable.
It does nothing more than finding out action name for update and invoking it.
So there is almost no overhead comparing to large `switch`, while you
can represent actions as separate methods keeping source much more readable and supportable.
New instance of controller is instantiated for each update.
This way every update is processed in isolation from others.
Bot controllers like usual rails controllers provides features like callbacks,
`rescue_from` and instrumentation.
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
# use callbacks like in any other controllers
# use callbacks like in any other controller
around_action :with_locale
# Every update can have one of: message, inline_query, chosen_inline_result,
# callback_query.
# Define method with same name to respond to this updates.
# Every update has one of: message, inline_query, chosen_inline_result,
# callback_query, etc.
# Define method with the same name to handle this type of update.
def message(message)
# message can be also accessed via instance method
message == self.payload # true
# store_message(message['text'])
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)
# inline_query(query, offset)
# chosen_inline_result(result_id, query)
# 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.
# Be sure to use splat args and default values to not get errors when
# someone passed more or less arguments in the message.
#
# For some commands like /message or /123 method names should start with
# `on_` to avoid conflicts.
def start(data = nil, *)
# do_smth_with(data)
def start!(word = nil, *other_words)
# do_smth_with(word)
# full message object is also available via `payload` instance method:
# process_raw_message(payload['text'])
# 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!'
# There is `respond_with` helper to set `chat_id` from received message:
respond_with :message, text: response
# `reply_with` also sets `reply_to_message_id`:
reply_with :photo, photo: File.open('party.jpg')
end
@@ -160,10 +253,27 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end
```
#### Reply helpers
There are helpers for basic responses. They just set chat/message/query
identifiers from the 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. Here are these methods signatures:
```ruby
def respond_with(type, params); end
def reply_with(type, params); end
def answer_inline_query(results, params = {}); end
def answer_callback_query(text, params = {}); end
def edit_message(type, params = {}); end
def answer_pre_checkout_query(ok, params = {}); end
def answer_shipping_query(ok, params = {}); end
```
#### Optional typecasting
You can enable typecasting of `update` with `telegram-bot-types` by including
`Telegram::Bot::UpdatesPoller::TypedUpdate`:
`Telegram::Bot::UpdatesController::TypedUpdate`:
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
@@ -177,35 +287,56 @@ end
#### Session
There is support for sessions using `ActiveSupport::Cache` stores.
This API is very close to ActiveController's session API, but works different
under the hood. Cookies can not be used to store session id or
whole session (like CookieStore does). So it uses key-value store and `session_key`
method to build identifier from update.
Store can be one of numerous `ActiveSupport::Cache` stores.
While `:file_store` is suitable for development and single-server deployments
without heavy load, it doesn't scale well. Key-value databases with persistance
like Redis are more appropriate for production use.
```ruby
# configure store in env files:
# In rails app store can be configured in env files:
config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month}
# In other app it can be done for all controllers with:
Telegram::Bot::UpdatesController.session_store = :redis_store, {expires_in: 1.month}
# or for specific one:
OneOfUpdatesController.session_store = :redis_store, {expires_in: 1.month}
```
Default session id is made from bot's username and `(from || chat)['id']`.
It means that session will be the same for updates from user in every chat,
and different for every user in the same group chat.
To change this behavior you can override `session_key` method, or even
define [multiple sessions](https://github.com/telegram-bot-rb/telegram-bot/wiki/Multiple-session-objects)
in single controller. For details see `Session` module.
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::Session
# or just shortcut:
use_session!
# You can override global config
# You can override global config for this controller.
self.session_store = :file_store
def write(text = nil, *)
def write!(text = nil, *)
session[:text] = text
end
def read
def read!(*)
respond_with :message, text: session[:text]
end
private
# By default it uses bot's username and user's id as a session key.
# Chat's id is used only when `from` field is empty.
# Override `session_key` method to change this behavior.
# In this case session will persist for user only in specific chat.
# Same user in other chat will have different session.
def session_key
# In this case session will persist for user only in specific chat:
"#{bot.username}:#{chat['id']}:#{from['id']}"
"#{bot.username}:#{chat['id']}:#{from['id']}" if chat && from
end
end
```
@@ -219,115 +350,123 @@ it asks you for additional argument. There is `MessageContext` for this:
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::MessageContext
def rename(*)
def rename!(*)
# set context for the next message
save_context :rename
save_context :rename_from_message
respond_with :message, text: 'What name do you like?'
end
# register context handlers to handle this context
context_handler :rename do |*words|
def rename_from_message(*words)
update_name words[0]
respond_with :message, text: 'Renamed!'
end
# You can do it in other way:
def rename(name = nil, *)
# You can use same action name as context name:
def rename!(name = nil, *)
if name
update_name name
respond_with :message, text: 'Renamed!'
else
save_context :rename
save_context :rename!
respond_with :message, text: 'What name do you like?'
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 use
context_to_action!
# It'll use context value as action name for all contexts which miss handlers.
end
```
You can use `CallbackQueryContext` in the similar way to split `#callback_query` into
several specific methods. It doesn't require session support, and takes context from
data. If data has a prefix with colon like this `my_ctx:smth...` it'll call
`my_ctx_callback_query('smth...')` when there is such action method. Otherwise
it'll call `callback_query('my_ctx:smth...')` as usual.
#### Callback queries
#### Processesing updates
To process update run:
You can include `CallbackQueryContext` module to split `#callback_query` into
several methods. It doesn't require session support, and takes context from
data: if data has a prefix with colon like this `my_ctx:smth...` it invokes
`my_ctx_callback_query('smth...')` when such action method is defined. Otherwise
it invokes `callback_query('my_ctx:smth...')` as usual.
Callback queries without prefix stay untouched.
```ruby
ControllerClass.dispatch(bot, update)
# This one handles `set_value:%{something}`.
def set_value_callback_query(new_value = nil, *)
save_this(value)
answer_callback_query('Saved!')
end
# And this one is for `make_cool:%{something}`
def make_cool_callback_query(thing = nil, *)
do_it(thing)
answer_callback_query("#{thing} is cool now! Like a callback query context.")
end
```
There is also ability to run action without update:
### Routes in Rails app
There is `telegram_webhook` helper for rails app to define routes for webhooks.
It defines routes at `telegram/#{hash_of(bot.token)}` and connects bots with controller.
```ruby
# Most likely you'll want to pass :from and :chat
controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat)
controller.process(:help, *args)
```
### Routes
Use `telegram_webhooks` helper to add routes. It will create routes for bots
at "telegram/#{bot.token}" path.
```ruby
# 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
# Most off apps would require
telegram_webhook TelegramController
# which is same as
telegram_webhook TelegramController, :default
# Use different controllers for each bot:
telegram_webhooks bot => TelegramChatController,
other_bot => TelegramAuctionController
telegram_webhook TelegramChatController, :chat
telegram_webhook TelegramAuctionController, :auction
# telegram_webhooks creates named routes.
# Route name depends on `Telegram.bots`.
# When there is single bot it will use 'telegram_webhook'.
# When there are it will use bot's key in the `Telegram.bots` as prefix
# Defined route is named and its name depends on `Telegram.bots`.
# For single bot it will use 'telegram_webhook',
# for multiple bots it uses bot's key in the `Telegram.bots` as prefix
# (eg. `chat_telegram_webhook`).
# You can override this options or specify others:
telegram_webhooks TelegramController, as: :my_webhook
telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
other_bot => TelegramAuctionController,
admin_chat: TelegramAdminChatController
# You can override this with `as` option:
telegram_webhook TelegramController, as: :custom_telegram_webhook
```
For Rack applications you can also use `Telegram::Bot::Middleware` or just
call `.dispatch(bot, update)` on controller.
#### Processing updates
To process update with controller call `.dispatch(bot, update)` on it.
There are several options to run it automatically:
- Use webhooks with routes helper (described above).
- Use `Telegram::Bot::Middleware` with rack ([example in wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki/Not-rails-application)).
- Use poller (described in the next section).
When controller runs in webhook mode `Telegram::Bot::Middleware` passes
`ActionDispatch::Request` object to the `.dispatch` method.
It's available via `#webhook_request` controller method.
To run action without update (ex., send notifications from jobs),
you can call `#process` directly. In this case controller can be initialized
with `:from` and/or `:chat` options instead of `update` object:
```ruby
controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat)
controller.process(:welcome, *args)
```
### Development & Debugging
Use `rake telegram:bot:poller` to run poller. It'll automatically load
changes without restart in development env. Optionally specify bot to run poller for
with `BOT` envvar (`BOT=chat`).
Use `rake telegram:bot:poller` to run poller in rails app. It automatically loads
changes without restart in development env.
Optionally pass bot id in `BOT` envvar (`BOT=chat`) to specify bot to run poller for.
This task will not work if you don't use `telegram_webhooks`.
You can run poller manually with
`Telegram::Bot::UpdatesPoller.start(bot, controller_class)`.
This task requires `telegram_webhook` helper to be used as it connects bots with controller.
To run poller in other cases use:
```ruby
Telegram::Bot::UpdatesPoller.start(bot, controller_class)
```
### Testing
There is `Telegram::Bot::ClientStub` class to stub client for tests.
Instead of performing API requests it stores them in `requests` hash.
There is a `Telegram::Bot::ClientStub` class to stub client for tests.
Instead of performing API requests it stores them in a `requests` hash.
To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before
initializing clients. Most likely you'll want something like this:
initializing clients. Here is a template for RSpec:
```ruby
# environments/test.rb
# Make sure to run it before defining routes or storing bot to some place in app!
# Make sure to run it before defining routes or accessing any bot in the app!
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!
@@ -335,90 +474,104 @@ Telegram::Bot::ClientStub.stub_all!
RSpec.configure do |config|
# ...
config.after { Telegram.bot.reset }
# or for multiple bots:
config.after { Telegram.bots.each_value(&:reset) }
# ...
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
# spec/requests/telegram_webhooks_spec.rb
require 'telegram/bot/rspec/integration'
require 'telegram/bot/rspec/integration/rails'
RSpec.describe TelegramWebhooksController, :telegram_bot do
# for old rspec add:
# include_context 'telegram/bot/integration'
RSpec.describe TelegramWebhooksController, telegram_bot: :rails do
# for old RSpec:
# 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 } }
# Using built in matcher for `respond_to`:
it { should respond_with_message 'Hi there!' }
end
end
# For controller specs use
# There is context for callback queries with related matchers,
# use :callback_query tag to include it.
describe '#hey_callback_query', :callback_query do
let(:data) { "hey:#{name}" }
let(:name) { 'Joe' }
it { should answer_callback_query('Hey Joe') }
it { should edit_current_message :text, text: 'Done' }
end
end
```
There is a context for testing bot controller in the way similar to Rails controller tests.
It's supposed to be a low-level alternative for integration tests. Among the differences is
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 add:
# for old RSpec:
# include_context 'telegram/bot/updates_controller'
# Same helpers and matchers like dispatch_command, answer_callback_query are available here.
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'))
```
See sample app for more examples.
### Deploying
### Deployment
Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots.
Certificate can be specified with `CERT=path/to/cert`.
### 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.
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)
for details.
### Async mode
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:
- 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.
- ???
@@ -426,7 +579,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 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
[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).
@@ -435,19 +588,18 @@ If you want async mode, but don't want to setup queue, know that Rails 5 are shi
with Async adapter by default, and there is
[Sucker Punch](https://github.com/brandonhilkert/sucker_punch) for Rails 4.
Be aware of some limitations:
- Client will not return API response.
- Sending files is not available in async mode [now],
because them can not be serialized.
To disable async mode for the block of code use `bot.async(false) { bot.send_photo }`.
Yes, it's threadsafe too.
#### Limitations
- Client will not return API response.
- Sending files is not available in async mode, because they can not be serialized.
## Development
After checking out the repo, run `bin/setup` to install dependencies.
Then, run `rake spec` to run the tests.
After checking out the repo, run `bin/setup` to install dependencies and git hooks.
Then, run `appraisal rake spec` to run the tests.
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`.
@@ -455,15 +607,7 @@ To release a new version, update the version number in `version.rb`,
and then run `bundle exec rake release`, which will create a git tag for the version,
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
### Different Rails versions
To setup development for specific major Rails version use:
```
RAILS=5 bundle install
# or
RAILS=5 bundle update
```
Use `bin/fetch-telegram-methods` to update API methods list from Telegram website.
## Contributing

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

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

@@ -1,7 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'telegram/bot'
require 'pry'
Pry.start
require 'irb'
require 'irb/completion'
IRB.setup(nil)
IRB::Irb.new.run(IRB.conf)

43
bin/fetch-telegram-methods Исполняемый файл
Просмотреть файл

@@ -0,0 +1,43 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# Fetch list of methods from Telegram docs.
# Use it to update client.rb.
require 'net/http'
require 'nokogiri'
DOCS_URL = 'https://core.telegram.org/bots/api'
page_html = Net::HTTP.get(URI(DOCS_URL))
doc = Nokogiri::HTML(page_html)
# Select `h4`s, but use `h3`s to group them.
headers = doc.css('h3, h4').
chunk_while { |_, x| x.name == 'h4' }.
map { |g| g.select { |x| x.name == 'h4' } }.
map { |g| g.map(&:text) }
# Method starts with lowercase and does not have spaces.
NOT_METHOD_REGEXP = /(\A[^a-z])|\s/.freeze
# Filter method names.
method_list = headers.
map { |g| g.grep_v(NOT_METHOD_REGEXP) }.
reject(&:empty?)
api_version = doc.text.match(/^(?:Introducing )?(Bot API ([\d\.]+))\.?$/)
result = ['# Generated with bin/fetch-telegram-methods']
result << "# #{api_version[1]}" if api_version
result << ''
result << method_list.map { |g| g.join("\n") }.join("\n\n")
result << ''
result_txt = result.join("\n")
puts result_txt
API_METHODS_FILE = File.expand_path('../lib/telegram/bot/client/api_methods.txt', __dir__).freeze
File.write(API_METHODS_FILE, result_txt)
puts '', "Updated #{API_METHODS_FILE}"

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

@@ -1,6 +1,7 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
root = File.expand_path('../../', __FILE__)
root = File.expand_path('..', __dir__)
hooks_dir = "#{root}/bin/git-hooks"
`ls -1 #{hooks_dir}`.each_line.map(&:strip).each do |file|

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

@@ -3,6 +3,7 @@ set -euo pipefail
IFS=$'\n\t'
bundle install
appraisal install
bin/install_git_hooks
# Do any other automated setup that you need to do here

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 5.1.0"
gem "railties", "~> 5.1.0"
group :development do
gem "appraisal", "~> 2.2"
gem "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 5.2.0"
gem "railties", "~> 5.2.0"
group :development do
gem "appraisal", "~> 2.2"
gem "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 6.0.0"
gem "railties", "~> 6.0.0"
group :development do
gem "appraisal", "~> 2.2"
gem "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.1"
gem "coveralls", "~> 0.8.23", require: false
end
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 "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.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 "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -0,0 +1,21 @@
# This file was generated by Appraisal
source "https://rubygems.org"
gem "actionpack", "~> 7.1.0"
gem "railties", "~> 7.1.0"
group :development do
gem "appraisal", "~> 2.2"
gem "debug", "~> 1.9.2"
gem "sdoc", "~> 2.0.3"
gem "telegram-bot-types", "~> 0.6.3"
gem "rspec", "~> 3.12.0"
gem "rspec-its", "~> 1.3.0"
gem "rspec-rails", "~> 4.0.2"
gem "rubocop", "~> 1.59.0"
gem "rubocop-rails", "~> 2.23.1"
gem "coveralls", "~> 0.8.23", require: false
end
gemspec path: "../"

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
namespace :telegram do
namespace :bot do
desc 'Run poller. It broadcasts Rails.logger to STDOUT in dev like `rails s` do. ' \
@@ -6,23 +8,34 @@ namespace :telegram do
ENV['BOT_POLLER_MODE'] = 'true'
Rake::Task['environment'].invoke
if ENV.fetch('LOG_TO_STDOUT') { Rails.env.development? }.present?
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
console = ActiveSupport::Logger.new($stderr)
if Rails.logger.respond_to?(:broadcast_to)
Rails.logger.broadcast_to(console)
else
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
end
Telegram::Bot::UpdatesPoller.start(ENV['BOT'].try!(:to_sym) || :default)
Telegram::Bot::UpdatesPoller.start(ENV['BOT']&.to_sym || :default)
end
desc 'Set webhook urls for all bots'
task set_webhook: :environment do
routes = Rails.application.routes.url_helpers
cert_file = ENV['CERT']
cert = File.open(cert_file) if cert_file
Telegram.bots.each do |key, bot|
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
url = routes.send("#{route_name}_url")
puts "Setting webhook for #{key}..."
bot.set_webhook(url: url, certificate: cert)
end
Telegram::Bot::Tasks.set_webhook
end
desc 'Delete webhooks for all or specific BOT'
task :delete_webhook do
Telegram::Bot::Tasks.delete_webhook
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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'telegram/bot/config_methods'
module Telegram
@@ -5,38 +7,23 @@ module Telegram
module Bot
class Error < StandardError; end
# Raised for valid telegram response with 403 status code.
class Forbidden < Error; end
# Raised for valid telegram response with 404 status code.
class NotFound < Error; end
# Error class for events when chat is not available anymore for bot.
# While Telegram has same error codes for different messages and there is no
# official docs for this error codes it uses `description` to
# check response.
class StaleChat < Error
DESCRIPTIONS = [
'Bot was blocked',
'bot was kicked',
"can't write to",
'group chat is deactivated',
].freeze
class << self
def match_response?(response)
description = response['description'].to_s
DESCRIPTIONS.any? { |x| description[x] }
end
end
end
autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client'
autoload :ClientStub, 'telegram/bot/client_stub'
autoload :DebugClient, 'telegram/bot/debug_client'
autoload :Initializers, 'telegram/bot/initializers'
autoload :Middleware, 'telegram/bot/middleware'
autoload :RSpec, 'telegram/bot/rspec'
autoload :Tasks, 'telegram/bot/tasks'
autoload :UpdatesController, 'telegram/bot/updates_controller'
autoload :UpdatesPoller, 'telegram/bot/updates_poller'
autoload :VERSION, 'telegram/bot/version'
end
end

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

@@ -1,28 +1,25 @@
# frozen_string_literal: true
module Telegram
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
# additional configuration. However you may want to enable async requests
# by default with `async: true` in `secrets.yml`. Botan client doesn't inherit
# async setting from client and must be configured separately.
# by default with `async: true` in `secrets.yml`.
#
# telegram:
# bots:
# chat_async:
# token: secret
# 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
# initialize client with `id` kwarg and make sure the client is
# accessible via `Teletgram.bots[id]` in job worker. Or just use
# `Telegram.bots_config=` for configuration.
#
# Being in async mode `#request` enqueues job instead to perform
# http request instead of performing it directly.
# Being in async mode `#request` enqueues job to perform
# http request instead of performing it immediately.
# Async behavior is controlled with `#async=` writer
# and can be enabled/disabled for the block with `#async`:
#
@@ -34,7 +31,7 @@ module Telegram
# while `#async(val, &block)` is thread-safe.
#
# 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
# other job provider by defining a class with `.perform_later(bot_id, *args)`
# method. See Async::Job for implemetation.
@@ -133,7 +130,7 @@ module Telegram
#
# If no block is given returns previously set value or the global one,
# set by #async=.
def async(val = true)
def async(val = true) # rubocop:disable Style/OptionalBooleanParameter
thread_key = object_id
thread_store = Async.thread_store
return thread_store.fetch(thread_key) { @async } unless block_given?
@@ -142,7 +139,7 @@ module Telegram
thread_store[thread_key] = self.class.prepare_async_val(val)
yield
ensure
if MISSING_VALUE == old_val
if old_val == MISSING_VALUE
thread_store.delete(thread_key)
else
thread_store[thread_key] = old_val

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

@@ -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 300 > status
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,20 +1,34 @@
require 'json'
require 'httpclient'
require 'active_support/core_ext/string/inflections'
# frozen_string_literal: true
require 'active_support/core_ext/hash/keys'
require 'httpclient'
module Telegram
module Bot
class Client
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
SERVER = 'https://api.telegram.org'
URL_TEMPLATE = '%<server>s/bot%<token>s/'
autoload :RequestBodyFormatter, 'telegram/bot/client/request_body_formatter'
autoload :TypedResponse, 'telegram/bot/client/typed_response'
extend Initializers
prepend Async
prepend Botan::ClientHelpers
include DebugClient
require 'telegram/bot/client/api_helper'
include ApiHelper
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)
Telegram.bots[id]
end
@@ -24,84 +38,36 @@ module Telegram
prepend TypedResponse
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
def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(RequestBodyFormatter.format(body, action))]
end
def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(prepare_body(body))]
def error_for_response(response)
result = JSON.parse(response.body) rescue nil # rubocop:disable Style/RescueModifier
return Error.new(response.reason) unless result
message = result['description'] || '-'
# This errors are raised only for valid responses from Telegram
case response.status
when 403 then Forbidden.new(message)
when 404 then NotFound.new(message)
else Error.new("#{response.reason}: #{message}")
end
end
end
attr_reader :client, :token, :username, :base_uri
def initialize(token = nil, username = nil, **options)
def initialize(token = nil, username = nil, server: SERVER, **options)
@client = HTTPClient.new
@token = token || options[:token]
@username = username || options[:username]
@base_uri = format URL_TEMPLATE, self.token
@base_uri = format(URL_TEMPLATE, server: server, token: self.token)
end
def request(action, body = {}) # rubocop:disable PerceivedComplexity
res = http_request("#{base_uri}#{action}", self.class.prepare_body(body))
status = res.status
return JSON.parse(res.body) if 300 > status
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = "#{res.reason}: #{result && result['description'] || '-'}"
if result
# NotFound is raised only for valid responses from Telegram
raise NotFound, err_msg if 404 == status
raise StaleChat, err_msg if StaleChat.match_response?(result)
end
raise Error, err_msg
end
# Splited to the sections similar to API docs.
%w(
deleteWebhook
getUpdates
getWebhookInfo
setWebhook
answerCallbackQuery
forwardMessage
getChat
getChatAdministrators
getChatMember
getChatMembersCount
getFile
getMe
getUserProfilePhotos
kickChatMember
leaveChat
sendAudio
sendChatAction
sendContact
sendDocument
sendLocation
sendMessage
sendPhoto
sendSticker
sendVenue
sendVideo
sendVoice
unbanChatMember
editMessageCaption
editMessageReplyMarkup
editMessageText
answerInlineQuery
getGameHighScores
sendGame
setGameScore
).each do |method|
define_method(method.underscore) { |*args| request(method, *args) }
def request(action, body = {})
response = http_request("#{base_uri}#{action}", RequestBodyFormatter.format(body, action))
raise self.class.error_for_response(response) if response.status >= 300
JSON.parse(response.body)
end
# Endpoint for low-level request. For easy host highjacking & instrumentation.

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

@@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'active_support/core_ext/string/inflections'
module Telegram
module Bot
class Client
module ApiHelper
METHODS_LIST_FILE = File.expand_path('api_methods.txt', __dir__)
class << self
def methods_list(file = METHODS_LIST_FILE)
File.read(file).lines.
map(&:strip).
reject { |x| x.empty? || x.start_with?('#') }
end
# Defines method with underscored name to post to specific endpoint:
#
# define_method :getMe
# # defines #get_me
def define_helpers(*list)
list.map(&:to_s).each do |method|
define_method(method.underscore) { |*args| request(method, *args) }
end
end
end
define_helpers(*methods_list)
end
end
end
end

131
lib/telegram/bot/client/api_methods.txt Обычный файл
Просмотреть файл

@@ -0,0 +1,131 @@
# Generated with bin/fetch-telegram-methods
# Bot API 7.3
getUpdates
setWebhook
deleteWebhook
getWebhookInfo
getMe
logOut
close
sendMessage
forwardMessage
forwardMessages
copyMessage
copyMessages
sendPhoto
sendAudio
sendDocument
sendVideo
sendAnimation
sendVoice
sendVideoNote
sendMediaGroup
sendLocation
sendVenue
sendContact
sendPoll
sendDice
sendChatAction
setMessageReaction
getUserProfilePhotos
getFile
banChatMember
unbanChatMember
restrictChatMember
promoteChatMember
setChatAdministratorCustomTitle
banChatSenderChat
unbanChatSenderChat
setChatPermissions
exportChatInviteLink
createChatInviteLink
editChatInviteLink
revokeChatInviteLink
approveChatJoinRequest
declineChatJoinRequest
setChatPhoto
deleteChatPhoto
setChatTitle
setChatDescription
pinChatMessage
unpinChatMessage
unpinAllChatMessages
leaveChat
getChat
getChatAdministrators
getChatMemberCount
getChatMember
setChatStickerSet
deleteChatStickerSet
getForumTopicIconStickers
createForumTopic
editForumTopic
closeForumTopic
reopenForumTopic
deleteForumTopic
unpinAllForumTopicMessages
editGeneralForumTopic
closeGeneralForumTopic
reopenGeneralForumTopic
hideGeneralForumTopic
unhideGeneralForumTopic
unpinAllGeneralForumTopicMessages
answerCallbackQuery
getUserChatBoosts
getBusinessConnection
setMyCommands
deleteMyCommands
getMyCommands
setMyName
getMyName
setMyDescription
getMyDescription
setMyShortDescription
getMyShortDescription
setChatMenuButton
getChatMenuButton
setMyDefaultAdministratorRights
getMyDefaultAdministratorRights
editMessageText
editMessageCaption
editMessageMedia
editMessageLiveLocation
stopMessageLiveLocation
editMessageReplyMarkup
stopPoll
deleteMessage
deleteMessages
sendSticker
getStickerSet
getCustomEmojiStickers
uploadStickerFile
createNewStickerSet
addStickerToSet
setStickerPositionInSet
deleteStickerFromSet
replaceStickerInSet
setStickerEmojiList
setStickerKeywords
setStickerMaskPosition
setStickerSetTitle
setStickerSetThumbnail
setCustomEmojiStickerSetThumbnail
deleteStickerSet
answerInlineQuery
answerWebAppQuery
sendInvoice
createInvoiceLink
answerShippingQuery
answerPreCheckoutQuery
setPassportDataErrors
sendGame
setGameScore
getGameHighScores

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

@@ -0,0 +1,63 @@
# frozen_string_literal: true
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

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

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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Telegram
module Bot
# Stubbed client for tests. Saves all requests into #requests hash.
@@ -5,11 +7,11 @@ module Telegram
attr_reader :requests
module StubbedConstructor
def new(*args)
def new(*args, **kwargs)
if self == ClientStub || !ClientStub.stub_all?
super
else
ClientStub.new(*args)
ClientStub.new(*args, **kwargs)
end
end
end
@@ -17,7 +19,7 @@ module Telegram
class << self
# Any call to Client.new will return ClientStub instance when `enabled` is true.
# Can be used with a block.
def stub_all!(enabled = true)
def stub_all!(enabled = true) # rubocop:disable Style/OptionalBooleanParameter
Client.extend(StubbedConstructor) unless Client < StubbedConstructor
return @_stub_all = enabled unless block_given?
begin
@@ -34,7 +36,8 @@ module Telegram
end
end
def initialize(token = nil, username = nil, **options)
def initialize(token = nil, username = nil, **options) # rubocop:disable Lint/MissingSuper
@token = token || options[:token]
@username = username || options[:username] || token
reset
end

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

@@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/hash/indifferent_access'
module Telegram
module Bot
@@ -33,24 +35,27 @@ module Telegram
# Default bot.
def bot
@bot ||= bots[:default]
end
# Hash of botan clients made from #bots.
def botans
@botans ||= bots.transform_values(&:botan)
@bot ||= bots.fetch(:default) do
raise 'Default bot is not configured.' \
' Add :default to bots_config' \
' or use telegram.bot/telegram.bots.default section in secrets.yml.'
end
end
# Returns config for .bots method. By default uses `telegram['bots']` section
# from `secrets.yml` merging `telegram['bot']` at `:default` key.
#
# Can be overwritten with .bots_config=
def bots_config
def bots_config # rubocop:disable Metrics/PerceivedComplexity
@bots_config ||=
if defined?(Rails.application)
telegram_config = Rails.application.secrets[:telegram] || {}
(telegram_config['bots'] || {}).symbolize_keys.tap do |config|
default = telegram_config['bot']
app = Rails.application
store = app.credentials[:telegram] if app.respond_to?(:credentials)
store ||= app.secrets[:telegram] if app.respond_to?(:secrets)
store ||= {}
store = store.with_indifferent_access
store.fetch(:bots, {}).symbolize_keys.tap do |config|
default = store[:bot]
config[:default] = default if default
end
else
@@ -63,7 +68,6 @@ module Telegram
@bots = nil
@bot = nil
@bots_config = nil
@botans = nil
end
end
end

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

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Telegram
module Bot
module DebugClient
def debug!(dev = STDOUT)
def debug!(dev = $stdout)
if block_given?
begin
old_dev = client.debug_dev

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

@@ -1,19 +0,0 @@
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,9 @@
# frozen_string_literal: true
require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/json'
require 'action_dispatch/http/mime_type'
require 'action_dispatch/http/request'
require 'action_dispatch'
module Telegram
module Bot
@@ -17,12 +18,12 @@ module Telegram
def call(env)
request = ActionDispatch::Request.new(env)
update = request.request_parameters
controller.dispatch(bot, update)
controller.dispatch(bot, update, request)
[200, {}, ['']]
end
def inspect
"#<#{self.class.name}(#{controller.try!(:name)})>"
"#<#{self.class.name}(#{controller&.name})>"
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'telegram/bot/routes_helper'
module Telegram
@@ -10,7 +12,7 @@ module Telegram
end
config.before_initialize do
::ActionDispatch::Routing::Mapper.send(:include, RoutesHelper)
::ActionDispatch::Routing::Mapper.include RoutesHelper
end
initializer 'telegram.bot.updates_controller.set_config' do |app|
@@ -18,7 +20,7 @@ module Telegram
ActiveSupport.on_load('telegram.bot.updates_controller') do
self.logger = options.logger || Rails.logger
self.session_store = options.session_store || Rails.cache
self.session_store = options.session_store if options.session_store
end
end

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

@@ -1,3 +1,8 @@
# frozen_string_literal: true
require 'base64'
require 'openssl'
require 'telegram/bot'
require 'active_support/core_ext/array/wrap'
@@ -17,50 +22,33 @@ module Telegram
end || 'telegram_webhook'
end
# Replaces colon with underscore so rails don't treat it as
# route parameter.
def escape_token(token)
token && token.tr(':', '_')
def token_hash(token)
Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.digest(token), padding: false)
end
end
# # Create routes for all Telegram.bots to use same controller:
# telegram_webhooks TelegramController
# Define route which processes requests using given controller and bot.
#
# # Or pass custom bots usin any of supported config options:
# telegram_webhooks TelegramController,
# bot,
# {token: token, username: username},
# other_bot_token
# telegram_webhook TelegramController, bot
#
# # Use different controllers for each bot:
# telegram_webhooks bot => TelegramChatController,
# other_bot => TelegramAuctionController
# telegram_webhook TelegramController
# # same as:
# telegram_webhook TelegramController, :default
#
# # telegram_webhooks creates named routes. See
# # RoutesHelper.route_name_for_bot for more info.
# # You can override this options or specify others:
# telegram_webhooks TelegramController, as: :my_webhook
# telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
# other_bot => TelegramAuctionController,
# admin_chat: TelegramAdminChatController
# # pass additional options
# telegram_webhook TelegramController, :default, as: :custom_route_name
#
def telegram_webhooks(controllers, bots = nil, **options)
unless controllers.is_a?(Hash)
bots = bots ? Array.wrap(bots) : Telegram.bots.values
controllers = Hash[bots.map { |x| [x, controllers] }]
end
controllers.each do |bot, controller|
bot = Client.wrap(bot)
controller, bot_options = controller if controller.is_a?(Array)
params = {
to: Middleware.new(bot, controller),
as: RoutesHelper.route_name_for_bot(bot),
format: false,
}.merge!(options).merge!(bot_options || {})
post("telegram/#{RoutesHelper.escape_token bot.token}", params)
UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode?
end
# # 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)
params = {
to: Middleware.new(bot, controller),
as: RoutesHelper.route_name_for_bot(bot),
format: false,
}.merge!(options)
post(path || "telegram/#{bot.token && RoutesHelper.token_hash(bot.token)}", params)
UpdatesPoller.add(bot, controller) if Telegram.bot_poller_mode?
end
end
end

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

@@ -1,7 +1,18 @@
# frozen_string_literal: true
module Telegram
module Bot
module RSpec
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

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

@@ -0,0 +1,41 @@
# frozen_string_literal: true
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,3 +1,5 @@
# frozen_string_literal: true
module Telegram
module Bot
module RSpec
@@ -61,14 +63,14 @@ module Telegram
attr_reader :performed_requests, :description
def initialize(bot, action, description: nil)
def initialize(bot, action, description: nil) # rubocop:disable Lint/MissingSuper
@bot = bot
@action = action
@description = description || "make #{action} telegram request"
exactly(1)
end
def matches?(proc) # rubocop:disable AbcSize
def matches?(proc) # rubocop:disable Metrics/AbcSize
raise ArgumentError, 'matcher only supports block expectations' unless proc.is_a?(Proc)
original_requests_count = bot.requests[action].count
proc.call
@@ -115,15 +117,15 @@ module Telegram
:arg_list_matcher, :matching_requests_count
def base_message
"make #{expectation_type.to_s.tr('_', ' ')} #{expected_number} " \
"#{bot.inspect}.#{action} requests,".tap do |msg|
msg << " with #{arg_list_matcher}," if arg_list_matcher
msg << " but made #{matching_requests_count}"
if performed_requests
actual_args = performed_requests.map(&:inspect).join(', ')
msg << ", and #{performed_requests.count} with #{actual_args}"
end
msg = "make #{expectation_type.to_s.tr('_', ' ')} #{expected_number} " \
"#{bot.inspect}.#{action} requests,"
msg += " with #{arg_list_matcher}," if arg_list_matcher
msg += " but made #{matching_requests_count}"
if performed_requests
actual_args = performed_requests.map(&:inspect).join(', ')
msg += ", and #{performed_requests.count} with #{actual_args}"
end
msg
end
end

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

@@ -1,49 +0,0 @@
RSpec.shared_context 'telegram/bot/integration' do
let(:bot) { Telegram.bot }
let(:from_id) { 123 }
let(:chat_id) { 456 }
let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} }
let(:controller_path) do
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
Rails.application.routes.url_helpers.public_send("#{route_name}_path")
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.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)
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.configure do |config|
if config.respond_to?(:include_context)
config.include_context 'telegram/bot/integration', :telegram_bot
end
end

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

@@ -0,0 +1,16 @@
# frozen_string_literal: true
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

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

@@ -0,0 +1,26 @@
# frozen_string_literal: true
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

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

@@ -0,0 +1,30 @@
# frozen_string_literal: true
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,16 @@
# frozen_string_literal: true
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&.clear if clear_session? }
end

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

@@ -0,0 +1,28 @@
# frozen_string_literal: true
# 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.fetch('CERT', nil)
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.fetch('IP_ADDRESS', nil),
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']&.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']&.downcase == 'true'
end
end
end
end

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

@@ -1,4 +1,7 @@
# frozen_string_literal: true
require 'abstract_controller'
require 'active_support/core_ext/string/inflections'
require 'active_support/callbacks'
require 'active_support/version'
@@ -6,13 +9,13 @@ module Telegram
module Bot
# Base class to create update processors. With callbacks, session and helpers.
#
# Define public methods for each command and they will be called when
# update has this command. Message is automatically parsed and
# words are passed as method arguments. Be sure to use default values and
# Public methods ending with `!` handle messages with commands.
# Message text is automatically parsed into method arguments.
# Be sure to use default values and
# splat arguments in every action method to not get errors, when user
# sends command without necessary args / with extra args.
#
# def start(token = nil, *)
# def start!(token = nil, *)
# if token
# # ...
# else
@@ -20,25 +23,21 @@ module Telegram
# end
# end
#
# def help(*)
# def help!(*)
# respond_with :message, text:
# end
#
# To process plain text messages (without commands) or other updates just
# define public method with name of payload type. They will receive payload
# as an argument.
# define public method with name of payload type.
# By default they receive payload as an argument, but some of them are called
# with more usefuk args:
#
# def message(message)
# respond_with :message, text: "Echo: #{message['text']}"
# end
#
# def inline_query(query)
# answer_inline_query results_for_query(query), is_personal: true
# end
#
# # To process conflicting commands (`/message args`) just use `on_` prefix:
# def on_message(*args)
# # ...
# def inline_query(query, offset)
# answer_inline_query results_for_query(query, offset), is_personal: true
# end
#
# To process update run:
@@ -50,45 +49,70 @@ module Telegram
# ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
# process(:help, *args)
#
class UpdatesController < AbstractController::Base
class UpdatesController < AbstractController::Base # rubocop:disable Metrics/ClassLength
abstract!
require 'telegram/bot/updates_controller/session'
require 'telegram/bot/updates_controller/log_subscriber'
require 'telegram/bot/updates_controller/instrumentation'
require 'telegram/bot/updates_controller/reply_helpers'
autoload :CallbackQueryContext, 'telegram/bot/updates_controller/callback_query_context'
autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
%w[
Commands
Instrumentation
LogSubscriber
ReplyHelpers
Rescue
Session
Translation
].each { |name| require "telegram/bot/updates_controller/#{name.underscore}" }
%w[
CallbackQueryContext
MessageContext
TypedUpdate
].each { |mod| autoload mod, "telegram/bot/updates_controller/#{mod.underscore}" }
include AbstractController::Callbacks
# Redefine callbacks with default terminator.
if ActiveSupport::VERSION::MAJOR >= 5
define_callbacks :process_action,
skip_after_callbacks_if_terminated: true
define_callbacks :process_action,
skip_after_callbacks_if_terminated: true
else
define_callbacks :process_action,
terminator: ->(_, result) { result == false },
skip_after_callbacks_if_terminated: true
define_callbacks :process_action,
terminator: ->(_, result) { result == false },
skip_after_callbacks_if_terminated: true
end
include AbstractController::Translation
include Commands
include Rescue
include ReplyHelpers
prepend Instrumentation
include Translation
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
include Instrumentation
extend Session::ConfigMethods
autoload :TypedUpdate, 'telegram/bot/updates_controller/typed_update'
PAYLOAD_TYPES = %w(
PAYLOAD_TYPES = Set.new(%w[
message
edited_message
channel_post
edited_channel_post
business_connection
business_message
edited_business_message
deleted_business_messages
message_reaction
message_reaction_count
inline_query
chosen_inline_result
callback_query
).freeze
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
shipping_query
pre_checkout_query
poll
poll_answer
my_chat_member
chat_member
chat_join_request
chat_boost
removed_chat_boost
].freeze)
class << self
# Initialize controller and process update.
@@ -96,114 +120,144 @@ module Telegram
new(*args).dispatch
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
def payload_from_update(update)
case update
when nil then nil
when Hash
# faster lookup for the case when telegram-bot-types is not used
update.find do |type, item|
return [item, type] if PAYLOAD_TYPES.include?(type)
end
else
payload_from_typed_update(update)
end
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
return if match[3] && username != true && match[3] != username
[match[1], text.split.drop(1)]
private
def payload_from_typed_update(update)
PAYLOAD_TYPES.find do |type|
begin
item = update[type]
return [item, type] if item
rescue Exception # rubocop:disable Lint/RescueException
# dry-rb raises exception if field is not defined in schema
end
end
end
end
attr_internal_reader :update, :bot, :payload, :payload_type, :is_command
alias_method :command?, :is_command
attr_internal_reader :bot, :payload, :payload_type, :update, :webhook_request
delegate :username, to: :bot, prefix: true, allow_nil: true
# Second argument can be either update object with hash access & string
# `update` can be either update object with hash access & string
# keys or Hash with `:from` or `:chat` to override this values and assume
# that update is nil.
def initialize(bot = nil, update = nil)
# ActionDispatch::Request object is passed in `webhook_request` when bot running
# in webhook mode.
def initialize(bot = nil, update = nil, webhook_request = nil) # rubocop:disable Lint/MissingSuper
if update.is_a?(Hash) && (update.key?(:from) || update.key?(:chat))
options = update
update = nil
end
@_update = update
@_bot = bot
@_chat, @_from = options && options.values_at(:chat, :from)
payload_data = nil
update && PAYLOAD_TYPES.find do |type|
item = update[type]
payload_data = [item, type] if item
end
@_payload, @_payload_type = payload_data
@_update = update
@_chat, @_from = options&.values_at(:chat, :from)
@_payload, @_payload_type = self.class.payload_from_update(update)
@_webhook_request = webhook_request
end
# Accessor to `'chat'` field of payload. Also tries `'chat'` in `'message'`
# when there is no such field in payload.
#
# Can be overriden with `chat` option for #initialize.
def chat
@_chat ||= payload.try! { |x| x['chat'] || x['message'] && x['message']['chat'] }
def chat # rubocop:disable Metrics/PerceivedComplexity
@_chat ||= # rubocop:disable Naming/MemoizedInstanceVariableName
if payload
if payload.is_a?(Hash)
payload['chat'] || (payload['message'] && payload['message']['chat'])
else
payload.try(:chat) || payload.try(:message)&.chat
end
end
end
# Accessor to `'from'` field of payload. Can be overriden with `from` option
# for #initialize.
def from
@_from ||= payload && payload['from']
@_from ||= # rubocop:disable Naming/MemoizedInstanceVariableName
payload.is_a?(Hash) ? payload['from'] : payload.try(:from)
end
# Processes current update.
def dispatch
@_is_command, action, args = action_for_payload
action, args = action_for_payload
process(action, *args)
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.
# Uses `action_for_#{payload_type}` methods.
# If this method doesn't return anything
# it uses fallback with action same as payload type.
# Returns array `[is_command?, action, args]`.
# Returns array `[action, args]`.
def action_for_payload
if payload_type
send("action_for_#{payload_type}") || [false, payload_type, [payload]]
send("action_for_#{payload_type}") || action_for_default_payload
else
[false, :unsupported_payload_type, []]
[:unsupported_payload_type, []]
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).
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
# It doesn't extract commands from edited messages. Just process
# them as usual ones.
def action_for_edited_message
def action_for_default_payload
[payload_type, [payload]]
end
def action_for_inline_query
[false, payload_type, [payload['query'], payload['offset']]]
[payload_type, [payload['query'], payload['offset']]]
end
def action_for_chosen_inline_result
[false, payload_type, [payload['result_id'], payload['query']]]
[payload_type, [payload['result_id'], payload['query']]]
end
def action_for_callback_query
[false, payload_type, [payload['data']]]
[payload_type, [payload['data']]]
end
# Silently ignore unsupported messages.
# Params are `action, *args`.
def action_missing(*)
def action_for_poll_answer
[payload_type, [payload['poll_id'], payload['option_ids']]]
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}" }
nil
end
PAYLOAD_TYPES.each do |type|
method = :"action_for_#{type}"
alias_method method, :action_for_default_payload unless instance_methods.include?(method)
end
ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self)

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
@@ -15,11 +17,14 @@ module Telegram
# because `data` param is controlled by client.
def action_for_callback_query
context, new_data = context_from_callback_query
# binding.pry
if context
action_name = "#{context}_callback_query"
[false, action_name, [new_data]] if action_method?(action_name)
end || super
if action_method?(action_name)
action_options = {type: :callback_query_context, context: context}
return [[action_name, action_options], [new_data]]
end
end
super
end
def context_from_callback_query

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

@@ -0,0 +1,46 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
# Support for parsing commands
module Commands
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i.freeze
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

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

@@ -1,15 +1,18 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
# Most methods are taken from ActionController::Instrumentation,
# some are slightly modified.
module Instrumentation
class << self
def prepended(base)
base.send :config_accessor, :logger
base.extend ClassMethods
end
extend ActiveSupport::Concern
included do
config_accessor :logger
end
class << self
def instrument(action, *args, &block)
ActiveSupport::Notifications.instrument(
"#{action}.updates_controller.bot.telegram",
@@ -22,8 +25,8 @@ module Telegram
def process_action(*args)
raw_payload = {
controller: self.class.name,
action: action_name,
update: update,
action: action_name,
update: update,
}
Instrumentation.instrument(:start_processing, raw_payload.dup)
Instrumentation.instrument(:process_action, raw_payload) do |payload|
@@ -39,7 +42,7 @@ module Telegram
Instrumentation.instrument(:respond_with, type: type) { super }
end
%i(answer_callback_query answer_inline_query).each do |type|
%i[answer_callback_query answer_inline_query].each do |type|
define_method(type) do |*args|
Instrumentation.instrument(:respond_with, type: type) { super(*args) }
end
@@ -48,7 +51,7 @@ module Telegram
private
# 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)
end
@@ -60,14 +63,14 @@ module Telegram
# end
#
# :api: plugin
def cleanup_view_runtime #:nodoc:
def cleanup_view_runtime # :nodoc:
yield
end
# Every time after an action is processed, this method is invoked
# with the payload, so you can add more information.
# :api: plugin
def append_info_to_payload(_payload) #:nodoc:
def append_info_to_payload(_payload) # :nodoc:
end
module ClassMethods
@@ -75,7 +78,7 @@ module Telegram
# controller process action. This method should return an array
# with the messages to be added.
# :api: plugin
def log_process_action(_payload) #:nodoc:
def log_process_action(_payload) # :nodoc:
[]
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'active_support/log_subscriber'
module Telegram
@@ -8,7 +10,7 @@ module Telegram
info do
payload = event.payload
"Processing by #{payload[:controller]}##{payload[:action]}\n" \
" Update: #{payload[:update].to_json}"
" Update: #{payload[:update].to_json}"
end
end
@@ -17,7 +19,7 @@ module Telegram
payload = event.payload
additions = UpdatesController.log_process_action(payload)
message = "Completed in #{event.duration.round}ms"
message << " (#{additions.join(' | ')})" unless additions.blank?
message += " (#{additions.join(' | ')})" if additions.present?
message
end
end

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

@@ -1,92 +1,78 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
# 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_message(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
extend ActiveSupport::Concern
include Session
included do
# As we use before_action context is cleared anyway,
# no matter we used it or not.
singleton_class.send :attr_reader, :context_handlers, :context_to_action
@context_handlers = {}
end
module ClassMethods
# 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
# 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.
def cancel
def cancel!
# Context is already cleared in action_for_message
end
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
# to select session for message context.
def message_context_session
session
end
# Fetches context and finds handler for it. If message has new command,
# it has higher priority than contextual action.
def action_for_message
val = session.delete(:context)
@context = val && val.to_sym
super || context && begin
handler = handler_for_context
[true, handler, payload['text'].try!(:split) || []] if handler
end
val = message_context_session.delete(:context)
context = val&.to_s
super || (context && begin
args = payload['text']&.split || []
action = action_for_message_context(context)
[[action, {type: :message_context, context: context}], args]
end)
end
# Save context for the next request.
def save_context(context)
session[:context] = context
message_context_session[:context] = context
end
def handler_for_context
self.class.context_handlers[context] || self.class.context_to_action && begin
action_name = context.to_s
unless action_method?(action_name)
raise AbstractController::ActionNotFound,
"The action '#{action_name}' could not be set from context " \
"for #{self.class.name}. " \
'context_to_action! supports only action methods for security reasons. ' \
'If you need to call this action use context_handler for it.'
end
action_name
end
# Returns action name for message context. By default it's the same as context name.
# Raises AbstractController::ActionNotFound if action is not available.
# This differs from other cases where invalid actions are silently ignored,
# because message context is controlled by developer, and users are not able
# to construct update to run any specific context.
def action_for_message_context(context)
action = context.to_s
return action if action_method?(action)
raise AbstractController::ActionNotFound,
"The context action '#{action}' is not found in #{self.class.name}"
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
@@ -21,7 +23,7 @@ module Telegram
respond_with(type, params)
end
# Same as reply_with, but for inline queries.
# Same as respond_with, but for inline queries.
def answer_inline_query(results, params = {})
params = params.merge(
inline_query_id: payload['id'],
@@ -30,7 +32,7 @@ module Telegram
bot.answer_inline_query(params)
end
# Same as reply_with, but for callback queries.
# Same as respond_with, but for callback queries.
def answer_callback_query(text, params = {})
params = params.merge(
callback_query_id: payload['id'],
@@ -38,6 +40,36 @@ module Telegram
)
bot.answer_callback_query(params)
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.
def edit_message(type, params = {})
params =
if message_id = payload['inline_message_id'] # rubocop:disable Lint/AssignmentInCondition
params.merge(inline_message_id: message_id)
elsif message = payload['message'] # rubocop:disable Lint/AssignmentInCondition
params.merge(chat_id: message['chat']['id'], message_id: message['message_id'])
else
raise 'Can not edit message without `inline_message_id` or `message`'
end
bot.public_send("edit_message_#{type}", params)
end
end
end
end

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

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

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

@@ -1,53 +1,44 @@
# frozen_string_literal: true
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
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) do
controller_class.new(bot, update).tap do |x|
controller_class.new(*controller_args).tap do |x|
x.extend Telegram::Bot::UpdatesController::Testing
end
end
let(:update) { build_update(payload_type, payload) }
let(:controller_args) { [bot, deep_stringify(update), webhook_request] }
let(:update) { {payload_type => payload} }
let(:webhook_request) { nil }
let(:payload_type) { :some_type }
let(:payload) { double(:payload) }
let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
let(:bot_name) { 'bot' }
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
def dispatch(bot = self.bot, update = self.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
# Process update.
def dispatch(update = self.update, bot = self.bot, webhook_request = self.webhook_request)
controller.dispatch_again(bot, deep_stringify(update), webhook_request)
end
# Same as `.as_json` but mocks-friendly.
def deep_stringify(input)
case input
when Array then input.map(&method(__callee__))
when Hash then input.transform_keys(&:to_s).transform_values(&method(__callee__))
when Hash then input.map { |k, v| [k.to_s, deep_stringify(v)] }.to_h
else input
end
end
# 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
RSpec.configure do |config|
if config.respond_to?(:include_context)
config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
end
Telegram::Bot::RSpec.with_include_context do |config|
config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/session/abstract/id'
require 'active_support/cache'
@@ -8,19 +10,27 @@ module Telegram
module Session
extend ActiveSupport::Concern
module ClassMethods
# Builds session with given key and optional store (default to session_store).
# This way it's easier to define multiple custom sessions,
# ex. one for group chat and one for user.
def build_session(key, store = session_store)
raise 'session_store is not configured' unless store
key ? SessionHash.new(store, key) : NullSessionHash.new
end
end
def process_action(*)
super
ensure
session.commit
session.commit if @_session
end
protected
def session
@_session ||= begin
key = session_key
key ? SessionHash.new(self.class.session_store, key) : NullSessionHash.new
end
@_session ||= # rubocop:disable Naming/MemoizedInstanceVariableName
self.class.build_session(session_key)
end
def session_key
@@ -34,7 +44,7 @@ module Telegram
class SessionHash < Rack::Session::Abstract::SessionHash
attr_reader :id
def initialize(store, id)
def initialize(store, id) # rubocop:disable Lint/MissingSuper
@store = store
@id = id
end
@@ -58,13 +68,13 @@ module Telegram
def commit
return unless loaded?
data = to_hash.delete_if { |_, v| v.nil? }
data = to_hash.compact
@store.write(id, data)
end
end
class NullSessionHash < Session::SessionHash
def initialize
def initialize # rubocop:disable Lint/MissingSuper
@data = {}
@loaded = true
@exists = true

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

@@ -1,13 +1,15 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
module Testing
IVARS_TO_KEEP = %i(@_session).freeze
IVARS_TO_KEEP = %i[@_session].freeze
# Perform multiple dispatches on same instance.
def dispatch_again(bot = nil, update = nil)
def dispatch_again(bot = nil, update = nil, webhook_request = nil)
recycle!
initialize(bot, update)
initialize(bot, update, webhook_request)
dispatch
end
@@ -16,7 +18,7 @@ module Telegram
# everything will be rewriten with #initialize.
#
# With `full` set to `true` it'll clear all cached instance variables.
def recycle!(full = false)
def recycle!(full = false) # rubocop:disable Style/OptionalBooleanParameter
return unless full
(instance_variables - IVARS_TO_KEEP).each do |ivar|
remove_instance_variable(ivar)
@@ -27,7 +29,8 @@ module Telegram
# Stubs session.
def session
@_session ||= Session::NullSessionHash.new
@_session ||= # rubocop:disable Naming/MemoizedInstanceVariableName
Session::NullSessionHash.new
end
end
end

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

@@ -0,0 +1,49 @@
# frozen_string_literal: true
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

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

@@ -1,10 +1,12 @@
# frozen_string_literal: true
module Telegram
module Bot
class UpdatesController
# Include this module to type cast update to Virtus model
# using `telegram-bot-types` gem (install this gem first).
module TypedUpdate
def initialize(bot = nil, update = nil)
def initialize(bot = nil, update = nil, webhook_request = nil)
update = Types::Update.new(update) if update && !update.is_a?(Types::Update)
super
end

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

@@ -1,9 +1,11 @@
# frozen_string_literal: true
module Telegram
module Bot
# Supposed to be used in development environments only.
class UpdatesPoller
class << self
@@instances = {} # rubocop:disable ClassVars
@@instances = {} # rubocop:disable Style/ClassVars
def instances
@@instances
@@ -36,43 +38,59 @@ module Telegram
end
def log(&block)
logger.info(&block) if logger
logger&.info(&block)
end
def start
return if running
@running = true
log { 'Started bot poller.' }
while running
begin
fetch_updates do |update|
controller.dispatch(bot, update)
end
rescue Interrupt
@running = false
rescue => e
logger.error { ([e.message] + e.backtrace).join("\n") } if logger
end
begin
@running = true
log { 'Started bot poller.' }
run
rescue Interrupt
nil # noop
ensure
@running = false
end
log { 'Stop polling bot updates.' }
log { 'Stopped polling bot updates.' }
end
def run
while running
updates = fetch_updates
process_updates(updates) if updates&.any?
end
end
# Method to stop poller from other thread.
def stop
return unless running
log { 'Killing polling thread.' }
log { 'Stopping polling bot updates.' }
@running = false
end
def fetch_updates
def fetch_updates(offset = self.offset)
response = bot.async(false) { bot.get_updates(offset: offset, timeout: timeout) }
return unless response['ok'] && response['result'].any?
response.is_a?(Array) ? response : response['result']
rescue Timeout::Error
log { 'Fetch timeout' }
nil
end
def process_updates(updates)
reload! do
response['result'].each do |update|
updates.each do |update|
@offset = update['update_id'] + 1
yield update
process_update(update)
end
end
rescue Timeout::Error # rubocop:disable HandleExceptions
rescue StandardError => exception
logger&.error { ([exception.message] + exception.backtrace).join("\n") }
end
# Override this method to setup custom error collector.
def process_update(update)
controller.dispatch(bot, update)
end
def reload!
@@ -86,10 +104,8 @@ module Telegram
end
if defined?(Rails.application) && Rails.application.respond_to?(:reloader)
def reloading_code
Rails.application.reloader.wrap do
yield
end
def reloading_code(&block)
Rails.application.reloader.wrap(&block)
end
else
def reloading_code

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

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Telegram
module Bot
VERSION = '0.10.1'.freeze
VERSION = '0.16.2'
def self.gem_version
Gem::Version.new VERSION

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

@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'integration_helper'
RSpec.describe DefaultBotController, telegram_bot: :rails do
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'from default' }
end
describe '#load_session' do
subject { -> { dispatch_command :load_session } }
it { should_not raise_error }
end
describe '#dispatch' do
it 'provides webhook_request' do
expect(described_class).to receive(:dispatch).
with(bot, hash_including('message'), instance_of(ActionDispatch::Request)).
and_call_original
dispatch_message :test
end
end
end

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

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

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

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

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

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

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

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

@@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'pathname'
require 'pry'
require 'debug'
require 'rspec/its'
if ENV['CI']
@@ -10,14 +12,15 @@ elsif ENV.key?('COV')
SimpleCov.start
end
GEM_ROOT = Pathname.new File.expand_path('../..', __FILE__)
GEM_ROOT = Pathname.new File.expand_path('..', __dir__)
$LOAD_PATH.unshift GEM_ROOT.join('lib')
require 'telegram/bot'
require 'telegram/bot/updates_controller/rspec_helpers'
require 'telegram/bot/types'
require 'active_support/json'
Dir[GEM_ROOT.join('spec/support/**/*.rb')].each { |f| require f }
Dir[GEM_ROOT.join('spec/support/**/*.rb')].sort.each { |f| require f }
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
@@ -31,6 +34,14 @@ RSpec.configure do |config|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
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|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to

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

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

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

@@ -0,0 +1,26 @@
# frozen_string_literal: true
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,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::Async::Job do
let(:job_class) do
described_class = self.described_class

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

@@ -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
include_examples '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,94 @@
# frozen_string_literal: true
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,9 +1,11 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::Client::TypedResponse do
let(:bot) { Telegram::Bot::Client.new('token').tap { |x| x.extend described_class } }
describe '#get_me' do
subject { bot.get_me }
before { expect(bot).to receive(:request).with(:getMe) { response } }
before { expect(bot).to receive(:request).with('getMe') { response } }
let(:response) { {'ok' => true, 'result' => {'id' => user_id}} }
let(:user_id) { 123 }
it { should be_instance_of Telegram::Bot::Types::User }
@@ -17,7 +19,7 @@ RSpec.describe Telegram::Bot::Client::TypedResponse do
describe '#get_updates' do
subject { bot.get_updates }
before { expect(bot).to receive(:request).with(:getUpdates) { response } }
before { expect(bot).to receive(:request).with('getUpdates') { response } }
let(:response) { {'ok' => true, 'result' => [{'update_id' => update_id}]} }
let(:update_id) { 123 }
it { should be_instance_of Array }
@@ -29,4 +31,16 @@ RSpec.describe Telegram::Bot::Client::TypedResponse do
it { expect { subject }.to raise_error Telegram::Bot::Error }
end
end
describe '#delete_webhook' do
subject { bot.delete_webhook }
before { expect(bot).to receive(:request).with('deleteWebhook') { response } }
let(:response) { {'ok' => true, 'result' => true} }
it { should eq true }
context 'on error' do
let(:response) { raise Telegram::Bot::Error }
it { expect { subject }.to raise_error Telegram::Bot::Error }
end
end
end

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

@@ -1,51 +1,167 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::Client do
let(:instance) { described_class.new 'token' }
let(:token) { 'token' }
let(:botan_token) { double(:botan_token) }
include_examples 'initializers'
include_examples 'async', request_args: -> { [double(:action), {body: :content}] }
it_behaves_like 'async', request_args: -> { [double(:action), {body: :content}] }
describe '.prepare_body' do
subject { described_class.prepare_body(input) }
describe '.wrap' do
subject { described_class.wrap(input, **options) }
let(:options) { {} }
let(:result) { double(:result) }
let(:username) { 'username' }
context 'when plain hash is given' do
let(:input) { {a: 1, b: '2', c: nil} }
it { should eq input }
context 'when input is a string' do
let(:input) { token }
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
context 'when nested hash is given' do
let(:input) { {a: 1, b: '2', c: [1, 2], d: {a: 1}, e: {b: []}} }
context 'when input is a hash' do
let(:input) { {token: token, 'username' => username, other: :options} }
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
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(: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
describe '.prepare_async_args' do
subject { described_class.prepare_async_args(*input) }
let(:input) { [:action, a: 1, b: :sym, c: [:other], 'd' => 'str'] }
it { should eq ['action', a: 1, b: 'sym', c: '["other"]', 'd' => 'str'] }
let(:input) { [:action, {a: 1, b: :sym, c: [:other], 'd' => 'str'}] }
it { should eq ['action', {a: 1, b: 'sym', c: '["other"]', 'd' => 'str'}] }
end
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
let(:args) { %w(secret superbot) }
its(:token) { should eq args[0] }
its(:username) { should eq args[1] }
its(:base_uri) { should include args[0] }
let(:args) { [token, username] }
its(:token) { should eq token }
its(:username) { should eq username }
its(:base_uri) { should eq "#{described_class::SERVER}/bot#{token}/" }
end
context 'when hash is given' do
let(:args) { [token: 'secret', username: 'superbot'] }
its(:token) { should eq args[0][:token] }
its(:username) { should eq args[0][:username] }
its(:base_uri) { should include args[0][:token] }
let(:kwargs) { {token: 'secret', username: 'superbot'} }
its(:token) { should eq token }
its(:username) { should eq username }
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
describe '#request' do
subject { -> { instance.request(action, request_body) } }
let(:action) { :some_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(:prepared_body) { double(:prepared_body) }
let(:response) { HTTP::Message.new_response(body).tap { |x| x.status = status } }
let(:status) { 200 }
let(:body) { body_json.to_json }
let(:body_json) { {'param' => 'val', 'description' => 'some description'} }
before do
expect(Telegram::Bot::Client::RequestBodyFormatter).
to receive(:format).with(request_body, action) { prepared_body }
expect(instance).to receive(:http_request).with(url, prepared_body) { response }
end
around { |ex| Telegram::Bot::ClientStub.stub_all!(false) { ex.run } }
shared_examples 'invalid body' do |error = Telegram::Bot::Error|
context 'when body is not json' do
let(:body) { '{' }
it { should raise_error error }
end
end
its(:call) { should eq body_json }
include_examples 'invalid body', JSON::ParserError
context 'when status is 403' do
let(:status) { 403 }
it { should raise_error Telegram::Bot::Forbidden, body_json['description'] }
include_examples 'invalid body'
end
context 'when status is 404' do
let(:status) { 404 }
it { should raise_error Telegram::Bot::NotFound, body_json['description'] }
include_examples 'invalid body'
end
context 'when status is other' do
let(:status) { 500 }
it { should raise_error Telegram::Bot::Error, /#{body_json['description']}/ }
end
end
end

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

@@ -1,7 +1,9 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::ClientStub do
describe '#stub_all!' do
let(:client) { Telegram::Bot::Client.new('token', 'bot_name') }
let(:clients) { ['token', token: 'token2'].map(&Telegram::Bot::Client.method(:wrap)) }
let(:clients) { ['token', {token: 'token2'}].map(&Telegram::Bot::Client.method(:wrap)) }
shared_examples 'constructors' do |expected_class|
it 'makes Client.new return ClientStub' do
@@ -37,17 +39,25 @@ RSpec.describe Telegram::Bot::ClientStub do
end
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
let(:args) { 'superbot' }
its(:username) { should eq args }
let(:args) { ['superbot'] }
its(:username) { should eq args[0] }
end
context 'when username and token are given' do
let(:args) { %w(token superbot) }
its(:token) { should eq nil }
let(:args) { %w[token superbot] }
its(:token) { should eq args[0] }
its(:username) { should eq args[1] }
end
context 'when hash config is given' do
let(:kwargs) { {token: 'token', username: 'superbot'} }
its(:token) { should eq kwargs[:token] }
its(:username) { should eq kwargs[:username] }
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::ConfigMethods do
let(:registry) do
Object.new.tap do |x|
@@ -11,19 +13,22 @@ RSpec.describe Telegram::Bot::ConfigMethods do
chat: {
token: 'chat_token',
username: 'Chat',
botan: 'chat_botan_token',
},
other_chat: {
'token' => 'other_chat_token',
'username' => 'OtherChat',
'botan' => 'other_chat_botan_token',
},
}
end
describe '#bot' do
subject { registry.bot }
it { should eq registry.bots[:default] }
subject { -> { registry.bot } }
its(:call) { should eq registry.bots[:default] }
context 'when bot is not configured' do
let(:config) { super().except(:default) }
it { should raise_error(/bot is not configured/) }
end
end
describe '#bots' do
@@ -38,7 +43,6 @@ RSpec.describe Telegram::Bot::ConfigMethods do
its(:id) { should eq :chat }
its(:token) { should eq config[:chat][:token] }
its(:username) { should eq config[:chat][:username] }
its('botan.token') { should eq config[:chat][:botan] }
end
context 'configured by hash with stringified keys' do
@@ -46,18 +50,6 @@ RSpec.describe Telegram::Bot::ConfigMethods do
its(:id) { should eq :other_chat }
its(:token) { should eq config[:other_chat]['token'] }
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
@@ -67,16 +59,18 @@ RSpec.describe Telegram::Bot::ConfigMethods do
context 'when not configured' do
let(:registry) { Object.new.tap { |x| x.extend described_class } }
before { hide_const('Rails') }
it { should eq({}) }
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) { {} }
it { should eq({}) }
context 'when there is telegram section in secrets' do
let(:secrets) { {telegram: config.stringify_keys} }
let(:config) do
let(:secrets) { {telegram: secrets_config} }
let(:secrets_config) do
{
bot: double(:bot_config),
bots: {
@@ -85,8 +79,22 @@ RSpec.describe Telegram::Bot::ConfigMethods do
},
}
end
it { should include default: config[:bot] }
it { should include config[:bots] }
it { should include default: secrets_config[:bot] }
it { should include secrets_config[:bots] }
context 'on rails >5.1 (deep symbolized keys)' do
let(:secrets) { super().deep_symbolize_keys }
it { should include default: secrets_config[:bot] }
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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/mock'
RSpec.describe Telegram::Bot::Middleware do
@@ -7,12 +9,11 @@ RSpec.describe Telegram::Bot::Middleware do
describe '#call' do
subject { instance.call(env) }
let(:env) { {'action_dispatch.request.request_parameters' => json_body} }
let(:update) { {'message' => {'id' => 1}} }
let(:env) do
Rack::MockRequest.env_for('/',
method: :post,
input: JSON.dump(update),
input: JSON.dump(update),
'CONTENT_TYPE' => 'application/json',
)
end
@@ -21,12 +22,12 @@ RSpec.describe Telegram::Bot::Middleware do
if ActionPack::VERSION::MAJOR < 5
# Before Rails 5, params are parsed in middleware.
# In Rails 5, they are parsed in Request#request_parameters.
require 'action_dispatch/middleware/params_parser'
let(:instance) { ActionDispatch::ParamsParser.new(super()) }
end
it 'calls dispatch on controller' do
expect(controller).to receive(:dispatch).with(bot, update)
expect(controller).to receive(:dispatch).
with(bot, update, instance_of(ActionDispatch::Request))
subject
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'telegram/bot/routes_helper'
RSpec.describe Telegram::Bot::RoutesHelper do
@@ -40,105 +42,62 @@ RSpec.describe Telegram::Bot::RoutesHelper do
end
end
describe '#telegram_webhooks' do
subject { mapper.telegram_webhooks(*input) }
describe '#telegram_webhook' do
subject { ->(*args, **kwargs) { mapper.telegram_webhook(*args, **kwargs) } }
let(:mapper) { double(:mapper).tap { |x| x.extend described_class } }
let(:bots) { {default: bot, other: other_bot} }
let(:controller) { double(:controller, name: :controller) }
let(:other_controller) { double(:other_controller, name: :other_controller) }
before { allow(Telegram).to receive(:bots) { bots } }
def assert_routes(*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
def assert_route(bot, controller, path: nil, **expected_options) # rubocop:disable Metrics/AbcSize
path ||= "telegram/#{described_class.token_hash(bot.token)}"
expect(mapper).to receive(:post) do |actual_path, actual_options|
expect(actual_path).to eq(path)
middleware = actual_options[:to]
expect(middleware.controller).to eq(controller)
expect(middleware.bot.token).to eq(bot.token)
expect(middleware.bot.username).to eq(bot.username)
expect(actual_options).to include(expected_options)
end
subject
yield
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]
it 'creates routes for default bot' do
assert_route(bot, controller, as: 'default_telegram_webhook') do
subject[controller]
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
other_controller = double(:other_controller, name: :other_controller)
assert_route(bot, other_controller, as: 'custom_route') do
subject[other_controller, as: 'custom_route']
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,
]
it 'passes extra options' do
assert_route(bot, controller, as: 'default_telegram_webhook', option: :val, other: 2) do
subject[controller, option: :val, other: 2]
end
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]
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
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,
]
assert_route(create_bot('custom_token'), controller, as: 'telegram_webhook') do
subject[controller, 'custom_token']
end
end
end
end

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

@@ -0,0 +1,44 @@
# frozen_string_literal: true
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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::RSpec::ClientMatchers do
let(:bot) { Telegram::Bot::ClientStub.new('TestBot') }
let(:other_bot) { Telegram::Bot::ClientStub.new('OtherTestBot') }
@@ -31,6 +33,9 @@ RSpec.describe Telegram::Bot::RSpec::ClientMatchers do
args = {text: 'test', parseMode: :Markdown}
block = ->(*) { bot.send_message(args) }
expect(&block).to make_telegram_request(bot, :sendMessage).with(args)
expect do
expect(&block).to make_telegram_request(bot, :sendMessage).with(args.merge(extra: 1))
end.to raise_error RSpec::Expectations::ExpectationNotMetError
expect(&block).to_not make_telegram_request(bot, :sendMessage).with(args.except(:text))
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).
with(args).exactly(3).times

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

@@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'telegram/bot/rspec/integration/poller'
RSpec.describe 'Poller integration spec', telegram_bot: :poller do
include_examples 'shared integration examples'
describe '#dispatch' do
it 'doesn`t provide webhook_request' do
expect(controller_class).to receive(:dispatch).
with(bot, hash_including('message')).
and_call_original
dispatch_message :test
end
end
end

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

@@ -0,0 +1,31 @@
# frozen_string_literal: true
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
describe '#dispatch' do
it 'provides webhook_request' do
expect(controller_class).to receive(:dispatch).
with(bot, hash_including('message'), instance_of(ActionDispatch::Request)).
and_call_original
dispatch_message :test
end
end
end

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

@@ -0,0 +1,2 @@
# frozen_string_literal: true
# Tested in spec/integration

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

@@ -1,71 +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
end

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

@@ -0,0 +1,117 @@
# frozen_string_literal: true
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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
@@ -6,7 +8,8 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
include described_class
attr_accessor :filter_done
before_action only: :ctx2_callback_query do
before_action only: :ctx_2_callback_query do
self.filter_done = true
end
@@ -14,12 +17,12 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
[:no_context, data]
end
def ctx1_callback_query(data)
[:ctx1, data]
def ctx_1_callback_query(data)
[:ctx_1, data]
end
def ctx2_callback_query(data)
[:ctx2, data]
def ctx_2_callback_query(data)
[:ctx_2, data]
end
end
end
@@ -27,7 +30,7 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
describe '#dispatch' do
subject { -> { dispatch } }
let(:payload_type) { :callback_query }
let(:payload) { {'data' => data} }
let(:payload) { {data: data} }
let(:data) { text }
let(:text) { 'asd qwe zxc' }
@@ -40,19 +43,19 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
let(:data) { "#{ctx}:#{text}" }
context 'and valid' do
let(:ctx) { 'ctx1' }
its(:call) { should eq [:ctx1, text] }
let(:ctx) { 'ctx_1' }
its(:call) { should eq [:ctx_1, text] }
it { should_not change(controller, :filter_done) }
context 'and context has callback' do
let(:ctx) { 'ctx2' }
its(:call) { should eq [:ctx2, text] }
let(:ctx) { 'ctx_2' }
its(:call) { should eq [:ctx_2, text] }
it { should change(controller, :filter_done) }
end
context 'and data has multiple colons' do
let(:text) { super().tr(' ', ':') }
its(:call) { should eq [:ctx1, text] }
its(:call) { should eq [:ctx_1, text] }
end
end

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

@@ -0,0 +1,113 @@
# frozen_string_literal: true
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

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

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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
@@ -6,30 +8,29 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
include described_class
attr_accessor :filter_done
before_action only: :redirect do
before_action only: :context_with_filter do
self.filter_done = true
end
attr_reader :callbacks_runs
before_action { @callbacks_runs = (@callbacks_runs || 0) + 1 }
def message(*args)
[:no_context, *args]
end
context_handler :block do |*args|
[:block_result, *args]
def handler_method(*args)
[:method_result_1, *args]
end
context_handler :redirect
context_handler :other_redirect, :redirect
def redirect(*args)
[:method_result, *args]
def context_with_filter(*args)
[:method_result_2, *args]
end
def action(*args)
[:action_result, *args]
def action!(*args)
[:command_result, *args]
end
private
@@ -52,85 +53,59 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
it { should_not change { session[:context] } }
end
context 'when context is handled by block' do
before { session[:context] = :block }
its(:call) { should eq [:block_result, *text.split] }
context 'when context is handled by handler_method' do
before { session[:context] = :handler_method }
its(:call) { should eq [:method_result_1, *text.split] }
it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil }
context 'when message has no text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
its(:call) { should eq [:block_result] }
its(:call) { should eq [:method_result_1] }
end
context 'when message has new command' do
let(:text) { '/action a s d' }
its(:call) { should eq [:action_result, *%w(a s d)] }
its(:call) { should eq [:command_result, 'a', 's', 'd'] }
it { should change { session[:context] }.to nil }
end
end
context 'when context is handled by short redirect' do
before { session[:context] = :redirect }
its(:call) { should eq [:method_result, *text.split] }
context 'when context is handled by short context_with_filter' do
before { session[:context] = :context_with_filter }
its(:call) { should eq [:method_result_2, *text.split] }
it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil }
it { should change(controller, :callbacks_runs).to 1 }
context 'when message has no text' do
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 { session[:context] }.to nil }
end
end
context 'when context is handled by custom redirect' do
before { session[:context] = :other_redirect }
its(:call) { should eq [:method_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] }
context 'when context is command-action`s name' do
before { session[:context] = :action! }
its(:call) { should eq [:command_result, *text.split] }
it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil }
end
context 'when context_to_action is true' do
before { controller_class.context_to_action! }
context 'when context is not set' do
its(:call) { should eq [:no_context, payload] }
it { should_not change(controller, :filter_done) }
it { should_not change { session[:context] } }
context 'when context is not an action`s name' do
before { session[:context] = :not_action }
it do
should raise_error(AbstractController::ActionNotFound).
and change { session[:context] }.to nil
end
end
context 'when context is action`s name but not mapped' do
before { session[:context] = :action }
its(:call) { should eq [:action_result, *text.split] }
it { should_not change(controller, :filter_done) }
it { should 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
context 'when context is invalid name' do
before { session[:context] = :invalid }
it do
should raise_error(AbstractController::ActionNotFound).
and change { session[:context] }.to nil
end
end
end

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

@@ -1,11 +1,13 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::UpdatesController do
include_context 'telegram/bot/updates_controller'
let(:params) { {arg: 1, 'other_arg' => 2} }
let(:respond_type) { :photo }
let(:result) { double(:result) }
let(:payload_type) { :message }
let(:payload) { {message_id: double(:message_id)} }
let(:chat) { {'id' => double(:chat_id)} }
let(:payload) { {message_id: double(:message_id), chat: chat} }
let(:chat) { {id: double(:chat_id)} }
shared_examples 'missing chat' do
context 'when chat is missing' do
@@ -19,9 +21,8 @@ RSpec.describe Telegram::Bot::UpdatesController do
include_examples 'missing chat'
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(chat_id: chat['id'])) { result }
with(params.merge(chat_id: chat[:id])) { result }
should eq result
end
end
@@ -31,19 +32,46 @@ RSpec.describe Telegram::Bot::UpdatesController do
include_examples 'missing chat'
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(
chat_id: chat['id'],
chat_id: chat[:id],
reply_to_message_id: payload[:message_id],
)) { result }
should eq result
end
context 'when update is not set' do
let(:update) { {chat: chat} }
let(:controller_args) { [bot, {chat: deep_stringify(chat)}] }
it 'sets chat_id' do
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
end
end
end
describe '#edit_message' do
subject { controller.edit_message(type, params) }
let(:type) { :reply_markup }
it { expect { subject }.to raise_error(/Can not edit message without/) }
context 'when inline_message_id is present' do
let(:payload) { {inline_message_id: double(:message_id)} }
it 'sets inline_message_id' do
expect(bot).to receive("edit_message_#{type}").with(params.merge(
inline_message_id: payload[:inline_message_id],
)) { result }
should eq result
end
end
context 'when message is present' do
let(:payload) { {message: super().merge(chat: chat)} }
it 'sets chat_id & message_id' do
expect(bot).to receive("edit_message_#{type}").with(params.merge(
message_id: payload[:message][:message_id],
chat_id: payload[:message][:chat][:id],
)) { result }
should eq result
end
end

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

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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::UpdatesController::Session do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
@@ -19,11 +21,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
controller_class.class_eval do
self.session_store = :memory_store
def write(text)
def write!(text)
session[:text] = text
end
def read
def read!
session[:text]
end
@@ -36,7 +38,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
end
def build_message(text, from)
{'message' => {'text' => text, 'from' => from.stringify_keys}}
deep_stringify(message: {text: text, from: from})
end
it 'stores session between requests' do
@@ -54,4 +56,45 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
end
end
end
describe '.build_session' do
subject { controller_class.build_session(key, *args) }
let(:key) {}
let(:args) { [] }
it { expect { subject }.to raise_error(/session_store is not configured/) }
shared_examples 'NullSessionHash when key is not present' do |store_proc|
it { should be_instance_of(described_class::NullSessionHash) }
context 'and key is present' do
let(:key) { :test_key }
it 'is valid SessionHash' do
expect(subject).to be_instance_of(described_class::SessionHash)
expect(subject.id).to eq key
expect(subject.instance_variable_get(:@store)).to be(instance_exec(&store_proc))
end
end
end
context 'when store configured' do
before { controller_class.session_store = nil }
include_examples 'NullSessionHash when key is not present',
-> { controller_class.session_store }
end
context 'when store is given' do
let(:args) { [double(:store)] }
include_examples 'NullSessionHash when key is not present', -> { args[0] }
end
end
describe '.session_store=' do
subject { ->(val) { controller_class.session_store = val } }
it 'casts to AS::Cache' do
expect { subject[:null_store] }.to change(controller_class, :session_store).
to(instance_of(ActiveSupport::Cache::NullStore))
expect { subject[nil] }.to change(controller_class, :session_store).
to(instance_of(ActiveSupport::Cache::MemoryStore))
end
end
end

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

@@ -1,3 +1,7 @@
# frozen_string_literal: true
require 'action_dispatch'
RSpec.describe Telegram::Bot::UpdatesController::Testing do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
@@ -24,4 +28,27 @@ RSpec.describe Telegram::Bot::UpdatesController::Testing do
it { should_not change { controller.send(:session)[:key] }.from 'sval' }
end
end
describe '#dispatch' do
it 'doesn`t provide webhook_request' do
dispatch
expect(controller.webhook_request).to eq nil
end
context 'when webhook_request is set' do
let(:webhook_request) { ActionDispatch::Request.new({}) }
it 'passes it to controller' do
dispatch
expect(controller.webhook_request).to eq webhook_request
end
end
context 'when webhook_request is given' do
it 'passes it to controller' do
webhook_request = ActionDispatch::Request.new({})
dispatch(update, bot, webhook_request)
expect(controller.webhook_request).to eq webhook_request
end
end
end
end

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

@@ -0,0 +1,82 @@
# frozen_string_literal: true
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

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe Telegram::Bot::UpdatesController::TypedUpdate do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
@@ -9,17 +11,56 @@ RSpec.describe Telegram::Bot::UpdatesController::TypedUpdate do
context 'when `update` is a virtus model' do
subject { controller }
%w(
message
inline_query
chosen_inline_result
).each do |type|
unique_types = (Telegram::Bot::UpdatesController::PAYLOAD_TYPES - %w[
edited_message
channel_post
edited_channel_post
my_chat_member
chat_member
chat_join_request
# TODO: remove when added to telegram-bot-types
business_connection
business_message
edited_business_message
deleted_business_messages
message_reaction
message_reaction_count
chat_boost
removed_chat_boost
]).
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
type_class = Telegram::Bot::Types.const_get(type.camelize)
let(:payload_type) { type }
let(:payload) { {} }
let(:payload) do
{}.tap do |result|
result[:chat] = chat if type_class.instance_methods.include?(:chat)
result[:from] = from if type_class.instance_methods.include?(:from)
end
end
let(:chat) { {id: 'chat_id'} }
let(:from) { {id: 'from_id'} }
its(:payload_type) { should eq payload_type }
its(:payload) { should be_instance_of type_class }
if type_class.instance_methods.include?(:chat)
# Virtus does not support ==. :(
its(:chat) { should be_instance_of Telegram::Bot::Types::Chat }
its('chat.to_hash') { should include chat }
else
its(:chat) { should eq nil }
end
if type_class.instance_methods.include?(:from)
its(:from) { should be_instance_of Telegram::Bot::Types::User }
its('from.to_hash') { should include from }
else
its(:from) { should eq nil }
end
end
end
end

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше