diff --git a/README.md b/README.md index dee6989..1088bd1 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,42 @@ Check out `telegram/bot/updates_controller/rspec_helpers` and Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots. Certificate can be specified with `CERT=path/to/cert`. +### Botan.io metrics + +Initialize with `bot = Bot.new(token, botan: 'botan token')` +or just add `botan` key in `secrets.yml`: + +```yml + telegram: + bot: + token: bot_token + botan: botan_token +``` + +Access to Botan client with `bot.botan`. +Use `bot.botan.track(event, uid, payload)` to track events. + +There are some helpers for controllers in `Telegram::Bot::UpdatesController::Botan`: + +```ruby +class Telegram::WebhookController < Telegram::Bot::UpdatesController + include Telegram::Bot::UpdatesController::Botan + + # This will track with event: action_name & data: payload + before_action :botan_track_action + + def smth(*) + # This will track event for current user only when botan is configured. + botan_track :my_event, custom_data + + # or get access directly to botan client: + botan.track(...) + end +end +``` + +There is no stubbing for botan clients, so don't set botan token in tests. + ## Development After checking out the repo, run `bin/setup` to install dependencies. diff --git a/lib/telegram/bot.rb b/lib/telegram/bot.rb index 001a8f8..06521b5 100644 --- a/lib/telegram/bot.rb +++ b/lib/telegram/bot.rb @@ -26,6 +26,7 @@ module Telegram end end + autoload :Botan, 'telegram/bot/botan' autoload :Client, 'telegram/bot/client' autoload :ClientStub, 'telegram/bot/client_stub' autoload :Middleware, 'telegram/bot/middleware' diff --git a/lib/telegram/bot/botan.rb b/lib/telegram/bot/botan.rb new file mode 100644 index 0000000..9c97a3e --- /dev/null +++ b/lib/telegram/bot/botan.rb @@ -0,0 +1,36 @@ +module Telegram + module Bot + class Botan + TRACK_URI = 'https://api.botan.io/track'.freeze + + class Error < Bot::Error; end + + include DebugClient + + attr_reader :client, :token + + def initialize(token) + @client = HTTPClient.new + @token = token + end + + def track(event, uid, payload = {}) + res = http_request( + :post, + TRACK_URI, + {token: token, name: event, uid: uid}, + payload.to_json, + ) + status = res.status + return JSON.parse(res.body) if 300 > status + result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier + err_msg = "#{res.reason}: #{result && result['info'] || '-'}" + raise Error, err_msg + end + + def http_request(method, uri, query, body) + client.request(method, uri, query, body) + end + end + end +end diff --git a/lib/telegram/bot/client.rb b/lib/telegram/bot/client.rb index 1a283a4..62cb1b2 100644 --- a/lib/telegram/bot/client.rb +++ b/lib/telegram/bot/client.rb @@ -2,14 +2,16 @@ 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 class Client - autoload :TypedResponse, 'telegram/bot/client/typed_response' - URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze + autoload :TypedResponse, 'telegram/bot/client/typed_response' + include DebugClient + class << self # Accepts different options to initialize bot. def wrap(input) @@ -18,7 +20,7 @@ module Telegram when Array then input.map(&method(__callee__)) when Hash then input = input.stringify_keys - new input['token'], input['username'] + new input['token'], input['username'], botan: input['botan'] when Symbol Telegram.bots[input] or raise "Bot #{input} not configured, check Telegram.bots_config." @@ -41,31 +43,14 @@ module Telegram end end - attr_reader :client, :token, :username, :base_uri + attr_reader :client, :token, :username, :base_uri, :botan - def initialize(token, username = nil) + def initialize(token, username = nil, botan: nil) @client = HTTPClient.new @token = token @username = username @base_uri = format URL_TEMPLATE, token - end - - def debug!(dev = STDOUT) - if block_given? - begin - old_dev = client.debug_dev - client.debug_dev = dev - yield - ensure - client.debug_dev = old_dev - end - else - client.debug_dev = dev - end - end - - def debug_off! - client.debug_dev = nil + @botan = Botan.new(botan) if botan end def request(action, body = {}) # rubocop:disable PerceivedComplexity diff --git a/lib/telegram/bot/debug_client.rb b/lib/telegram/bot/debug_client.rb new file mode 100644 index 0000000..4a8a733 --- /dev/null +++ b/lib/telegram/bot/debug_client.rb @@ -0,0 +1,23 @@ +module Telegram + module Bot + module DebugClient + def debug!(dev = STDOUT) + if block_given? + begin + old_dev = client.debug_dev + client.debug_dev = dev + yield + ensure + client.debug_dev = old_dev + end + else + client.debug_dev = dev + end + end + + def debug_off! + client.debug_dev = nil + end + end + end +end diff --git a/lib/telegram/bot/updates_controller.rb b/lib/telegram/bot/updates_controller.rb index 2a4fa95..b41c94b 100644 --- a/lib/telegram/bot/updates_controller.rb +++ b/lib/telegram/bot/updates_controller.rb @@ -57,6 +57,7 @@ module Telegram require 'telegram/bot/updates_controller/log_subscriber' require 'telegram/bot/updates_controller/instrumentation' autoload :MessageContext, 'telegram/bot/updates_controller/message_context' + autoload :Botan, 'telegram/bot/updates_controller/botan' include AbstractController::Callbacks # Redefine callbacks with default terminator. diff --git a/lib/telegram/bot/updates_controller/botan.rb b/lib/telegram/bot/updates_controller/botan.rb new file mode 100644 index 0000000..b260600 --- /dev/null +++ b/lib/telegram/bot/updates_controller/botan.rb @@ -0,0 +1,33 @@ +module Telegram + module Bot + class UpdatesController + # Helpers for botan.io metrics. + module Botan + class MissingFrom < Error; end + + protected + + def botan + @botan ||= bot.try!(:botan) + end + + # Track custom event for user taken from `from` field: + # + # botan_track :my_event, {data: :val} + # + def botan_track(event, data = {}) + raise MissingFrom, 'Can not track without user' unless from + botan.try! { |x| x.track(event, from['id'], data) } + end + + # Track current action and payload for current user. Best used with `before_action`: + # + # before_action :botan_track_action + # + def botan_track_action + botan_track(action_name, payload) + end + end + end + end +end diff --git a/spec/telegram/bot/client_spec.rb b/spec/telegram/bot/client_spec.rb index e9e27f8..786531a 100644 --- a/spec/telegram/bot/client_spec.rb +++ b/spec/telegram/bot/client_spec.rb @@ -1,10 +1,12 @@ RSpec.describe Telegram::Bot::Client do + let(:instance) { described_class.new 'token' } + let(:token) { 'token' } + let(:botan_token) { double(:botan_token) } + describe '.wrap' do subject { described_class.wrap(input) } let(:result) { double(:result) } - let(:token) { 'token' } let(:username) { 'username' } - let(:instance) { described_class.new 'token' } context 'when input is a string' do let(:input) { token } @@ -19,9 +21,20 @@ RSpec.describe Telegram::Bot::Client do let(:input) { {token: token, username: username, ignore: :ignore} } it 'extracts token & username' do - expect(described_class).to receive(:new).with(token, username) { result } + expect(described_class).to receive(:new). + with(token, username, botan: nil) { result } should eq result end + + context 'when `botan` is given' do + let(:input) { super().merge(botan: botan_token) } + + it 'passes it to initializer' do + expect(described_class).to receive(:new). + with(token, username, botan: botan_token) { result } + should eq result + end + end end context 'when input is an instance of described_class' do @@ -50,7 +63,7 @@ RSpec.describe Telegram::Bot::Client do it 'calls wrap for every element' do expect(described_class).to receive(:new).with('other_token') { result } - expect(described_class).to receive(:new).with(token, username) { result_2 } + expect(described_class).to receive(:new).with(token, username, botan: nil) { result_2 } should eq [result, instance, result_2] end end @@ -74,4 +87,15 @@ RSpec.describe Telegram::Bot::Client do end end end + + describe '#botan' do + subject { instance.botan } + it { should eq nil } + + context 'when botan token is set' do + let(:instance) { described_class.new token, botan: botan_token } + it { should be_instance_of Telegram::Bot::Botan } + its(:token) { should eq botan_token } + end + end end diff --git a/spec/telegram/bot/updates_controller/botan_spec.rb b/spec/telegram/bot/updates_controller/botan_spec.rb new file mode 100644 index 0000000..1b63717 --- /dev/null +++ b/spec/telegram/bot/updates_controller/botan_spec.rb @@ -0,0 +1,54 @@ +RSpec.describe Telegram::Bot::UpdatesController::Botan 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 + let(:botan) { double(:botan) } + let(:result) { double(:result) } + let(:payload) { {from: {id: user_id}} } + let(:payload_type) { :message } + let(:user_id) { double(:user_id) } + before { allow(bot).to receive(:botan) { botan } } + + shared_examples 'basic tracking' do + context 'when botan is not configured' do + let(:botan) {} + it { should eq nil } + end + + context 'when `from` is empty' do + let(:payload) { {text: 'test'} } + it { expect { subject }.to raise_error described_class::MissingFrom } + end + end + + describe '#botan_track' do + subject { controller.send(:botan_track, event, data) } + let(:event) { double(:event) } + let(:data) { double(:data) } + + it 'calls botan#track' do + expect(botan).to receive(:track).with(event, user_id, data) { result } + should eq result + end + + include_examples 'basic tracking' + end + + describe '#botan_track_action' do + subject { controller.send(:botan_track_action) } + let(:action_name) { double(:action_name) } + + it 'calls botan#track with current action and payload' do + expect(controller).to receive(:action_name) { action_name } + expect(botan).to receive(:track). + with(action_name, user_id, deep_stringify(payload)) { result } + should eq result + end + + include_examples 'basic tracking' + end +end