diff --git a/README.md b/README.md index fe6f874..f8c193b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ bot.get_me.class # => Telegram::Bot::Types::User ```ruby class Telegram::WebhookController < Telegram::Bot::UpdatesController # use callbacks like in any other controllers - around_action :set_locale + around_action :with_locale # Every update can have one of: message, inline_query & chosen_inline_result. # Define method with same name to respond to this updates. @@ -123,7 +123,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController private - def set_locale(&block) + def with_locale(&block) I18n.with_locale(locale_for_update, &block) end @@ -150,6 +150,36 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController end ``` +There is support for sessions using `ActiveSupport::Cache` stores. + +```ruby +# configure store in env files: +config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month} + +class Telegram::WebhookController < Telegram::Bot::UpdatesController + include Telegram::Bot::UpdatesController::Session + # You can override global config + self.session_store = :file_store + + def write(text = nil, *) + session[:text] = text + end + + def read + session[:text] + end + + private + # By default it uses bot's username and user's id as a session key. + # Chat's id is used only when `from` field is empty. + # Override `session_key` method to change this behavior. + def session_key + # In this case session will persist for user only in specific chat: + "#{bot.username}:#{chat['id']}:#{from['id']}" + end +end +``` + ### Routes Use `telegram_webhooks` helper to add routes. It will create routes for bots diff --git a/lib/telegram/bot/railtie.rb b/lib/telegram/bot/railtie.rb index 4c08258..6c08ba2 100644 --- a/lib/telegram/bot/railtie.rb +++ b/lib/telegram/bot/railtie.rb @@ -18,6 +18,7 @@ module Telegram ActiveSupport.on_load('telegram.bot.updates_controller') do self.logger = options.logger || Rails.logger + self.session_store = options.session_store || Rails.cache end end diff --git a/lib/telegram/bot/updates_controller.rb b/lib/telegram/bot/updates_controller.rb index 8d37353..eb8f748 100644 --- a/lib/telegram/bot/updates_controller.rb +++ b/lib/telegram/bot/updates_controller.rb @@ -7,6 +7,10 @@ module Telegram class UpdatesController < AbstractController::Base abstract! + require 'telegram/bot/updates_controller/session' + require 'telegram/bot/updates_controller/log_subscriber' + require 'telegram/bot/updates_controller/instrumentation' + include AbstractController::Callbacks # Redefine callbacks with default terminator. if ActiveSupport.gem_version >= Gem::Version.new('5') @@ -19,10 +23,8 @@ module Telegram end include AbstractController::Translation - - require 'telegram/bot/updates_controller/log_subscriber' - require 'telegram/bot/updates_controller/instrumentation' prepend Instrumentation + extend Session::ConfigMethods autoload :TypedUpdate, 'telegram/bot/updates_controller/typed_update' diff --git a/lib/telegram/bot/updates_controller/session.rb b/lib/telegram/bot/updates_controller/session.rb new file mode 100644 index 0000000..47139d3 --- /dev/null +++ b/lib/telegram/bot/updates_controller/session.rb @@ -0,0 +1,72 @@ +require 'rack/session/abstract/id' +require 'active_support/cache' + +module Telegram + module Bot + class UpdatesController + # Add functionality to store data between requests. + module Session + extend ActiveSupport::Concern + + def process_action(*) + super + ensure + session.commit + end + + protected + + def session + @_session ||= SessionHash.new(self.class.session_store, session_key) + end + + def session_key + "#{bot.username}:#{from ? "from:#{from['id']}" : "chat:#{chat['id']}"}" + end + + # Rack::Session::Abstract::SessionHash is taken to provide lazy loading. + # All methods that access store are overriden to support + # ActiveSupport::Cache::Store stores. + class SessionHash < Rack::Session::Abstract::SessionHash + attr_reader :id + + def initialize(store, id) + @store = store + @id = id + end + + def destroy + clear + @store.delete(id) + end + + def exists? + return @exists if defined?(@exists) + @data = {} + @exists = @store.exist? id + end + + def load! + session = @store.read(id) + @data = session ? stringify_keys(session) : {} + @loaded = true + end + + def commit + return unless loaded? + data = to_hash.delete_if { |_, v| v.nil? } + @store.write(id, data) + end + end + + module ConfigMethods + delegate :session_store, to: :config + + def session_store=(store) + config.session_store = ActiveSupport::Cache.lookup_store(store) + end + end + end + end + end +end diff --git a/spec/telegram/bot/updates_controller/session_spec.rb b/spec/telegram/bot/updates_controller/session_spec.rb new file mode 100644 index 0000000..6e95eca --- /dev/null +++ b/spec/telegram/bot/updates_controller/session_spec.rb @@ -0,0 +1,43 @@ +RSpec.describe Telegram::Bot::UpdatesController::Session do + include_context 'telegram/bot/updates_controller' + let(:controller_class) do + described_class = self.described_class + Class.new(Telegram::Bot::UpdatesController) do + include described_class + end + end + + describe '.action_methods' do + subject { controller_class.action_methods } + it { should be_empty } + end + + describe '.dispatch' do + subject { ->(*args) { controller_class.dispatch(*args) } } + let(:other_bot) { double(username: 'otherBot') } + before do + controller_class.class_eval do + self.session_store = :memory_store + + def write(text) + session[:text] = text + end + + def read + session[:text] + end + end + end + + def build_message(text, from) + {'message' => {'text' => text, 'from' => from.stringify_keys}} + end + + it 'stores session between requests' do + subject.call(bot, build_message('/write test', id: 1)) + expect(subject.call(bot, build_message('/read', id: 1))).to eq 'test' + expect(subject.call(bot, build_message('/read', id: 2))).to eq nil + expect(subject.call(other_bot, build_message('/read', id: 1))).to eq nil + end + end +end