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

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

..

43 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
88178113d7 Support (edited_)channel_post updates, add api methods from 2.3, 2.3.1 2016-12-09 13:23:58 +03:00
printercu
1a2ac0dd83 Update README.md 2016-11-16 21:31:02 +03:00
Max Melentiev
fa6e385ca1 v0.10.0 2016-11-16 20:51:41 +03:00
Max Melentiev
1098118826 Fix integration spec for Rails 4 2016-11-16 14:34:42 +03:00
Max Melentiev
0c9a4a5fa0 Inegration context for RSpec
ActionDispatch::Testing::Integration requires smth else and
defines Rails, so had to change `defined?(Rails)` all over the code.
2016-11-16 14:18:45 +03:00
printercu
5c299887bb Merge pull request #18 from bob-frost/ruby2.2.0
Error "can't dup Fixnum (TypeError)"
2016-11-13 19:16:59 +03:00
bob-frost
9f2234c132 Fix "can't dup Fixnum (TypeError)" for Ruby 2.2.0 2016-11-13 09:47:01 +06:00
Max Melentiev
ec1d5bad7d v0.9.0 2016-11-07 20:24:01 +03:00
Max Melentiev
194fc78202 RSpec matchers 2016-11-07 20:17:34 +03:00
Max Melentiev
ececfa660e [Docs] Note about queue adapters 2016-10-19 14:20:39 +03:00
printercu
9e67122465 Merge pull request #2 from telegram-bot-rb/async
Async requests
2016-10-19 14:00:26 +03:00
Max Melentiev
caee9809d0 More docs on async mode 2016-10-19 13:59:54 +03:00
Max Melentiev
4b511f51a7 update changelog 2016-10-19 13:36:03 +03:00
Max Melentiev
d2d6202fa7 API methods from 2016-10-03 update
https://core.telegram.org/bots/api-changelog#october-3-2016
2016-10-17 17:43:13 +03:00
Max Melentiev
dd38cfea93 Take chat from message for callback queries 2016-10-07 12:45:16 +03:00
Max Melentiev
3cb6e930b8 Fix typo in module name: CallbackQueyContext -> CallbackQueryContext 2016-09-22 01:12:19 +03:00
Max Melentiev
e42a293804 edit_message_* methods 2016-09-21 22:53:06 +03:00
Max Melentiev
2e405acfb1 0.9.0.alpha2 2016-08-15 21:51:42 +06:00
Max Melentiev
b2eca2d37f Thread-safety for async config methods 2016-08-15 21:50:42 +06:00
Max Melentiev
6d687e459a 0.9.0.alpha1 2016-08-09 18:25:54 +06:00
Max Melentiev
71f736e0a2 Async requests with ActiveJob 2016-08-09 18:20:56 +06:00
Max Melentiev
85ff07de9b Extract Botan-related code from Client to Botan::ClientHelpers 2016-08-09 16:07:44 +06:00
Max Melentiev
27602b20bd Simplified .wrap method, added for Botan 2016-08-09 16:07:21 +06:00
Max Melentiev
91e0aa2096 Moved Botan helpers to its namespace 2016-08-06 18:06:50 +06:00
Max Melentiev
4e539e1867 One more description for StaleChat error 2016-08-01 15:52:59 +06:00
printercu
33f0049f6e Add link to sample standalone app to README 2016-07-30 19:34:16 +06:00
Max Melentiev
207754499e telegram_webhooks doesn't crash when bot's token is nil
Fixes #11
2016-07-30 19:03:24 +06:00
Max Melentiev
995cde5607 v0.8.0 2016-06-02 21:36:05 +03:00
Max Melentiev
9ba5a817cd Fixed doubled instrumentation for reply_with 2016-06-02 21:19:27 +03:00
Max Melentiev
0205891985 Controller#respond_with to reply without reply_to_message_id 2016-06-02 21:07:15 +03:00
printercu
7393b1a1f3 Merge pull request #7 from telegram-bot-rb/rails5
Rails5 compatibility
2016-06-02 20:34:09 +03:00
Max Melentiev
43b519c5e8 fixed controller's callbacks for rails5 2016-06-02 20:30:35 +03:00
Max Melentiev
93ff9851f6 Add rails 5 to travis matrix 2016-06-02 19:48:19 +03:00
Max Melentiev
7c1f1b5b63 Middleware is rails5-compatible 2016-06-02 19:38:29 +03:00
printercu
028849a617 Merge pull request #5 from dreyks/reply_to_message_id
Fix: reply_with sets reply_to_message_id instead of reply_to_message
2016-06-01 23:22:32 +03:00
Roman Usherenko
ba1b3c0974 fix reply_to_message_id 2016-06-01 15:18:29 +03:00
Max Melentiev
f61cc1e536 v0.7.4 2016-05-31 17:44:24 +03:00
printercu
2f7ff9d50e Merge pull request #4 from dreyks/rails5
Rails5 compatibility
2016-05-31 17:34:31 +03:00
Roman Usherenko
aca35a1934 Rails5 compatibility 2016-05-31 17:12:36 +03:00
Max Melentiev
5bf98b419b v0.7.3 2016-05-31 11:49:54 +03:00
Max Melentiev
8077ef9bcf Fixed issues with poller in production. Fixes #3 2016-05-31 11:46:58 +03:00
Max Melentiev
5fef6fb43a v0.7.2 2016-05-31 11:02:55 +03:00
Max Melentiev
07be01c07d Bot API 2.1 2016-05-31 11:01:00 +03:00
46 изменённых файлов: 1459 добавлений и 217 удалений

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

@@ -2,5 +2,8 @@ language: ruby
cache: bundler cache: bundler
rvm: rvm:
- 2.2.3 - 2.2.3
env:
- RAILS=4
- RAILS=5
notifications: notifications:
email: false email: false

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

@@ -1,4 +1,43 @@
# 0.7.1 # 0.10.1
- Support `(edited_)channel_post` updates.
- New methods from 2.3, 2.3.1 API updates.
# 0.10.0
- Integration helpers for RSpec.
- Fix for ruby 2.2.0 bug by @bob-frost.
# 0.9.0
- Async API requests.
- One more description for StaleChat error.
- edit_message_* methods.
- API methods from 2016-10-03 update
- Fix typo in module name: CallbackQueyContext -> CallbackQueryContext.
- Take `chat` from `message` for callback queries
- RSpec matchers.
# 0.8.0
- Fixed `#reply_with`, now it sets `reply_to_message_id` as it's supposed to.
Added `#respond_with` which works the same way, but doesn't set `reply_to_message_id`.
Please, replace all occurrences of `reply_with` to `respond_with` to
keep it working the old way.
- Fixes for Rails 5:
- Controller callbacks
- Middleware
- Setup travis builds
# 0.7.4
- Rails 5 support by @dreyks (#4).
# 0.7.3
- Fixed issues with poller in production (#3)
# 0.7.2
- Bot API 2.1 - Bot API 2.1
- Fixed possible crashes when payload type is not supported. - Fixed possible crashes when payload type is not supported.

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

@@ -1,14 +1,21 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
case ENV['RAILS']
when '5'
gem 'actionpack', '5.0.0.rc1'
when '4'
gem 'actionpack', '~> 4.2'
end
group :development do group :development do
gem 'sdoc', '~> 0.4.1' gem 'sdoc', '~> 0.4.1'
gem 'pry', '~> 0.10.1' gem 'pry', '~> 0.10.1'
gem 'pry-byebug', '~> 3.2.0' gem 'pry-byebug', '~> 3.2.0'
gem 'telegram-bot-types', '~> 0.2.0' gem 'telegram-bot-types', '~> 0.3.0'
gem 'rspec', '~> 3.3.0' gem 'rspec', '~> 3.5.0'
gem 'rspec-its', '~> 1.1.0' gem 'rspec-its', '~> 1.1.0'
gem 'rubocop', '~> 0.37.0' gem 'rubocop', '~> 0.37.0'

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

@@ -5,7 +5,8 @@
[![Build Status](https://travis-ci.org/telegram-bot-rb/telegram-bot.svg)](https://travis-ci.org/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)
Tools for developing bot for Telegram. Best used with Rails, but can be be used in Tools for developing bot for Telegram. Best used with Rails, but can be be used in
standalone app. Supposed to be used in webhook-mode in production, and poller-mode [standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Non-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. in development, but you can use poller in production if you want.
Package contains: Package contains:
@@ -16,11 +17,16 @@ Package contains:
- Middleware and routes helpers for production env. - Middleware and routes helpers for production env.
- Poller with automatic source-reloader for development env. - Poller with automatic source-reloader for development env.
- Rake tasks to update webhook urls. - Rake tasks to update webhook urls.
- __[Async mode](#async-mode)__ for Telegram and/or Botan API.
Let the queue adapter handle network errors!
Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app) Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app)
with session, keyboards and inline queries. with session, keyboards and inline queries.
Run it on your local machine in 1 minute! Run it on your local machine in 1 minute!
And here is [app teamplate](https://github.com/telegram-bot-rb/rails_template)
to generate clean app in seconds.
## Installation ## Installation
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
@@ -130,10 +136,12 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# do_smth_with(data) # do_smth_with(data)
# There are `chat` & `from` shortcut methods. # There are `chat` & `from` shortcut methods.
# For callback queries `chat` if taken from `message` when it's available.
response = from ? "Hello #{from['username']}!" : 'Hi there!' response = from ? "Hello #{from['username']}!" : 'Hi there!'
# There is `reply_with` helper to set basic fields # There is `respond_with` helper to set `chat_id` from received message:
# like `reply_to_message` & `chat_id`. respond_with :message, text: response
reply_with :message, text: response # `reply_with` also sets `reply_to_message_id`:
reply_with :photo, photo: File.open('party.jpg')
end end
private private
@@ -188,7 +196,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
def read def read
reply_with :message, text: session[:text] respond_with :message, text: session[:text]
end end
private private
@@ -214,23 +222,23 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
def rename(*) def rename(*)
# set context for the next message # set context for the next message
save_context :rename save_context :rename
reply_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
# register context handlers to handle this context # register context handlers to handle this context
context_handler :rename do |*words| context_handler :rename do |*words|
update_name words[0] update_name words[0]
reply_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
end end
# You can do it in other way: # You can do it in other way:
def rename(name = nil, *) def rename(name = nil, *)
if name if name
update_name name update_name name
reply_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
else else
save_context :rename save_context :rename
reply_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
end end
@@ -243,7 +251,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
You can use `CallbackQueyContext` in the similar way to split `#callback_query` into You can use `CallbackQueryContext` in the similar way to split `#callback_query` into
several specific methods. It doesn't require session support, and takes context from several specific methods. It doesn't require session support, and takes context from
data. If data has a prefix with colon like this `my_ctx:smth...` it'll call data. If data has a prefix with colon like this `my_ctx:smth...` it'll call
`my_ctx_callback_query('smth...')` when there is such action method. Otherwise `my_ctx_callback_query('smth...')` when there is such action method. Otherwise
@@ -318,18 +326,51 @@ To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before
initializing clients. Most likely you'll want something like this: initializing clients. Most likely you'll want something like this:
```ruby ```ruby
# environments/test.rb
# Make sure to run it before defining routes or storing bot to some place in app!
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!
# rails_helper.rb
RSpec.configure do |config| RSpec.configure do |config|
# ... # ...
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!
config.after { Telegram.bot.reset } config.after { Telegram.bot.reset }
# ... # ...
end end
``` ```
There are also some helpers for controller tests. There are integration and controller contexts for RSpec and some built-in matchers:
Check out `telegram/bot/updates_controller/rspec_helpers` and
`telegram/bot/updates_controller/testing`. ```ruby
# spec/requests/telegram_webhooks_spec.rb
require 'telegram/bot/rspec/integration'
RSpec.describe TelegramWebhooksController, :telegram_bot do
# for old rspec add:
# include_context 'telegram/bot/integration'
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'Hi there!' }
end
end
# For controller specs use
require 'telegram/bot/updates_controller/rspec_helpers'
RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do
# for old rspec add:
# include_context 'telegram/bot/updates_controller'
end
# Matchers are available for custom specs:
include Telegram::Bot::RSpec::ClientMatchers
expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option)
expect(&process_update).
to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
```
See sample app for more examples.
### Deploying ### Deploying
@@ -351,11 +392,11 @@ or just add `botan` key in `secrets.yml`:
Access to Botan client with `bot.botan`. Access to Botan client with `bot.botan`.
Use `bot.botan.track(event, uid, payload)` to track events. Use `bot.botan.track(event, uid, payload)` to track events.
There are some helpers for controllers in `Telegram::Bot::UpdatesController::Botan`: There are some helpers for controllers in `Telegram::Bot::Botan::ControllerHelpers`:
```ruby ```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::Botan include Telegram::Bot::Botan::ControllerHelpers
# This will track with event: action_name & data: payload # This will track with event: action_name & data: payload
before_action :botan_track_action before_action :botan_track_action
@@ -372,6 +413,37 @@ end
There is no stubbing for botan clients, so don't set botan token in tests. There is no stubbing for botan clients, so don't set botan token in tests.
### 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.
- Handle and retry network and other errors with queue adapter.
- ???
Instead of performing request instantly client serializes it, pushes to queue,
and immediately return control back. The job is then fetched with a worker
and real API request is performed. And this all is absolutely transparent for the app.
To enable this mode add `async: true` to bot's and botan's config.
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).
If you want async mode, but don't want to setup queue, know that Rails 5 are shipped
with Async adapter by default, and there is
[Sucker Punch](https://github.com/brandonhilkert/sucker_punch) for Rails 4.
Be aware of some limitations:
- Client will not return API response.
- Sending files is not available in async mode [now],
because them can not be serialized.
To disable async mode for the block of code use `bot.async(false) { bot.send_photo }`.
Yes, it's threadsafe too.
## Development ## Development
After checking out the repo, run `bin/setup` to install dependencies. After checking out the repo, run `bin/setup` to install dependencies.
@@ -383,6 +455,16 @@ 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, 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). 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
```
## Contributing ## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot. Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot.

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

@@ -1,9 +1,14 @@
namespace :telegram do namespace :telegram do
namespace :bot do namespace :bot do
desc 'Run poller' desc 'Run poller. It broadcasts Rails.logger to STDOUT in dev like `rails s` do. ' \
task poller: :environment do 'Use LOG_TO_STDOUT to enable/disable broadcasting.'
console = ActiveSupport::Logger.new(STDERR) task :poller do
Rails.logger.extend ActiveSupport::Logger.broadcast console 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
end
Telegram::Bot::UpdatesPoller.start(ENV['BOT'].try!(:to_sym) || :default) Telegram::Bot::UpdatesPoller.start(ENV['BOT'].try!(:to_sym) || :default)
end end

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

@@ -13,6 +13,7 @@ module Telegram
# check response. # check response.
class StaleChat < Error class StaleChat < Error
DESCRIPTIONS = [ DESCRIPTIONS = [
'Bot was blocked',
'bot was kicked', 'bot was kicked',
"can't write to", "can't write to",
'group chat is deactivated', 'group chat is deactivated',
@@ -26,10 +27,14 @@ module Telegram
end end
end end
autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan' autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client' autoload :Client, 'telegram/bot/client'
autoload :ClientStub, 'telegram/bot/client_stub' autoload :ClientStub, 'telegram/bot/client_stub'
autoload :DebugClient, 'telegram/bot/debug_client'
autoload :Initializers, 'telegram/bot/initializers'
autoload :Middleware, 'telegram/bot/middleware' autoload :Middleware, 'telegram/bot/middleware'
autoload :RSpec, 'telegram/bot/rspec'
autoload :UpdatesController, 'telegram/bot/updates_controller' autoload :UpdatesController, 'telegram/bot/updates_controller'
autoload :UpdatesPoller, 'telegram/bot/updates_poller' autoload :UpdatesPoller, 'telegram/bot/updates_poller'
end end

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

@@ -0,0 +1,162 @@
module Telegram
module Bot
# Telegram & Botan 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.
#
# 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.
# Async behavior is controlled with `#async=` writer
# and can be enabled/disabled for the block with `#async`:
#
# client = Telegram::Bot::Client.new(**config, async: true)
# client.send_message(message)
# client.async(false) { client.send_message(other_one) }
#
# `#async=` sets global value for all threads,
# 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
# 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.
module Async
# Used to track missing key in a hash in local variable.
MISSING_VALUE = Object.new.freeze
module Job
class << self
def included(base)
base.singleton_class.send :attr_accessor, :client_class
end
end
def perform(client_id, *args)
client = self.class.client_class.wrap(client_id.to_sym)
client.async(false) { client.request(*args) }
end
end
module ClassMethods
def default_async_job
@default_async_job ||= begin
begin
ApplicationJob
rescue NameError
raise 'Define ApplicationJob class or setup #async= with custom job class'
end
klass = Class.new(ApplicationJob) { include Job }
klass.client_class = self
const_set(:AsyncJob, klass)
end
end
# This is used in specs.
def default_async_job=(val)
@default_async_job = val
remove_const(:AsyncJob) if const_defined?(:AsyncJob, false)
end
# Prepares argments for async job. ActiveJob doesn't support
# Symbol in argumens. Also we can encode json bodies only once here,
# so it would not be unnecessarily serialized-deserialized.
#
# This is stub method, which returns input. Every client class
# must prepare args itself.
def prepare_async_args(*args)
args
end
# Returns default_async_job if `true` is given,
# treats String as a constant name, or bypasses any other values.
def prepare_async_val(val)
case val
when true then default_async_job
when String then Object.const_get(val)
else val
end
end
end
class << self
def prepended(base)
base.extend(ClassMethods)
end
# Transforms symbols to strings in hash values.
def prepare_hash(hash)
return hash unless hash.is_a?(Hash)
hash = hash.dup
hash.each { |key, val| hash[key] = val.to_s if val.is_a?(Symbol) }
end
# Thread-local hash to store async config for every client.
def thread_store
Thread.current[:telegram_bot_async] ||= {}
end
end
attr_reader :id
def initialize(*, id: nil, async: nil, **options)
@id = id
self.async = async
super
end
# Sets default async value for all threads.
# Uses `self.class.prepare_async_val` to prepare value.
def async=(val)
@async = self.class.prepare_async_val(val)
end
# Sets async value in a thread-safe way for the block.
# Uses `self.class.prepare_async_val` to prepare value.
#
# If no block is given returns previously set value or the global one,
# set by #async=.
def async(val = true)
thread_key = object_id
thread_store = Async.thread_store
return thread_store.fetch(thread_key) { @async } unless block_given?
begin
old_val = thread_store.fetch(thread_key) { MISSING_VALUE }
thread_store[thread_key] = self.class.prepare_async_val(val)
yield
ensure
if MISSING_VALUE == old_val
thread_store.delete(thread_key)
else
thread_store[thread_key] = old_val
end
end
end
# Uses job if #async is set.
def request(*args)
job_class = async
return super unless job_class
raise 'Can not enqueue job without client id' unless id
job_class.perform_later(id.to_s, *self.class.prepare_async_args(*args))
end
end
end
end

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

@@ -3,24 +3,37 @@ module Telegram
class Botan class Botan
TRACK_URI = 'https://api.botan.io/track'.freeze 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 class Error < Bot::Error; end
extend Initializers
prepend Async
include DebugClient 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 attr_reader :client, :token
def initialize(token) def initialize(token = nil, **options)
@client = HTTPClient.new @client = HTTPClient.new
@token = token @token = token || options[:token]
end end
def track(event, uid, payload = {}) def track(event, uid, payload = {})
res = http_request( request(:post, TRACK_URI, {name: event, uid: uid}, payload.to_json)
:post, end
TRACK_URI,
{token: token, name: event, uid: uid}, def request(method, uri, query = {}, body = nil)
payload.to_json, res = http_request(method, uri, query.merge(token: token), body)
)
status = res.status status = res.status
return JSON.parse(res.body) if 300 > status return JSON.parse(res.body) if 300 > status
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
@@ -31,6 +44,10 @@ module Telegram
def http_request(method, uri, query, body) def http_request(method, uri, query, body)
client.request(method, uri, query, body) client.request(method, uri, query, body)
end end
def inspect
"#<#{self.class.name}##{object_id}(#{@id})>"
end
end end
end end
end end

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

@@ -0,0 +1,15 @@
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,8 +1,8 @@
module Telegram module Telegram
module Bot module Bot
class UpdatesController class Botan
# Helpers for botan.io metrics. # Helpers for botan.io metrics.
module Botan module ControllerHelpers
class MissingFrom < Error; end class MissingFrom < Error; end
protected protected

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

@@ -2,7 +2,6 @@ require 'json'
require 'httpclient' require 'httpclient'
require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/keys'
require 'telegram/bot/debug_client'
module Telegram module Telegram
module Bot module Bot
@@ -10,23 +9,14 @@ module Telegram
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
autoload :TypedResponse, 'telegram/bot/client/typed_response' autoload :TypedResponse, 'telegram/bot/client/typed_response'
extend Initializers
prepend Async
prepend Botan::ClientHelpers
include DebugClient include DebugClient
class << self class << self
# Accepts different options to initialize bot. def by_id(id)
def wrap(input) Telegram.bots[id]
case input
when self then input
when Array then input.map(&method(__callee__))
when Hash then
input = input.stringify_keys
new input['token'], input['username'], botan: input['botan']
when Symbol
Telegram.bots[input] or
raise "Bot #{input} not configured, check Telegram.bots_config."
else
new(input)
end
end end
# Prepend TypedResponse module. # Prepend TypedResponse module.
@@ -41,16 +31,19 @@ module Telegram
body[k] = val.to_json if val.is_a?(Hash) || val.is_a?(Array) body[k] = val.to_json if val.is_a?(Hash) || val.is_a?(Array)
end end
end end
def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(prepare_body(body))]
end
end end
attr_reader :client, :token, :username, :base_uri, :botan attr_reader :client, :token, :username, :base_uri
def initialize(token, username = nil, botan: nil) def initialize(token = nil, username = nil, **options)
@client = HTTPClient.new @client = HTTPClient.new
@token = token @token = token || options[:token]
@username = username @username = username || options[:username]
@base_uri = format URL_TEMPLATE, token @base_uri = format URL_TEMPLATE, self.token
@botan = Botan.new(botan) if botan
end end
def request(action, body = {}) # rubocop:disable PerceivedComplexity def request(action, body = {}) # rubocop:disable PerceivedComplexity
@@ -67,15 +60,24 @@ module Telegram
raise Error, err_msg raise Error, err_msg
end end
# Splited to the sections similar to API docs.
%w( %w(
deleteWebhook
getUpdates
getWebhookInfo
setWebhook
answerCallbackQuery answerCallbackQuery
answerInlineQuery
forwardMessage forwardMessage
getChat
getChatAdministrators
getChatMember
getChatMembersCount
getFile getFile
getMe getMe
getUpdates
getUserProfilePhotos getUserProfilePhotos
kickChatMember kickChatMember
leaveChat
sendAudio sendAudio
sendChatAction sendChatAction
sendContact sendContact
@@ -87,8 +89,17 @@ module Telegram
sendVenue sendVenue
sendVideo sendVideo
sendVoice sendVoice
setWebhook
unbanChatMember unbanChatMember
editMessageCaption
editMessageReplyMarkup
editMessageText
answerInlineQuery
getGameHighScores
sendGame
setGameScore
).each do |method| ).each do |method|
define_method(method.underscore) { |*args| request(method, *args) } define_method(method.underscore) { |*args| request(method, *args) }
end end

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

@@ -9,7 +9,7 @@ module Telegram
if self == ClientStub || !ClientStub.stub_all? if self == ClientStub || !ClientStub.stub_all?
super super
else else
ClientStub.new(args[1]) ClientStub.new(*args)
end end
end end
end end
@@ -34,8 +34,8 @@ module Telegram
end end
end end
def initialize(username = nil) def initialize(token = nil, username = nil, **options)
@username = username @username = username || options[:username] || token
reset reset
end end
@@ -43,7 +43,7 @@ module Telegram
@requests = Hash.new { |h, k| h[k] = [] } @requests = Hash.new { |h, k| h[k] = [] }
end end
def request(action, body) def request(action, body = {})
requests[action.to_sym] << body requests[action.to_sym] << body
end end
end end

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

@@ -14,14 +14,21 @@ module Telegram
# It just tells routes helpers whether to add routed bots to # It just tells routes helpers whether to add routed bots to
# Bot::UpdatesPoller, so their config will be available by bot key in # Bot::UpdatesPoller, so their config will be available by bot key in
# Bot::UpdatesPoller.start. # Bot::UpdatesPoller.start.
#
# It's enabled by default in Rails dev environment and `rake telegram:bot:poller`
# task. Use `BOT_POLLER_MODE=true` envvar to set it manually.
def bot_poller_mode? def bot_poller_mode?
return @bot_poller_mode if defined?(@bot_poller_mode) return @bot_poller_mode if defined?(@bot_poller_mode)
Rails.env.development? if defined?(Rails) @bot_poller_mode = ENV.fetch('BOT_POLLER_MODE') do
Rails.env.development? if defined?(Rails.env)
end
end end
# Hash of bots made with bots_config. # Hash of bots made with bots_config.
def bots def bots
@bots ||= bots_config.transform_values(&Client.method(:wrap)) @bots ||= bots_config.each_with_object({}) do |(id, config), h|
h[id] = Client.wrap(config, id: id)
end
end end
# Default bot. # Default bot.
@@ -29,17 +36,26 @@ module Telegram
@bot ||= bots[:default] @bot ||= bots[:default]
end end
# Hash of botan clients made from #bots.
def botans
@botans ||= bots.transform_values(&:botan)
end
# Returns config for .bots method. By default uses `telegram['bots']` section # Returns config for .bots method. By default uses `telegram['bots']` section
# from `secrets.yml` merging `telegram['bot']` at `:default` key. # from `secrets.yml` merging `telegram['bot']` at `:default` key.
# #
# Can be overwritten with .bots_config= # Can be overwritten with .bots_config=
def bots_config def bots_config
return @bots_config if @bots_config @bots_config ||=
telegram_config = Rails.application.secrets[:telegram] if defined?(Rails.application)
(telegram_config['bots'] || {}).symbolize_keys.tap do |config| telegram_config = Rails.application.secrets[:telegram] || {}
default = telegram_config['bot'] (telegram_config['bots'] || {}).symbolize_keys.tap do |config|
config[:default] = default if default default = telegram_config['bot']
end config[:default] = default if default
end
else
{}
end
end end
# Resets all cached bots and their configs. # Resets all cached bots and their configs.
@@ -47,6 +63,7 @@ module Telegram
@bots = nil @bots = nil
@bot = nil @bot = nil
@bots_config = nil @bots_config = nil
@botans = nil
end end
end end
end end

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

@@ -0,0 +1,19 @@
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,6 +1,8 @@
require 'active_support/concern' 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/mime_type'
require 'action_dispatch/middleware/params_parser' require 'action_dispatch/http/request'
module Telegram module Telegram
module Bot module Bot
@@ -13,7 +15,8 @@ module Telegram
end end
def call(env) def call(env)
update = env['action_dispatch.request.request_parameters'] request = ActionDispatch::Request.new(env)
update = request.request_parameters
controller.dispatch(bot, update) controller.dispatch(bot, update)
[200, {}, ['']] [200, {}, ['']]
end end

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

@@ -20,7 +20,7 @@ module Telegram
# Replaces colon with underscore so rails don't treat it as # Replaces colon with underscore so rails don't treat it as
# route parameter. # route parameter.
def escape_token(token) def escape_token(token)
token.tr(':', '_') token && token.tr(':', '_')
end end
end end

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

@@ -0,0 +1,7 @@
module Telegram
module Bot
module RSpec
autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers'
end
end
end

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

@@ -0,0 +1,151 @@
module Telegram
module Bot
module RSpec
# Proxy that uses RSpec::Mocks::ArgListMatcher when it's available.
# Otherwise just performs `#==` match.
#
# Also allows to check argumets with custom block.
class ArgListMatcher
attr_reader :expected, :expected_proc
def initialize(*args, &block)
@expected_proc = block if block_given?
@expected =
if mocks_matcher?
::RSpec::Mocks::ArgumentListMatcher.new(*args)
else
args
end
end
def args_match?(*actual)
if expected_proc
expected_proc[*actual]
true
elsif mocks_matcher?
expected.args_match?(*actual)
else
expected == actual
end
end
def args
mocks_matcher? ? expected.args : expected
end
def mocks_matcher?
defined?(::RSpec::Mocks::ArgumentListMatcher)
end
def to_s
if mocks_matcher?
expected.expected_args.inspect
elsif expected_proc
'(proc matcher)'
else
expected.inspect
end
end
end
# Matchers to test requests to Telegram API.
#
# Complex matchers requires `rspec-mocks` to be installed.
module ClientMatchers
class MakeTelegramRequest < ::RSpec::Matchers::BuiltIn::BaseMatcher
EXPECTATION_TYPES = {
exactly: :==,
at_most: :>=,
at_least: :<=,
}.freeze
attr_reader :performed_requests, :description
def initialize(bot, action, description: nil)
@bot = bot
@action = action
@description = description || "make #{action} telegram request"
exactly(1)
end
def matches?(proc) # rubocop:disable AbcSize
raise ArgumentError, 'matcher only supports block expectations' unless proc.is_a?(Proc)
original_requests_count = bot.requests[action].count
proc.call
@performed_requests = bot.requests[action].drop(original_requests_count)
@matching_requests_count = performed_requests.count do |request|
!arg_list_matcher || arg_list_matcher.args_match?(request)
end
expectation_method = EXPECTATION_TYPES[expectation_type]
expected_number.public_send(expectation_method, matching_requests_count)
end
def with(*args, &block)
@arg_list_matcher = ArgListMatcher.new(*args, &block)
self
end
EXPECTATION_TYPES.each_key do |type|
define_method type do |count|
@expectation_type = type
@expected_number = Integer(count)
self
end
end
def times
self
end
def failure_message
"expected to #{base_message}"
end
def failure_message_when_negated
"expected not to #{base_message}"
end
def supports_block_expectations?
true
end
private
attr_reader :bot, :action, :expectation_type, :expected_number,
:arg_list_matcher, :matching_requests_count
def base_message
"make #{expectation_type.to_s.tr('_', ' ')} #{expected_number} " \
"#{bot.inspect}.#{action} requests,".tap do |msg|
msg << " with #{arg_list_matcher}," if arg_list_matcher
msg << " but made #{matching_requests_count}"
if performed_requests
actual_args = performed_requests.map(&:inspect).join(', ')
msg << ", and #{performed_requests.count} with #{actual_args}"
end
end
end
end
# Check that bot performed request to telegram API:
#
# expect { dispatch_message('Hi!') }.
# to make_telegram_request(bot, :sendMessage).
# with(text: 'Hello!', chat_id: chat_id)
def make_telegram_request(bot, action)
MakeTelegramRequest.new(bot, action)
end
# Helper for asserting message is sent. Note that options are checked
# with `hash_including`. For strict checks use #make_telegram_request.
def send_telegram_message(bot, text = nil, options = {})
description = "send telegram message #{text.inspect}"
text = a_string_matching(text) if text.is_a?(Regexp)
options = options.merge(text: text) if text
MakeTelegramRequest.new(bot, :sendMessage, description: description).
with(hash_including(options))
end
end
end
end
end

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

@@ -0,0 +1,49 @@
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

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

@@ -21,7 +21,7 @@ module Telegram
# end # end
# #
# def help(*) # def help(*)
# reply_with :message, text: # respond_with :message, text:
# end # end
# #
# To process plain text messages (without commands) or other updates just # To process plain text messages (without commands) or other updates just
@@ -29,7 +29,7 @@ module Telegram
# as an argument. # as an argument.
# #
# def message(message) # def message(message)
# reply_with :message, text: "Echo: #{message['text']}" # respond_with :message, text: "Echo: #{message['text']}"
# end # end
# #
# def inline_query(query) # def inline_query(query)
@@ -50,20 +50,19 @@ module Telegram
# ControllerClass.new(bot, from: telegram_user, chat: telegram_chat). # ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
# process(:help, *args) # process(:help, *args)
# #
class UpdatesController < AbstractController::Base # rubocop:disable ClassLength class UpdatesController < AbstractController::Base
abstract! abstract!
require 'telegram/bot/updates_controller/session' require 'telegram/bot/updates_controller/session'
require 'telegram/bot/updates_controller/log_subscriber' require 'telegram/bot/updates_controller/log_subscriber'
require 'telegram/bot/updates_controller/instrumentation' require 'telegram/bot/updates_controller/instrumentation'
require 'telegram/bot/updates_controller/reply_helpers' require 'telegram/bot/updates_controller/reply_helpers'
autoload :CallbackQueyContext, 'telegram/bot/updates_controller/callback_query_context' autoload :CallbackQueryContext, 'telegram/bot/updates_controller/callback_query_context'
autoload :MessageContext, 'telegram/bot/updates_controller/message_context' autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
autoload :Botan, 'telegram/bot/updates_controller/botan'
include AbstractController::Callbacks include AbstractController::Callbacks
# Redefine callbacks with default terminator. # Redefine callbacks with default terminator.
if ActiveSupport.gem_version >= Gem::Version.new('5') if ActiveSupport::VERSION::MAJOR >= 5
define_callbacks :process_action, define_callbacks :process_action,
skip_after_callbacks_if_terminated: true skip_after_callbacks_if_terminated: true
else else
@@ -81,10 +80,12 @@ module Telegram
PAYLOAD_TYPES = %w( PAYLOAD_TYPES = %w(
message message
edited_message
channel_post
edited_channel_post
inline_query inline_query
chosen_inline_result chosen_inline_result
callback_query callback_query
edited_message
).freeze ).freeze
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)") CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
@@ -141,10 +142,12 @@ module Telegram
@_payload, @_payload_type = payload_data @_payload, @_payload_type = payload_data
end end
# Accessor to `'chat'` field of payload. Can be overriden with `chat` option # Accessor to `'chat'` field of payload. Also tries `'chat'` in `'message'`
# for #initialize. # when there is no such field in payload.
#
# Can be overriden with `chat` option for #initialize.
def chat def chat
@_chat ||= payload && payload['chat'] @_chat ||= payload.try! { |x| x['chat'] || x['message'] && x['message']['chat'] }
end end
# Accessor to `'from'` field of payload. Can be overriden with `from` option # Accessor to `'from'` field of payload. Can be overriden with `from` option

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

@@ -3,7 +3,7 @@ module Telegram
class UpdatesController class UpdatesController
# Use separate actions for different callback queries. # Use separate actions for different callback queries.
# It doesn't require session support. Simply add `%{context}:` prefix to data. # It doesn't require session support. Simply add `%{context}:` prefix to data.
module CallbackQueyContext module CallbackQueryContext
protected protected
# Uses #context_from_callback_query as context name. # Uses #context_from_callback_query as context name.

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

@@ -6,7 +6,7 @@ module Telegram
module Instrumentation module Instrumentation
class << self class << self
def prepended(base) def prepended(base)
base.config_accessor :logger base.send :config_accessor, :logger
base.extend ClassMethods base.extend ClassMethods
end end
@@ -35,13 +35,13 @@ module Telegram
end end
end end
def reply_with(type, *) def respond_with(type, *)
Instrumentation.instrument(:reply_with, type: type) { super } Instrumentation.instrument(:respond_with, type: type) { super }
end 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| define_method(type) do |*args|
Instrumentation.instrument(:reply_with, type: type) { super(*args) } Instrumentation.instrument(:respond_with, type: type) { super(*args) }
end end
end end

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

@@ -22,8 +22,8 @@ module Telegram
end end
end end
def reply_with(event) def respond_with(event)
info { "Replied with #{event.payload[:type]}" } info { "Responded with #{event.payload[:type]}" }
end end
def halted_callback(event) def halted_callback(event)

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

@@ -2,21 +2,23 @@ module Telegram
module Bot module Bot
class UpdatesController class UpdatesController
module ReplyHelpers module ReplyHelpers
# Helper to call bot's `send_#{type}` method with already set `chat_id` and # Helper to call bot's `send_#{type}` method with already set `chat_id`:
# `reply_to_message_id`:
# #
# reply_with :message, text: 'Hello!' # respond_with :message, text: 'Hello!'
# reply_with :message, text: '__Hello!__', parse_mode: :Markdown # respond_with :message, text: '__Hello!__', parse_mode: :Markdown
# reply_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!" # respond_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!"
def reply_with(type, params) def respond_with(type, params)
method = "send_#{type}"
chat = self.chat chat = self.chat
chat_id = chat && chat['id'] or raise 'Can not respond_with when chat is not present'
bot.public_send("send_#{type}", params.merge(chat_id: chat_id))
end
# Same as respond_with but also sets `reply_to_message_id`.
def reply_with(type, params)
payload = self.payload payload = self.payload
params = params.merge( message_id = payload && payload['message_id']
chat_id: (chat && chat['id'] or raise 'Can not reply_with when chat is not present'), params = params.merge(reply_to_message_id: message_id) if message_id
reply_to_message: payload && payload['message_id'], respond_with(type, params)
)
bot.public_send(method, params)
end end
# Same as reply_with, but for inline queries. # Same as reply_with, but for inline queries.

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

@@ -13,13 +13,18 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
let(:bot) { Telegram::Bot::ClientStub.new(bot_name) } let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
let(:bot_name) { 'bot' } let(:bot_name) { 'bot' }
let(:session) { controller.send(:session) } let(:session) { controller.send(:session) }
let(:from_id) { 123 }
let(:chat_id) { 456 }
let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} }
include Telegram::Bot::RSpec::ClientMatchers
def dispatch(bot = self.bot, update = self.update) def dispatch(bot = self.bot, update = self.update)
controller.dispatch_again(bot, update) controller.dispatch_again(bot, update)
end end
def dispatch_message(text, options = {}) def dispatch_message(text, options = {})
update = build_update :message, options.merge(text: text) update = build_update :message, default_message_options.merge(options).merge(text: text)
dispatch bot, update dispatch bot, update
end end
@@ -34,4 +39,15 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
else input else input
end end
end end
# Matcher to check response. Make sure to define `let(:chat_id)`.
def respond_with_message(expected)
send_telegram_message(bot, expected, chat_id: chat_id)
end
end
RSpec.configure do |config|
if config.respond_to?(:include_context)
config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
end
end end

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

@@ -27,12 +27,12 @@ module Telegram
attr_reader :bot, :controller, :timeout, :offset, :logger, :running, :reload attr_reader :bot, :controller, :timeout, :offset, :logger, :running, :reload
def initialize(bot, controller, **options) def initialize(bot, controller, **options)
@logger = options.fetch(:logger) { defined?(Rails) && Rails.logger } @logger = options.fetch(:logger) { defined?(Rails.logger) && Rails.logger }
@bot = bot @bot = bot
@controller = controller @controller = controller
@timeout = options.fetch(:timeout) { DEFAULT_TIMEOUT } @timeout = options.fetch(:timeout) { DEFAULT_TIMEOUT }
@offset = options[:offset] @offset = options[:offset]
@reload = options.fetch(:reload) { defined?(Rails) && Rails.env.development? } @reload = options.fetch(:reload) { defined?(Rails.env) && Rails.env.development? }
end end
def log(&block) def log(&block)
@@ -64,7 +64,7 @@ module Telegram
end end
def fetch_updates def fetch_updates
response = bot.get_updates(offset: offset, timeout: timeout) response = bot.async(false) { bot.get_updates(offset: offset, timeout: timeout) }
return unless response['ok'] && response['result'].any? return unless response['ok'] && response['result'].any?
reload! do reload! do
response['result'].each do |update| response['result'].each do |update|
@@ -77,11 +77,25 @@ module Telegram
def reload! def reload!
return yield unless reload return yield unless reload
ActionDispatch::Reloader.prepare! reloading_code do
if controller.is_a?(Class) && controller.name if controller.is_a?(Class) && controller.name
@controller = Object.const_get(controller.name) @controller = Object.const_get(controller.name)
end
yield
end
end
if defined?(Rails.application) && Rails.application.respond_to?(:reloader)
def reloading_code
Rails.application.reloader.wrap do
yield
end
end
else
def reloading_code
ActionDispatch::Reloader.prepare!
yield.tap { ActionDispatch::Reloader.cleanup! }
end end
yield.tap { ActionDispatch::Reloader.cleanup! }
end end
end end
end end

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

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

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

@@ -17,6 +17,8 @@ require 'telegram/bot'
require 'telegram/bot/updates_controller/rspec_helpers' require 'telegram/bot/updates_controller/rspec_helpers'
require 'telegram/bot/types' require 'telegram/bot/types'
Dir[GEM_ROOT.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config| RSpec.configure do |config|
config.expect_with :rspec do |expectations| config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description` # This option will default to `true` in RSpec 4. It makes the `description`

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

@@ -0,0 +1,143 @@
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 }
let!(:application_job_class) do
klass = Class.new do
def self.perform_later(*)
end
end
klass.tap { |x| stub_const('ApplicationJob', x) }
end
after { described_class.default_async_job = nil }
describe '#async' do
subject { ->(*args, &block) { instance.async(*args, &block) } }
its(:call) { should eq described_class.default_async_job }
context 'when async is disabled' do
let(:async) { false }
its(:call) { should eq false }
end
context 'when using with block' do
it 'sets value inside block' do
expect do
expect do
subject.call(false) do
expect do
subject.call(nil) { expect(subject[]).to eq nil }
end.to_not change(&subject).from false
raise 'TestError'
end
end.to raise_error(/TestError/)
end.to_not change(instance, :async).from(described_class.default_async_job)
end
end
context 'in multi-threaded environment' do
it 'changes only in current thread' do
thread = nil
expect do
thread = Thread.new do
expect do
subject.call(false) do
expect(subject[]).to eq false
end
end.to_not change(instance, :async).from(described_class.default_async_job)
sleep 0.2
end
end.to_not change(instance, :async).from(described_class.default_async_job)
thread.join
end
it 'uses default value after block' do
thread = Thread.new do
subject.call(false) {}
expect { sleep 0.2 }.to change(instance, :async).
from(described_class.default_async_job).to(nil)
end
sleep 0.1
instance.async = nil
thread.join
end
end
end
describe '#async=' do
subject { ->(val = new_val) { instance.async = val } }
let(:async) {}
context 'when true is given' do
let(:new_val) { true }
it { should change(instance, :async).to described_class.default_async_job }
end
context 'when false is given' do
let(:new_val) { false }
it { should change(instance, :async).to false }
end
context 'when string is given' do
let(:new_val) { 'Telegram::Bot::Client' }
it { should change(instance, :async).to Object.const_get(new_val) }
end
context 'in multi-threaded environment' do
subject { ->(val = new_val) { Thread.new { instance.async = val }.join } }
let(:new_val) { true }
it { should change(instance, :async).to described_class.default_async_job }
end
end
describe '.default_async_job' do
subject { described_class.default_async_job }
its(:superclass) { should eq application_job_class }
it { should include Telegram::Bot::Async::Job }
its(:client_class) { should eq described_class }
context 'when ApplicationJob is not defined' do
let(:application_job_class) {}
it { expect { subject }.to raise_error(/Define ApplicationJob/) }
end
end
describe '#request' do
subject { ->(*args) { instance.request(*(args.empty? ? self.args : args)) } }
let(:args, &request_args)
let(:result) { double(:result) }
shared_examples 'enqueues job' do
it 'enqueues job' do
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
end
end
include_examples 'enqueues job'
context 'with custom job class' do
let(:async) { double(:job_class) }
include_examples 'enqueues job'
end
context 'when id is not set' do
let(:id) {}
it { should raise_error(/Can not enqueue/) }
end
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'
end
end
end
end

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

@@ -0,0 +1,65 @@
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

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

@@ -0,0 +1,28 @@
RSpec.describe Telegram::Bot::Async::Job do
let(:job_class) do
described_class = self.described_class
client_class = self.client_class
Class.new do
include described_class
self.client_class = client_class
end
end
let(:client_class) { Telegram::Bot::Client }
let(:instance) { job_class.new }
describe '#perform' do
subject { instance.perform(id, *args) }
let(:id) { 'bot_id' }
let(:args) { [double(:action), {body: :content}] }
let(:client) { Telegram::Bot::Client.new(async: custom_job_class) }
let(:custom_job_class) { Class.new }
let(:result) { double(status: 200, body: '{"test":"ok"}') }
it 'finds client and performs request' do
expect(client_class).to receive(:wrap).with(id.to_sym) { client }
expect(client).to receive(:request).with(*args).and_call_original
expect(client).to receive(:http_request) { result }
should eq 'test' => 'ok'
end
end
end

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

@@ -0,0 +1,33 @@
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,4 +1,4 @@
RSpec.describe Telegram::Bot::UpdatesController::Botan do RSpec.describe Telegram::Bot::Botan::ControllerHelpers do
include_context 'telegram/bot/updates_controller' include_context 'telegram/bot/updates_controller'
let(:controller_class) do let(:controller_class) do
described_class = self.described_class described_class = self.described_class

27
spec/telegram/bot/botan_spec.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,27 @@
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

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

@@ -3,71 +3,8 @@ RSpec.describe Telegram::Bot::Client do
let(:token) { 'token' } let(:token) { 'token' }
let(:botan_token) { double(:botan_token) } let(:botan_token) { double(:botan_token) }
describe '.wrap' do include_examples 'initializers'
subject { described_class.wrap(input) } include_examples 'async', request_args: -> { [double(:action), {body: :content}] }
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
end
context 'when input is a hash' do
let(:input) { {token: token, username: username, ignore: :ignore} }
it 'extracts token & username' do
expect(described_class).to receive(:new).
with(token, username, botan: nil) { result }
should eq result
end
context 'when `botan` is given' do
let(:input) { super().merge(botan: botan_token) }
it 'passes it to initializer' do
expect(described_class).to receive(:new).
with(token, username, botan: botan_token) { 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) { :bot_1 }
before { allow(Telegram).to receive(:bots) { {bot_1: instance} } }
it { should eq Telegram.bots[:bot_1] }
context 'and there is no such bot' do
let(:input) { :invalid }
it { expect { subject }.to raise_error(/not configured/) }
end
end
context 'when input is an array' do
let!(:input) { ['other_token', instance, token: token, username: username] }
let(:result_2) { double(:result_2) }
it 'calls wrap for every element' do
expect(described_class).to receive(:new).with('other_token') { result }
expect(described_class).to receive(:new).with(token, username, botan: nil) { result_2 }
should eq [result, instance, result_2]
end
end
end
describe '.prepare_body' do describe '.prepare_body' do
subject { described_class.prepare_body(input) } subject { described_class.prepare_body(input) }
@@ -88,14 +25,27 @@ RSpec.describe Telegram::Bot::Client do
end end
end end
describe '#botan' do describe '.prepare_async_args' do
subject { instance.botan } subject { described_class.prepare_async_args(*input) }
it { should eq nil } let(:input) { [:action, a: 1, b: :sym, c: [:other], 'd' => 'str'] }
it { should eq ['action', a: 1, b: 'sym', c: '["other"]', 'd' => 'str'] }
end
context 'when botan token is set' do describe '.new' do
let(:instance) { described_class.new token, botan: botan_token } subject { described_class.new(*args) }
it { should be_instance_of Telegram::Bot::Botan }
its(:token) { should eq botan_token } 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] }
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] }
end end
end end
end end

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

@@ -1,7 +1,7 @@
RSpec.describe Telegram::Bot::ClientStub do RSpec.describe Telegram::Bot::ClientStub do
describe '#stub_all!' do describe '#stub_all!' do
let(:client) { Telegram::Bot::Client.new('token', 'bot_name') } let(:client) { Telegram::Bot::Client.new('token', 'bot_name') }
let(:clients) { Telegram::Bot::Client.wrap(['token', token: 'token2']) } let(:clients) { ['token', token: 'token2'].map(&Telegram::Bot::Client.method(:wrap)) }
shared_examples 'constructors' do |expected_class| shared_examples 'constructors' do |expected_class|
it 'makes Client.new return ClientStub' do it 'makes Client.new return ClientStub' do
@@ -9,7 +9,7 @@ RSpec.describe Telegram::Bot::ClientStub do
expect(client.username).to eq 'bot_name' expect(client.username).to eq 'bot_name'
end end
it 'makes Client.wrap raturn ClientStub' do it 'makes Client.wrap return ClientStub' do
expect(clients).to contain_exactly instance_of(expected_class), expect(clients).to contain_exactly instance_of(expected_class),
instance_of(expected_class) instance_of(expected_class)
end end
@@ -35,4 +35,19 @@ RSpec.describe Telegram::Bot::ClientStub do
include_examples 'constructors', Telegram::Bot::Client include_examples 'constructors', Telegram::Bot::Client
end end
end end
describe '#new' do
subject { described_class.new(*args) }
context 'when only username is given' do
let(:args) { 'superbot' }
its(:username) { should eq args }
end
context 'when username and token are given' do
let(:args) { %w(token superbot) }
its(:token) { should eq nil }
its(:username) { should eq args[1] }
end
end
end end

94
spec/telegram/bot/config_methods_spec.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,94 @@
RSpec.describe Telegram::Bot::ConfigMethods do
let(:registry) do
Object.new.tap do |x|
x.extend described_class
x.bots_config = config
end
end
let(:config) do
{
default: 'default_token',
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] }
end
describe '#bots' do
context 'configured by token' do
subject { registry.bots[:default] }
its(:id) { should eq :default }
its(:token) { should eq config[:default] }
end
context 'configured by hash' do
subject { registry.bots[:chat] }
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
subject { registry.bots[:other_chat] }
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
describe '#bots_config' do
subject { registry.bots_config }
it { should eq config }
context 'when not configured' do
let(:registry) { Object.new.tap { |x| x.extend described_class } }
it { should eq({}) }
context 'in rails environment' do
before { stub_const('Rails', double(application: double(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
{
bot: double(:bot_config),
bots: {
chat: double(:chat_config),
other_chat: double(:other_chat_config),
},
}
end
it { should include default: config[:bot] }
it { should include config[:bots] }
end
end
end
end
end

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

@@ -1,3 +1,5 @@
require 'rack/mock'
RSpec.describe Telegram::Bot::Middleware do RSpec.describe Telegram::Bot::Middleware do
let(:instance) { described_class.new bot, controller } let(:instance) { described_class.new bot, controller }
let(:bot) { double(:bot) } let(:bot) { double(:bot) }
@@ -6,10 +8,25 @@ RSpec.describe Telegram::Bot::Middleware do
describe '#call' do describe '#call' do
subject { instance.call(env) } subject { instance.call(env) }
let(:env) { {'action_dispatch.request.request_parameters' => json_body} } let(:env) { {'action_dispatch.request.request_parameters' => json_body} }
let(:json_body) { double(:json_body) } let(:update) { {'message' => {'id' => 1}} }
let(:env) do
Rack::MockRequest.env_for('/',
method: :post,
input: JSON.dump(update),
'CONTENT_TYPE' => 'application/json',
)
end
require 'action_pack/version'
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 it 'calls dispatch on controller' do
expect(controller).to receive(:dispatch).with(bot, json_body) expect(controller).to receive(:dispatch).with(bot, update)
subject subject
end end

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

@@ -50,8 +50,9 @@ RSpec.describe Telegram::Bot::RoutesHelper do
def assert_routes(*expected) # rubocop:disable AbcSize def assert_routes(*expected) # rubocop:disable AbcSize
expected.each do |(bot, controller, route_name, options)| 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(mapper).to receive(:post) do |path, params|
expect(path).to eq "telegram/#{bot.token}" expect(path).to eq expected_path
middleware = params[:to] middleware = params[:to]
expect(middleware.controller).to eq(controller) expect(middleware.controller).to eq(controller)
expect(middleware.bot.token).to eq(bot.token) expect(middleware.bot.token).to eq(bot.token)
@@ -70,6 +71,27 @@ RSpec.describe Telegram::Bot::RoutesHelper do
assert_routes [bot, controller, 'default_telegram_webhook', option: :val], assert_routes [bot, controller, 'default_telegram_webhook', option: :val],
[other_bot, controller, 'other_telegram_webhook', option: :val] [other_bot, controller, 'other_telegram_webhook', option: :val]
end end
context 'and bot does not have configured token' do
let(:bot) { create_bot(nil) }
it 'creates routes for every bot and this controller' do
assert_routes [bot, controller, 'default_telegram_webhook', option: :val],
[other_bot, controller, 'other_telegram_webhook', option: :val]
end
end
context 'and bot has colon in token' do
let(:bot) { create_bot('some:token') }
it 'replaces colon with underscore' do
assert_routes [
bot,
controller,
'default_telegram_webhook',
option: :val,
path: 'telegram/some_token',
], [other_bot, controller, 'other_telegram_webhook', option: :val]
end
end
end end
context 'when called with hash' do context 'when called with hash' do

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

@@ -0,0 +1,87 @@
RSpec.describe Telegram::Bot::RSpec::ClientMatchers do
let(:bot) { Telegram::Bot::ClientStub.new('TestBot') }
let(:other_bot) { Telegram::Bot::ClientStub.new('OtherTestBot') }
include described_class
describe '#make_telegram_request' do
context 'without args' do
it 'works for matching' do
block = ->(*) { bot.send_message(text: 'test') }
expect(&block).to make_telegram_request(bot, :sendMessage)
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).exactly(3).times
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).at_least(2).times
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).at_most(4).times
end
it 'works for not-matching' do
block = ->(*) { bot.get_me }
expect {}.to_not make_telegram_request(bot, :getMe)
expect { other_bot.get_me }.to_not make_telegram_request(bot, :getMe)
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe)
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).exactly(2).times
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).exactly(4).times
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).at_least(4).times
expect { 3.times(&block) }.to_not make_telegram_request(bot, :getMe).at_most(2).times
end
end
context 'with args' do
it 'works for exact matching' do
args = {text: 'test', parseMode: :Markdown}
block = ->(*) { bot.send_message(args) }
expect(&block).to make_telegram_request(bot, :sendMessage).with(args)
expect(&block).to_not make_telegram_request(bot, :sendMessage).with(args.except(:text))
expect { 3.times(&block) }.to make_telegram_request(bot, :sendMessage).
with(args).exactly(3).times
end
it 'works for block matchers' do
args = {text: 'test', parseMode: :Markdown}
block = ->(*) { bot.send_message(args) }
expect(&block).to make_telegram_request(bot, :sendMessage).
with { |actual| expect(actual).to eq(args) }
# It ignores block's result! Custom expectations must be used inside block.
expect(&block).to make_telegram_request(bot, :sendMessage).
with { |actual| actual == {} }
expect do
expect(&block).to_not make_telegram_request(bot, :sendMessage).
with { |actual| expect(actual).to eq({}) }
end.to raise_error RSpec::Expectations::ExpectationNotMetError
n = -1
expect { 3.times { |i| bot.send_message text: i } }.
to make_telegram_request(bot, :sendMessage).exactly(3).times.
with { |actual| expect(actual).to eq(text: n += 1) }
end
it 'works for RSpec::Mocks matchers' do
args = {text: 'test', parseMode: :Markdown}
block = ->(*) { bot.send_message(args) }
expect(&block).to make_telegram_request(bot, :sendMessage).with(hash_including(args))
expect(&block).to make_telegram_request(bot, :sendMessage).with(
text: a_string_matching(/est$/),
parseMode: :Markdown,
)
end
end
end
describe '#send_telegram_message' do
let(:args) { {text: 'test', parseMode: :Markdown} }
let(:block) { ->(*) { bot.send_message(args) } }
it 'works for matching' do
expect(&block).to send_telegram_message(bot, 'test')
expect(&block).to send_telegram_message(bot, /^tes/)
expect(&block).to send_telegram_message(bot, 'test', parseMode: :Markdown)
expect(&block).to send_telegram_message(bot, a_string_including('es'))
end
it 'works for not-matching' do
expect(&block).to_not send_telegram_message(bot, 'test!')
expect(&block).to_not send_telegram_message(bot, /tes$/)
expect(&block).to_not send_telegram_message(bot, 'test', parseMode: :HTML)
expect(&block).to_not send_telegram_message(bot, a_string_including('smth'))
end
end
end

71
spec/telegram/bot/rspec/integration_spec.rb Обычный файл
Просмотреть файл

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

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

@@ -1,4 +1,4 @@
RSpec.describe Telegram::Bot::UpdatesController::CallbackQueyContext do RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do
include_context 'telegram/bot/updates_controller' include_context 'telegram/bot/updates_controller'
let(:controller_class) do let(:controller_class) do
described_class = self.described_class described_class = self.described_class

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

@@ -1,36 +1,49 @@
RSpec.describe Telegram::Bot::UpdatesController do RSpec.describe Telegram::Bot::UpdatesController do
include_context 'telegram/bot/updates_controller' include_context 'telegram/bot/updates_controller'
let(: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)} }
describe '#reply_with' do shared_examples 'missing chat' do
subject { controller.reply_with type, params }
let(:params) { {arg: 1, 'other_arg' => 2} }
let(:type) { :photo }
let(:result) { double(:result) }
let(:payload_type) { :message }
let(:payload) { {message_id: double(:message_id)} }
let(:chat) { {'id' => double(:chat_id)} }
it 'sets chat_id & reply_to_message' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{type}").with(params.merge(
chat_id: chat['id'],
reply_to_message: payload[:message_id],
)) { result }
should eq result
end
context 'when chat is missing' do context 'when chat is missing' do
let(:payload_type) { :some_type } let(:payload_type) { :some_type }
it { expect { subject }.to raise_error(/chat/) } it { expect { subject }.to raise_error(/chat/) }
end end
end
describe '#respond_with' do
subject { controller.respond_with respond_type, params }
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 }
should eq result
end
end
describe '#reply_with' do
subject { controller.reply_with respond_type, params }
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'],
reply_to_message_id: payload[:message_id],
)) { result }
should eq result
end
context 'when update is not set' do context 'when update is not set' do
let(:update) { {chat: chat} } let(:update) { {chat: chat} }
it 'sets chat_id & reply_to_message' do it 'sets chat_id' do
expect(bot).to receive("send_#{type}").with(params.merge( expect(bot).to receive("send_#{respond_type}").
chat_id: chat['id'], with(params.merge(chat_id: chat['id'])) { result }
reply_to_message: nil,
)) { result }
should eq result should eq result
end end
end end

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

@@ -225,12 +225,12 @@ RSpec.describe Telegram::Bot::UpdatesController do
it { should change(controller, :acted).to true } it { should change(controller, :acted).to true }
its(:call) { should eq [nil, nil, args] } its(:call) { should eq [nil, nil, args] }
context 'when callback returns false' do context 'when callback halts chain' do
before do before do
controller_class.prepend(Module.new do controller_class.prepend(Module.new do
def hook def hook
super super
false ActiveSupport::VERSION::MAJOR >= 5 ? throw(:abort) : false
end end
end) end)
end end
@@ -293,4 +293,49 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
end end
end end
describe '#chat' do
subject { controller.chat }
let(:payload_type) { :message }
let(:payload) { {chat: 'test_value'} }
it { should eq payload[:chat] }
context 'when payload is not set' do
let(:payload) {}
it { should eq nil }
end
context 'when payload has no such field' do
let(:payload) { {smth: 'other'} }
it { should eq nil }
context 'but has `message`' do
let(:payload) { {message: message} }
let(:message) { {text: 'Hello bot!'} }
it { should eq nil }
context 'with `chat` set' do
let(:message) { super().merge(chat: 'test value') }
it { should eq message[:chat] }
end
end
end
end
describe '#from' do
subject { controller.from }
let(:payload_type) { :message }
let(:payload) { {from: 'test_value'} }
it { should eq payload[:from] }
context 'when payload is not set' do
let(:payload) {}
it { should eq nil }
end
context 'when payload has no such field' do
let(:payload) { {smth: 'other'} }
it { should eq nil }
end
end
end end

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

@@ -17,6 +17,9 @@ RSpec.describe Telegram::Bot do
expect(subject.call( expect(subject.call(
'description' => 'Bad request: group chat is deactivated' 'description' => 'Bad request: group chat is deactivated'
)).to eq true )).to eq true
expect(subject.call(
'description' => 'Forbidden: Bot was blocked by the user'
)).to eq true
end end
end end
end end

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

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