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

Merge pull request #2 from telegram-bot-rb/async

Async requests
Этот коммит содержится в:
printercu
2016-10-19 14:00:26 +03:00
коммит произвёл GitHub
родитель 4b511f51a7 caee9809d0
Коммит 9e67122465
24 изменённых файлов: 728 добавлений и 121 удалений

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

@@ -27,9 +27,12 @@ module Telegram
end
end
autoload :Async, 'telegram/bot/async'
autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client'
autoload :ClientStub, 'telegram/bot/client_stub'
autoload :DebugClient, 'telegram/bot/debug_client'
autoload :Initializers, 'telegram/bot/initializers'
autoload :Middleware, 'telegram/bot/middleware'
autoload :UpdatesController, 'telegram/bot/updates_controller'
autoload :UpdatesPoller, 'telegram/bot/updates_poller'

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

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, **)
super
@botan = Botan.wrap(botan, id: id) if botan
end
end
end
end
end

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

@@ -1,8 +1,8 @@
module Telegram
module Bot
class UpdatesController
class Botan
# Helpers for botan.io metrics.
module Botan
module ControllerHelpers
class MissingFrom < Error; end
protected

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

@@ -2,7 +2,6 @@ require 'json'
require 'httpclient'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/keys'
require 'telegram/bot/debug_client'
module Telegram
module Bot
@@ -10,23 +9,14 @@ module Telegram
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
autoload :TypedResponse, 'telegram/bot/client/typed_response'
extend Initializers
prepend Async
prepend Botan::ClientHelpers
include DebugClient
class << self
# Accepts different options to initialize bot.
def wrap(input)
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
def by_id(id)
Telegram.bots[id]
end
# Prepend TypedResponse module.
@@ -41,16 +31,19 @@ module Telegram
body[k] = val.to_json if val.is_a?(Hash) || val.is_a?(Array)
end
end
def prepare_async_args(action, body = {})
[action.to_s, Async.prepare_hash(prepare_body(body))]
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
@token = token
@username = username
@base_uri = format URL_TEMPLATE, token
@botan = Botan.new(botan) if botan
@token = token || options[:token]
@username = username || options[:username]
@base_uri = format URL_TEMPLATE, self.token
end
def request(action, body = {}) # rubocop:disable PerceivedComplexity

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

@@ -9,7 +9,7 @@ module Telegram
if self == ClientStub || !ClientStub.stub_all?
super
else
ClientStub.new(args[1])
ClientStub.new(*args)
end
end
end
@@ -34,8 +34,8 @@ module Telegram
end
end
def initialize(username = nil)
@username = username
def initialize(token = nil, username = nil, **options)
@username = username || options[:username] || token
reset
end

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

@@ -26,7 +26,9 @@ module Telegram
# Hash of bots made with bots_config.
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
# Default bot.
@@ -34,17 +36,26 @@ module Telegram
@bot ||= bots[:default]
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
# from `secrets.yml` merging `telegram['bot']` at `:default` key.
#
# Can be overwritten with .bots_config=
def bots_config
return @bots_config if @bots_config
telegram_config = Rails.application.secrets[:telegram]
(telegram_config['bots'] || {}).symbolize_keys.tap do |config|
default = telegram_config['bot']
config[:default] = default if default
end
@bots_config ||=
if defined?(Rails)
telegram_config = Rails.application.secrets[:telegram] || {}
(telegram_config['bots'] || {}).symbolize_keys.tap do |config|
default = telegram_config['bot']
config[:default] = default if default
end
else
{}
end
end
# Resets all cached bots and their configs.
@@ -52,6 +63,7 @@ module Telegram
@bots = nil
@bot = nil
@bots_config = nil
@botans = nil
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

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

@@ -59,7 +59,6 @@ module Telegram
require 'telegram/bot/updates_controller/reply_helpers'
autoload :CallbackQueryContext, 'telegram/bot/updates_controller/callback_query_context'
autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
autoload :Botan, 'telegram/bot/updates_controller/botan'
include AbstractController::Callbacks
# Redefine callbacks with default terminator.

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

@@ -64,7 +64,7 @@ module Telegram
end
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?
reload! do
response['result'].each do |update|

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

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