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

Merge pull request #45 from telegram-bot-rb/update_sessions

Update sessions
Этот коммит содержится в:
printercu
2017-10-06 11:19:01 +03:00
коммит произвёл GitHub
родитель df7039326d 7e41bd4f9b
Коммит 0ae17fb34e
8 изменённых файлов: 124 добавлений и 22 удалений

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

@@ -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
``` ```

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

@@ -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

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

@@ -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