From e178f808b6e28750939c42e69d6d40320fc99a18 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 27 Nov 2020 09:33:39 +0000 Subject: [PATCH 1/3] Update to Bot API 5.0, add rake tasks for `deleteWebhook`, `close` & `logOut` --- .rubocop.yml | 1 + CHANGELOG.md | 1 + bin/fetch-telegram-methods | 2 +- lib/tasks/telegram-bot.rake | 25 ++++++--- lib/telegram/bot.rb | 1 + lib/telegram/bot/client/api_methods.txt | 6 +- lib/telegram/bot/tasks.rb | 63 +++++++++++++++++++++ spec/integration/tasks_spec.rb | 74 +++++++++++++++++++++++++ spec/integration_helper.rb | 1 + 9 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 lib/telegram/bot/tasks.rb create mode 100644 spec/integration/tasks_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index f4d1051..53ce884 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -40,6 +40,7 @@ Style/IfUnlessModifier: {Enabled: false} # Consistent to other definitions. Style/EmptyMethod: {EnforcedStyle: expanded} +Style/Lambda: {EnforcedStyle: literal} Style/ModuleFunction: {Enabled: false} Style/NestedParenthesizedCalls: {Enabled: false} Style/SignalException: {EnforcedStyle: only_raise} diff --git a/CHANGELOG.md b/CHANGELOG.md index 926fccc..bb175b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Add `:path` option to `telegram_webhook` route helper. - __Breaking change!__ Default route is generated using hashed bot token. Please reconfigure webhook after update (`rake telegram:bot:set_webhook`). +- Update to Bot API 5.0, add rake tasks for `deleteWebhook`, `close` & `logOut`. # 0.14.4 diff --git a/bin/fetch-telegram-methods b/bin/fetch-telegram-methods index 0092284..193d19a 100755 --- a/bin/fetch-telegram-methods +++ b/bin/fetch-telegram-methods @@ -25,7 +25,7 @@ method_list = headers. map { |g| g.reject { |x| x.match?(NOT_METHOD_REGEXP) } }. reject(&:empty?) -api_version = doc.text.match(/^(Bot API ([\d\.]+))\.?$/) +api_version = doc.text.match(/^(?:Introducing )?(Bot API ([\d\.]+))\.?$/) result = ['# Generated with bin/fetch-telegram-methods'] result << "# #{api_version[1]}" if api_version diff --git a/lib/tasks/telegram-bot.rake b/lib/tasks/telegram-bot.rake index 2ee40e8..01c5cd9 100644 --- a/lib/tasks/telegram-bot.rake +++ b/lib/tasks/telegram-bot.rake @@ -14,15 +14,22 @@ namespace :telegram do desc 'Set webhook urls for all bots' task set_webhook: :environment do - routes = Rails.application.routes.url_helpers - cert_file = ENV['CERT'] - cert = File.open(cert_file) if cert_file - Telegram.bots.each do |key, bot| - route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) - url = routes.send("#{route_name}_url") - puts "Setting webhook for #{key}..." - bot.async(false) { bot.set_webhook(url: url, certificate: cert) } - end + Telegram::Bot::Tasks.set_webhook + end + + desc 'Delete webhooks for all or specific BOT' + task :delete_webhook do + Telegram::Bot::Tasks.delete_webhook + end + + desc 'Perform logOut command for all or specific BOT' + task :log_out do + Telegram::Bot::Tasks.log_out + end + + desc 'Perform `close` command for all or specific BOT' + task :close do + Telegram::Bot::Tasks.close end end end diff --git a/lib/telegram/bot.rb b/lib/telegram/bot.rb index c3792e0..f6c1ffc 100644 --- a/lib/telegram/bot.rb +++ b/lib/telegram/bot.rb @@ -28,6 +28,7 @@ module Telegram autoload :Initializers, 'telegram/bot/initializers' autoload :Middleware, 'telegram/bot/middleware' autoload :RSpec, 'telegram/bot/rspec' + autoload :Tasks, 'telegram/bot/tasks' autoload :UpdatesController, 'telegram/bot/updates_controller' autoload :UpdatesPoller, 'telegram/bot/updates_poller' end diff --git a/lib/telegram/bot/client/api_methods.txt b/lib/telegram/bot/client/api_methods.txt index 24b1ffc..eee297a 100644 --- a/lib/telegram/bot/client/api_methods.txt +++ b/lib/telegram/bot/client/api_methods.txt @@ -1,5 +1,5 @@ # Generated with bin/fetch-telegram-methods -# Bot API 4.7 +# Bot API 5.0 getUpdates setWebhook @@ -7,8 +7,11 @@ deleteWebhook getWebhookInfo getMe +logOut +close sendMessage forwardMessage +copyMessage sendPhoto sendAudio sendDocument @@ -40,6 +43,7 @@ setChatTitle setChatDescription pinChatMessage unpinChatMessage +unpinAllChatMessages leaveChat getChat getChatAdministrators diff --git a/lib/telegram/bot/tasks.rb b/lib/telegram/bot/tasks.rb new file mode 100644 index 0000000..2bd1787 --- /dev/null +++ b/lib/telegram/bot/tasks.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Telegram + module Bot + module Tasks + extend self + + def set_webhook + routes = Rails.application.routes.url_helpers + cert_file = ENV['CERT'] + cert = File.open(cert_file) if cert_file + each_bot do |key, bot| + route_name = RoutesHelper.route_name_for_bot(bot) + url = routes.send("#{route_name}_url") + say("Setting webhook for #{key}...") + bot.set_webhook( + url: url, + certificate: cert, + ip_address: ENV['IP_ADDRESS'], + drop_pending_updates: drop_pending_updates, + ) + end + end + + def delete_webhook + each_bot do |key, bot| + say("Deleting webhook for #{key}...") + bot.delete_webhook(drop_pending_updates: drop_pending_updates) + end + end + + def log_out + each_bot do |key, bot| + say("Logging out #{key}...") + bot.log_out + end + end + + def close + each_bot do |key, bot| + say("Closing #{key}...") + bot.close + end + end + + private + + def say(text) + puts(text) unless Rails.env.test? # rubocop:disable Rails/Output + end + + def each_bot(&block) + id = ENV['BOT'].try!(:to_sym) + bots = id ? {id => Client.by_id(id)} : Telegram.bots + bots.each { |key, bot| bot.async(false) { block[key, bot] } } + end + + def drop_pending_updates + ENV['DROP_PENDING_UPDATES'].try!(:downcase) == 'true' + end + end + end +end diff --git a/spec/integration/tasks_spec.rb b/spec/integration/tasks_spec.rb new file mode 100644 index 0000000..39b2d35 --- /dev/null +++ b/spec/integration/tasks_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'integration_helper' + +RSpec.describe Telegram::Bot::Tasks, type: :request do + include Telegram::Bot::RSpec::ClientMatchers + + def stub_env(new_values) + new_values.stringify_keys! + old_values = ENV.to_h.slice(new_values.keys) + new_values.each { |k, v| ENV[k] = v.to_s } + yield + ensure + new_values.each_key { |k| ENV[k] = old_values[k] } + end + + shared_examples 'uses BOT envar' do |bot_matcher| + it 'runs for all bots by default' do + matcher = Telegram.bots.values.map { |x| instance_exec(x, &bot_matcher) }.reduce(&:and) + expect(subject).to matcher + end + + context 'when BOT specified' do + around { |ex| stub_env(BOT: :other) { ex.run } } + + it 'runs for this bot only' do + expect(subject).to instance_exec(Telegram.bots[:other], &bot_matcher) + end + end + end + + describe '#log_out' do + subject { -> { described_class.log_out } } + include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :logOut) } + end + + describe '#close' do + subject { -> { described_class.close } } + include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :close) } + end + + describe '#set_webhook' do + subject { -> { described_class.set_webhook } } + include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :setWebhook) } + + context 'with options in env' do + around do |ex| + stub_env(IP_ADDRESS: '1.2.3.4', DROP_PENDING_UPDATES: 'TrUe') do + ex.run + end + end + include_examples 'uses BOT envar', ->(bot) { + make_telegram_request(bot, :setWebhook).with(hash_including( + ip_address: '1.2.3.4', + drop_pending_updates: true, + )) + } + end + end + + describe '#delete_webhook' do + subject { -> { described_class.delete_webhook } } + include_examples 'uses BOT envar', ->(bot) { make_telegram_request(bot, :deleteWebhook) } + + context 'with options in env' do + around { |ex| stub_env(DROP_PENDING_UPDATES: 'TrUe') { ex.run } } + include_examples 'uses BOT envar', ->(bot) { + make_telegram_request(bot, :deleteWebhook).with(hash_including( + drop_pending_updates: true, + )) + } + end + end +end diff --git a/spec/integration_helper.rb b/spec/integration_helper.rb index dcd91f5..e876124 100644 --- a/spec/integration_helper.rb +++ b/spec/integration_helper.rb @@ -14,6 +14,7 @@ class TestApplication < Rails::Application config.eager_load = false config.log_level = :debug config.action_dispatch.show_exceptions = false + routes.default_url_options = {host: 'test.rpsec'} telegram_config = { bot: 'default_token', From f654071c4cf19d09e41824bd384c374d1f9de318 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 27 Nov 2020 09:37:55 +0000 Subject: [PATCH 2/3] Drop Initializer module It was required for Botan support. --- lib/telegram/bot.rb | 1 - lib/telegram/bot/client.rb | 12 ++++- lib/telegram/bot/initializers.rb | 21 --------- spec/support/examples/initializers.rb | 65 --------------------------- spec/telegram/bot/client_spec.rb | 65 ++++++++++++++++++++++++++- 5 files changed, 75 insertions(+), 89 deletions(-) delete mode 100644 lib/telegram/bot/initializers.rb delete mode 100644 spec/support/examples/initializers.rb diff --git a/lib/telegram/bot.rb b/lib/telegram/bot.rb index f6c1ffc..50d4e3b 100644 --- a/lib/telegram/bot.rb +++ b/lib/telegram/bot.rb @@ -25,7 +25,6 @@ module Telegram 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 :RSpec, 'telegram/bot/rspec' autoload :Tasks, 'telegram/bot/tasks' diff --git a/lib/telegram/bot/client.rb b/lib/telegram/bot/client.rb index 9ff861e..beb32ee 100644 --- a/lib/telegram/bot/client.rb +++ b/lib/telegram/bot/client.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/keys' require 'json' require 'httpclient' @@ -7,7 +8,6 @@ module Telegram URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze autoload :TypedResponse, 'telegram/bot/client/typed_response' - extend Initializers prepend Async include DebugClient @@ -15,6 +15,16 @@ module Telegram include ApiHelper class << self + # 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) Telegram.bots[id] end diff --git a/lib/telegram/bot/initializers.rb b/lib/telegram/bot/initializers.rb deleted file mode 100644 index 3dfb08e..0000000 --- a/lib/telegram/bot/initializers.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'active_support/core_ext/hash/keys' - -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 diff --git a/spec/support/examples/initializers.rb b/spec/support/examples/initializers.rb deleted file mode 100644 index 552c985..0000000 --- a/spec/support/examples/initializers.rb +++ /dev/null @@ -1,65 +0,0 @@ -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 diff --git a/spec/telegram/bot/client_spec.rb b/spec/telegram/bot/client_spec.rb index 0a82bd3..1b0ae06 100644 --- a/spec/telegram/bot/client_spec.rb +++ b/spec/telegram/bot/client_spec.rb @@ -2,9 +2,72 @@ RSpec.describe Telegram::Bot::Client do let(:instance) { described_class.new 'token' } let(:token) { 'token' } - include_examples 'initializers' it_behaves_like 'async', request_args: -> { [double(:action), {body: :content}] } + 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(:bots) { {client_1: instance} } } + it { should eq Telegram.bots[:client_1] } + + context 'and there is no such bot' do + let(:input) { :invalid } + it { expect { subject }.to raise_error(/not configured/) } + end + end + end + describe '.prepare_body' do subject { described_class.prepare_body(input) } From 4b9eedb5a2a34f4e96f2fd10e0a3b3ddeeab7d43 Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 27 Nov 2020 18:47:47 +0000 Subject: [PATCH 3/3] Add `server` option for client to support local bot API servers --- CHANGELOG.md | 1 + README.md | 7 ++++++- lib/telegram/bot/client.rb | 7 ++++--- spec/telegram/bot/client_spec.rb | 35 ++++++++++++++++++++++++-------- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb175b4..7af6252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - __Breaking change!__ Default route is generated using hashed bot token. Please reconfigure webhook after update (`rake telegram:bot:set_webhook`). - Update to Bot API 5.0, add rake tasks for `deleteWebhook`, `close` & `logOut`. +- Add `server` option for client to support local bot API servers. # 0.14.4 diff --git a/README.md b/README.md index f3b8224..d506b98 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,11 @@ which is used for `Telegram.bot`. ```ruby Telegram.bots_config = { default: DEFAULT_BOT_TOKEN, - chat: {token: CHAT_BOT_TOKEN, username: 'chatbot'}, + chat: { + token: CHAT_BOT_TOKEN, + username: 'ChatBot', # to support commands with mentions (/help@ChatBot) + server: 'http://local.bot.api.server', # for Local Bot API Server + }, } Telegram.bot.get_updates @@ -87,6 +91,7 @@ development: bot: token: TOKEN username: SomeBot + server: http://local.bot.api.server # For multiple bots in single app use hash of `internal_bot_id => settings` bots: diff --git a/lib/telegram/bot/client.rb b/lib/telegram/bot/client.rb index beb32ee..fcff19c 100644 --- a/lib/telegram/bot/client.rb +++ b/lib/telegram/bot/client.rb @@ -5,7 +5,8 @@ require 'httpclient' module Telegram module Bot class Client - URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze + SERVER = 'https://api.telegram.org'.freeze + URL_TEMPLATE = '%s/bot%s/'.freeze autoload :TypedResponse, 'telegram/bot/client/typed_response' prepend Async @@ -61,11 +62,11 @@ module Telegram attr_reader :client, :token, :username, :base_uri - def initialize(token = nil, username = nil, **options) + def initialize(token = nil, username = nil, server: SERVER, **options) @client = HTTPClient.new @token = token || options[:token] @username = username || options[:username] - @base_uri = format(URL_TEMPLATE, token: self.token) + @base_uri = format(URL_TEMPLATE, server: server, token: self.token) end def request(action, body = {}) diff --git a/spec/telegram/bot/client_spec.rb b/spec/telegram/bot/client_spec.rb index 1b0ae06..5553a18 100644 --- a/spec/telegram/bot/client_spec.rb +++ b/spec/telegram/bot/client_spec.rb @@ -95,26 +95,45 @@ RSpec.describe Telegram::Bot::Client do describe '.new' do subject { described_class.new(*args) } + let(:token) { 'secret' } + let(:username) { 'superbot' } 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] } + let(:args) { [token, username] } + its(:token) { should eq token } + its(:username) { should eq username } + its(:base_uri) { should eq "#{described_class::SERVER}/bot#{token}/" } 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] } + its(:token) { should eq token } + its(:username) { should eq username } + its(:base_uri) { should eq "#{described_class::SERVER}/bot#{token}/" } + end + + context 'with custom server' do + let(:server) { 'http://my.server' } + let(:args) { [token, username, server: server] } + its(:base_uri) { should eq "#{server}/bot#{token}/" } + + context 'and hash options' do + let(:args) { [token: token, username: username, server: server] } + its(:base_uri) { should eq "#{server}/bot#{token}/" } + end end end describe '#request' do subject { -> { instance.request(action, request_body) } } let(:action) { :some_action } - let(:url) { "#{format(described_class::URL_TEMPLATE, token: token)}#{action}" } + let(:url) do + base_uri = format(described_class::URL_TEMPLATE, + server: described_class::SERVER, + token: token, + ) + "#{base_uri}#{action}" + end let(:request_body) { double(:body) } let(:prepared_body) { double(:prepared_body) } let(:response) { HTTP::Message.new_response(body).tap { |x| x.status = status } }