зеркало из
https://github.com/glebtv/telegram-bot.git
synced 2026-09-04 02:05:52 +03:00
Сравнить коммиты
7 Коммитов
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
da93abaaf7 | ||
|
|
0ae17fb34e | ||
|
|
7e41bd4f9b | ||
|
|
e529a79339 | ||
|
|
ef4f3caa5f | ||
|
|
df7039326d | ||
|
|
4f47506e22 |
@@ -1,5 +1,12 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
|
# 0.12.1
|
||||||
|
|
||||||
|
- Fix `set_webhook` rake task for async bots with self-issued certificates.
|
||||||
|
- Make `#session` raise error when store is not configured,
|
||||||
|
don't use Rails.cache as fallback for session_store.
|
||||||
|
- Allow use different sessions for MessageContext.
|
||||||
|
|
||||||
# 0.12.0
|
# 0.12.0
|
||||||
|
|
||||||
- New API methods and payload types (up to Bot API 3.2).
|
- New API methods and payload types (up to Bot API 3.2).
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -24,7 +24,7 @@ Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bo
|
|||||||
with session, keyboards and inline queries.
|
with session, keyboards and inline queries.
|
||||||
Run it on your local machine in 1 minute!
|
Run it on your local machine in 1 minute!
|
||||||
|
|
||||||
And here is [app teamplate](https://github.com/telegram-bot-rb/rails_template)
|
And here is [app template](https://github.com/telegram-bot-rb/rails_template)
|
||||||
to generate clean app in seconds.
|
to generate clean app in seconds.
|
||||||
|
|
||||||
Examples and cookbook in [wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki).
|
Examples and cookbook in [wiki](https://github.com/telegram-bot-rb/telegram-bot/wiki).
|
||||||
@@ -199,12 +199,33 @@ end
|
|||||||
|
|
||||||
#### Session
|
#### Session
|
||||||
|
|
||||||
There is support for sessions using `ActiveSupport::Cache` stores.
|
This API is very close to ActiveController's session API, but works different
|
||||||
|
under the hood. Cookies can not be used to store session id or
|
||||||
|
whole session (like CookieStore does). So it uses key-value store and `session_key`
|
||||||
|
method to build identifier from update.
|
||||||
|
|
||||||
|
Store can be one of numerous `ActiveSupport::Cache` stores.
|
||||||
|
While `:file_store` is suitable for development and single-server deployments
|
||||||
|
without heavy load, it doesn't scale well. Key-value databases with persistance
|
||||||
|
like Redis are more appropriate for production use.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# configure store in env files:
|
# In rails app store can be configured in env files:
|
||||||
config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month}
|
config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month}
|
||||||
|
|
||||||
|
# In other app it can be done for all controllers with:
|
||||||
|
Telegram::Bot::UpdatesController.session_store = :redis_store, {expires_in: 1.month}
|
||||||
|
# or for specific one:
|
||||||
|
OneOfUpdatesController.session_store = :redis_store, {expires_in: 1.month}
|
||||||
|
```
|
||||||
|
|
||||||
|
Default session id is made from bot's username and `(from || chat)['id']`.
|
||||||
|
It means that session will be the same for updates from user in every chat,
|
||||||
|
and different for every user in the same group chat.
|
||||||
|
To change this behavior you can override `session_key` method, or even
|
||||||
|
define multiple sessions in single controller. For details see `Session` module.
|
||||||
|
|
||||||
|
```ruby
|
||||||
class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
||||||
include Telegram::Bot::UpdatesController::Session
|
include Telegram::Bot::UpdatesController::Session
|
||||||
# or just shortcut:
|
# or just shortcut:
|
||||||
@@ -222,12 +243,16 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
# By default it uses bot's username and user's id as a session key.
|
|
||||||
# Chat's id is used only when `from` field is empty.
|
# In this case session will persist for user only in specific chat.
|
||||||
# Override `session_key` method to change this behavior.
|
# Same user in other chat will have different session.
|
||||||
def session_key
|
def session_key
|
||||||
# In this case session will persist for user only in specific chat:
|
"#{bot.username}:#{chat['id']}:#{from['id']}" if chat && from
|
||||||
"#{bot.username}:#{chat['id']}:#{from['id']}"
|
end
|
||||||
|
|
||||||
|
# This session will be the same for all updates in chat.
|
||||||
|
def chat_session
|
||||||
|
@_chat_session ||= self.class.build_session(chat && "#{bot.username}:#{chat['id']}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace :telegram do
|
|||||||
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
|
route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
|
||||||
url = routes.send("#{route_name}_url")
|
url = routes.send("#{route_name}_url")
|
||||||
puts "Setting webhook for #{key}..."
|
puts "Setting webhook for #{key}..."
|
||||||
bot.set_webhook(url: url, certificate: cert)
|
bot.async(false) { bot.set_webhook(url: url, certificate: cert) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module Telegram
|
|||||||
|
|
||||||
ActiveSupport.on_load('telegram.bot.updates_controller') do
|
ActiveSupport.on_load('telegram.bot.updates_controller') do
|
||||||
self.logger = options.logger || Rails.logger
|
self.logger = options.logger || Rails.logger
|
||||||
self.session_store = options.session_store || Rails.cache
|
self.session_store = options.session_store if options.session_store
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ module Telegram
|
|||||||
|
|
||||||
include Session
|
include Session
|
||||||
|
|
||||||
included do
|
|
||||||
# As we use before_action context is cleared anyway,
|
|
||||||
# no matter we used it or not.
|
|
||||||
singleton_class.send :attr_reader, :context_handlers, :context_to_action
|
|
||||||
@context_handlers = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
|
def context_handlers
|
||||||
|
@_context_handlers ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
# Registers handler for context.
|
# Registers handler for context.
|
||||||
#
|
#
|
||||||
# context_handler :rename do |*|
|
# context_handler :rename do |*|
|
||||||
@@ -39,6 +36,8 @@ module Telegram
|
|||||||
context_handlers[context] = action || context
|
context_handlers[context] = action || context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attr_reader :context_to_action
|
||||||
|
|
||||||
# Use it to use context value as action name for all contexts
|
# Use it to use context value as action name for all contexts
|
||||||
# which miss handlers.
|
# which miss handlers.
|
||||||
# For security reasons it supports only action methods and will
|
# For security reasons it supports only action methods and will
|
||||||
@@ -59,10 +58,16 @@ module Telegram
|
|||||||
# according to previous request.
|
# according to previous request.
|
||||||
attr_reader :context
|
attr_reader :context
|
||||||
|
|
||||||
|
# Controller may have multiple sessions, let it be possible
|
||||||
|
# to select session for message context.
|
||||||
|
def message_context_session
|
||||||
|
session
|
||||||
|
end
|
||||||
|
|
||||||
# Fetches context and finds handler for it. If message has new command,
|
# Fetches context and finds handler for it. If message has new command,
|
||||||
# it has higher priority than contextual action.
|
# it has higher priority than contextual action.
|
||||||
def action_for_message
|
def action_for_message
|
||||||
val = session.delete(:context)
|
val = message_context_session.delete(:context)
|
||||||
@context = val && val.to_sym
|
@context = val && val.to_sym
|
||||||
super || context && begin
|
super || context && begin
|
||||||
handler = handler_for_context
|
handler = handler_for_context
|
||||||
@@ -72,7 +77,7 @@ module Telegram
|
|||||||
|
|
||||||
# Save context for the next request.
|
# Save context for the next request.
|
||||||
def save_context(context)
|
def save_context(context)
|
||||||
session[:context] = context
|
message_context_session[:context] = context
|
||||||
end
|
end
|
||||||
|
|
||||||
def handler_for_context
|
def handler_for_context
|
||||||
|
|||||||
@@ -8,19 +8,26 @@ module Telegram
|
|||||||
module Session
|
module Session
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# Builds session with given key and optional store (default to session_store).
|
||||||
|
# This way it's easier to define multiple custom sessions,
|
||||||
|
# ex. one for group chat and one for user.
|
||||||
|
def build_session(key, store = session_store)
|
||||||
|
raise 'session_store is not configured' unless store
|
||||||
|
key ? SessionHash.new(store, key) : NullSessionHash.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def process_action(*)
|
def process_action(*)
|
||||||
super
|
super
|
||||||
ensure
|
ensure
|
||||||
session.commit
|
session.commit if @_session
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def session
|
def session
|
||||||
@_session ||= begin
|
@_session ||= self.class.build_session(session_key)
|
||||||
key = session_key
|
|
||||||
key ? SessionHash.new(self.class.session_store, key) : NullSessionHash.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def session_key
|
def session_key
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module Telegram
|
module Telegram
|
||||||
module Bot
|
module Bot
|
||||||
VERSION = '0.12.0'.freeze
|
VERSION = '0.12.1'.freeze
|
||||||
|
|
||||||
def self.gem_version
|
def self.gem_version
|
||||||
Gem::Version.new VERSION
|
Gem::Version.new VERSION
|
||||||
|
|||||||
@@ -5,4 +5,9 @@ RSpec.describe DefaultBotController, :telegram_bot, type: :request do
|
|||||||
subject { -> { dispatch_command :start } }
|
subject { -> { dispatch_command :start } }
|
||||||
it { should respond_with_message 'from default' }
|
it { should respond_with_message 'from default' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#load_session' do
|
||||||
|
subject { -> { dispatch_command :load_session } }
|
||||||
|
it { should_not raise_error }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,4 +6,9 @@ RSpec.describe OtherBotController, :telegram_bot, type: :request do
|
|||||||
subject { -> { dispatch_command :start } }
|
subject { -> { dispatch_command :start } }
|
||||||
it { should respond_with_message 'from other' }
|
it { should respond_with_message 'from other' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#load_session' do
|
||||||
|
subject { -> { dispatch_command :load_session } }
|
||||||
|
it { should raise_error(/session_store is not configured/) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ require 'action_dispatch'
|
|||||||
require 'action_dispatch/testing/integration'
|
require 'action_dispatch/testing/integration'
|
||||||
|
|
||||||
require 'rails'
|
require 'rails'
|
||||||
|
require 'telegram/bot/railtie'
|
||||||
require 'rspec/rails/adapters'
|
require 'rspec/rails/adapters'
|
||||||
require 'rspec/rails/fixture_support'
|
require 'rspec/rails/fixture_support'
|
||||||
require 'rspec/rails/example/rails_example_group'
|
require 'rspec/rails/example/rails_example_group'
|
||||||
@@ -13,6 +14,7 @@ ENV['RAILS_ENV'] = 'test'
|
|||||||
class TestApplication < Rails::Application
|
class TestApplication < Rails::Application
|
||||||
config.eager_load = false
|
config.eager_load = false
|
||||||
config.log_level = :debug
|
config.log_level = :debug
|
||||||
|
config.action_dispatch.show_exceptions = false
|
||||||
secrets[:secret_key_base] = 'test'
|
secrets[:secret_key_base] = 'test'
|
||||||
secrets[:telegram] = {
|
secrets[:telegram] = {
|
||||||
bot: 'default_token',
|
bot: 'default_token',
|
||||||
@@ -34,6 +36,18 @@ Rails.application.initialize!
|
|||||||
Object.const_set("#{bot_name}_bot_controller".camelize, controller)
|
Object.const_set("#{bot_name}_bot_controller".camelize, controller)
|
||||||
end
|
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|
|
RSpec.configure do |config|
|
||||||
config.include RSpec::Rails::RequestExampleGroup, type: :request
|
config.include RSpec::Rails::RequestExampleGroup, type: :request
|
||||||
|
|
||||||
|
|||||||
@@ -54,4 +54,45 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.build_session' do
|
||||||
|
subject { controller_class.build_session(key, *args) }
|
||||||
|
let(:key) {}
|
||||||
|
let(:args) { [] }
|
||||||
|
it { expect { subject }.to raise_error(/session_store is not configured/) }
|
||||||
|
|
||||||
|
shared_examples 'NullSessionHash when key is not present' do |store_proc|
|
||||||
|
it { should be_instance_of(described_class::NullSessionHash) }
|
||||||
|
|
||||||
|
context 'and key is present' do
|
||||||
|
let(:key) { :test_key }
|
||||||
|
it 'is valid SessionHash' do
|
||||||
|
expect(subject).to be_instance_of(described_class::SessionHash)
|
||||||
|
expect(subject.id).to eq key
|
||||||
|
expect(subject.instance_variable_get(:@store)).to be(instance_exec(&store_proc))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when store configured' do
|
||||||
|
before { controller_class.session_store = nil }
|
||||||
|
include_examples 'NullSessionHash when key is not present',
|
||||||
|
-> { controller_class.session_store }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when store is given' do
|
||||||
|
let(:args) { [double(:store)] }
|
||||||
|
include_examples 'NullSessionHash when key is not present', -> { args[0] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.session_store=' do
|
||||||
|
subject { ->(val) { controller_class.session_store = val } }
|
||||||
|
it 'casts to AS::Cache' do
|
||||||
|
expect { subject[:null_store] }.to change(controller_class, :session_store).
|
||||||
|
to(instance_of(ActiveSupport::Cache::NullStore))
|
||||||
|
expect { subject[nil] }.to change(controller_class, :session_store).
|
||||||
|
to(instance_of(ActiveSupport::Cache::MemoryStore))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user