diff --git a/Gemfile b/Gemfile index e549386..0f023ab 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,10 @@ gem 'sidekiq-rate-limiter', git: 'https://github.com/centosadmin/sidekiq-rate-li gem 'telegram-bot-ruby', '~> 0.8.6' gem 'slack-ruby-bot' gem 'celluloid-io' -gem 'tdlib-ruby', '~> 0.9' +gem 'tdlib-ruby', '~> 1.0' +gem 'jwt' +gem 'filelock' +gem 'lazy_object', '~> 0.0.2' group :test do gem 'timecop' diff --git a/app/controllers/redmine_telegram_setup_controller.rb b/app/controllers/redmine_telegram_setup_controller.rb index 783e830..2b53557 100644 --- a/app/controllers/redmine_telegram_setup_controller.rb +++ b/app/controllers/redmine_telegram_setup_controller.rb @@ -19,7 +19,7 @@ def authorize authenticate.(params) save_phone_settings(phone_number: params['phone_number']) redirect_to plugin_settings_path('redmine_bots'), notice: t('telegram_common.client.authorize.success') - rescue RedmineBots::Telegram::TdlibAuthenticate::AuthenticationError => e + rescue RedmineBots::Telegram::Tdlib::Authenticate::AuthenticationError => e redirect_to plugin_settings_path('redmine_bots'), alert: e.message end end diff --git a/app/controllers/telegram_login_controller.rb b/app/controllers/telegram_login_controller.rb index f2f072a..a3d43a3 100644 --- a/app/controllers/telegram_login_controller.rb +++ b/app/controllers/telegram_login_controller.rb @@ -4,12 +4,33 @@ def index def check_auth user = User.find_by_id(session[:otp_user_id]) || User.current + auth = RedmineBots::Telegram::Bot::Authenticate.(user, login_params, context: context) - auth = RedmineBots::Telegram::Bot::Authenticate.(user, login_params) + handle_auth_result(auth, user) + end + + def send_sign_in_link + user = session[:otp_user_id] ? User.find(session[:otp_user_id]) : User.current + RedmineBots::Telegram::Bot::SendSignInLink.(user, context: context, params: params.slice(:autologin, :back_url)) + end + + def check_jwt + user = User.find_by_id(session[:otp_user_id]) || User.current + auth = RedmineBots::Telegram::Bot::AuthenticateByToken.(user, params[:token], context: context) + + handle_auth_result(auth, user) + end + private + + def context + session[:otp_user_id] ? '2fa_connection' : 'account_connection' + end + + def handle_auth_result(auth, user) if auth.success? - if session[:otp_user_id] - user.update_column(:two_fa_id, AuthSource.find_by_name('Telegram').id) + if user != User.current + user.update!(two_fa_id: AuthSource.find_by_name('Telegram').id) successful_authentication(user) else redirect_to my_page_path, notice: t('redmine_bots.telegram.bot.login.success') @@ -19,8 +40,6 @@ def check_auth end end - private - def login_params params.permit(:id, :first_name, :last_name, :username, :photo_url, :auth_date, :hash) end diff --git a/app/views/settings/redmine_bots/_telegram.erb b/app/views/settings/redmine_bots/_telegram.erb index def3e4c..83198cf 100644 --- a/app/views/settings/redmine_bots/_telegram.erb +++ b/app/views/settings/redmine_bots/_telegram.erb @@ -136,6 +136,6 @@ <%= hidden_field_tag 'settings[telegram_bot_name]', @settings['telegram_bot_name'] %> -<%= hidden_field_tag 'settings[telegram_bot_id]', @settings['bot_id'] %> +<%= hidden_field_tag 'settings[telegram_bot_id]', @settings['telegram_bot_id'] %> <%= hidden_field_tag 'settings[telegram_robot_id]', @settings['telegram_robot_id'] %> <%= hidden_field_tag 'settings[telegram_phone_number]', @settings['telegram_phone_number'] %> diff --git a/app/views/telegram_login/_widget.erb b/app/views/telegram_login/_widget.erb index 6347ba9..61504ef 100644 --- a/app/views/telegram_login/_widget.erb +++ b/app/views/telegram_login/_widget.erb @@ -1 +1,19 @@ - + +<% current_user = @user || User.current %> +<% telegram_account = + case context + when 'account_connection' + current_user.telegram_account + when '2fa_connection' + current_user.telegram_connection + end +%> +
+ +<% if current_user.logged? && telegram_account.present? %> + <%= link_to send_telegram_sign_in_link_path(params.slice(:autologin, :back_url)), method: :post, remote: true do %> + <%= t('redmine_bots.telegram.bot.login.send_to_telegram') %> + <% end %> (<%= t('redmine_bots.telegram.bot.login.widget_not_visible') %>) +<% else %> + <%= t('redmine_bots.telegram.bot.login.write_to_bot', bot: Setting.plugin_redmine_bots['telegram_bot_name']) %> +<% end %> diff --git a/app/views/telegram_login/index.html.erb b/app/views/telegram_login/index.html.erb index 88bc1c5..f6dc774 100644 --- a/app/views/telegram_login/index.html.erb +++ b/app/views/telegram_login/index.html.erb @@ -1,3 +1,3 @@
- <%= render partial: 'telegram_login/widget' %> + <%= render partial: 'telegram_login/widget', locals: { context: 'account_connection' } %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index fab0a79..0597bde 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -58,6 +58,7 @@ en: start: "Start work with bot" connect: "Connect Redmine and Telegram account" help: "Help about commands" + token: Get auth link login: success: Authentication succeed errors: @@ -66,6 +67,11 @@ en: hash_outdated: Request is outdated wrong_account: Wrong Telegram account not_persisted: Failed to persist Telegram account data + invalid_token: Token is invalid + follow_link: Please, follow link + send_to_telegram: Send auth link to Telegram + widget_not_visible: if widget isn not visible + write_to_bot: "Please, write command /token to bot @%{bot} if widget is not visible" slack: commands: connect: connect Slack account with Redmine diff --git a/config/locales/ru.yml b/config/locales/ru.yml index e693dda..5bc0c2b 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -58,6 +58,7 @@ ru: start: "Начало работы с ботом" connect: "Связывание аккаунтов Redmine и Telegram" help: "Справка по командам" + token: Получить ссылку для аутентификации login: success: Аутентификация прошла успешно errors: @@ -66,6 +67,11 @@ ru: hash_outdated: Истекло время ожидания wrong_account: Неверный аккаунт Telegram not_persisted: Не удалось сохранить данные о Telegram-аккаунте + invalid_token: Неверный token + follow_link: Пожалуйста, проследуйте по ссылке + send_to_telegram: Отправить ссылку в Telegram + widget_not_visible: если виджет недоступен + write_to_bot: "Пожалуйста, напишите команду /token боту @%{bot}, если не видно виджет" slack: commands: connect: связывание аккаунтов Slack и Redmine diff --git a/config/routes.rb b/config/routes.rb index b745a35..cd37f2e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,4 +21,6 @@ get 'login' => 'telegram_login#index', as: 'telegram_login' get 'check_auth' => 'telegram_login#check_auth' + get 'check_jwt' => 'telegram_login#check_jwt' + post 'send_sign_in_link' => 'telegram_login#send_sign_in_link', as: 'send_telegram_sign_in_link' end diff --git a/lib/redmine_bots/result.rb b/lib/redmine_bots/result.rb new file mode 100644 index 0000000..b94f02b --- /dev/null +++ b/lib/redmine_bots/result.rb @@ -0,0 +1,17 @@ +module RedmineBots + class Result + attr_reader :value + + def initialize(success, value) + @success, @value = success, value + end + + def success? + @success + end + + def failure? + !success? + end + end +end diff --git a/lib/redmine_bots/telegram.rb b/lib/redmine_bots/telegram.rb index 2c04584..6650467 100644 --- a/lib/redmine_bots/telegram.rb +++ b/lib/redmine_bots/telegram.rb @@ -2,10 +2,6 @@ module RedmineBots::Telegram extend Tdlib::DependencyProviders::GetMe extend Tdlib::DependencyProviders::AddBot - def self.table_name_prefix - 'telegram_common_' - end - def self.set_locale I18n.locale = Setting['default_language'] end diff --git a/lib/redmine_bots/telegram/bot.rb b/lib/redmine_bots/telegram/bot.rb index 6686d47..a572580 100644 --- a/lib/redmine_bots/telegram/bot.rb +++ b/lib/redmine_bots/telegram/bot.rb @@ -5,6 +5,7 @@ class Bot include BotCommand::Start include BotCommand::Connect include BotCommand::Help + include BotCommand::Token attr_reader :bot_token, :logger, :command diff --git a/lib/redmine_bots/telegram/bot/authenticate.rb b/lib/redmine_bots/telegram/bot/authenticate.rb index 6291637..6c8deed 100644 --- a/lib/redmine_bots/telegram/bot/authenticate.rb +++ b/lib/redmine_bots/telegram/bot/authenticate.rb @@ -1,77 +1,80 @@ -module RedmineBots::Telegram - class Bot::Authenticate - AUTH_TIMEOUT = 60.minutes +module RedmineBots + module Telegram + class Bot::Authenticate + AUTH_TIMEOUT = 60.minutes - def self.call(user, auth_data) - new(user, auth_data).call - end - - def initialize(user, auth_data) - @user, @auth_data = user, Hash[auth_data.sort_by { |k, _| k }] - end + def self.call(user, auth_data, context:) + new(user, auth_data, context: context).call + end - def call - return failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_logged')) unless @user.logged? - return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_invalid')) unless hash_valid? - return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_outdated')) unless up_to_date? + def initialize(user, auth_data, context:) + @user, @auth_data, @context = user, Hash[auth_data.sort_by { |k, _| k }], context + end - telegram_account = TelegramAccount.find_by(user_id: @user.id) + def call + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_logged')) unless @user.logged? + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_invalid')) unless hash_valid? + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_outdated')) unless up_to_date? - if telegram_account.present? - if telegram_account.telegram_id - unless @auth_data['id'].to_i == telegram_account.telegram_id - return failure(I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account')) - end + case @context + when '2fa_connection' + telegram_account = prepare_telegram_account(model_class: Redmine2FA::TelegramConnection) + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account')) unless telegram_account + when 'account_connection' + telegram_account = prepare_telegram_account(model_class: TelegramAccount) + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account')) unless telegram_account + telegram_account.assign_attributes(@auth_data.slice('first_name', 'last_name', 'username')) else - telegram_account.telegram_id = @auth_data['id'] + return failure('Invalid context') end - else - telegram_account = TelegramAccount.find_or_initialize_by(telegram_id: @auth_data['id']) - if telegram_account.user_id - unless telegram_account.user_id == @user.id - return failure(I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account')) - end + + if telegram_account.save + success(telegram_account) else - telegram_account.user_id = @user.id + failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_persisted')) end end - telegram_account.assign_attributes(@auth_data.slice(*%w[first_name last_name username])) - - if telegram_account.save - success(telegram_account) - else - failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_persisted')) - end - end - - private + private - def hash_valid? - Utils.auth_hash(@auth_data) == @auth_data['hash'] - end - - def up_to_date? - Time.at(@auth_data['auth_date'].to_i) > Time.now - AUTH_TIMEOUT - end + def prepare_telegram_account(model_class:) + telegram_account = model_class.find_by(user_id: @user.id) - def success(value) - Result.new(true, value) - end + if telegram_account.present? + if telegram_account.telegram_id + unless @auth_data['id'].to_i == telegram_account.telegram_id + return nil + end + else + telegram_account.telegram_id = @auth_data['id'] + end + else + telegram_account = model_class.find_or_initialize_by(telegram_id: @auth_data['id']) + if telegram_account.user_id + unless telegram_account.user_id == @user.id + return nil + end + else + telegram_account.user_id = @user.id + end + end + telegram_account + end - def failure(value) - Result.new(false, value) - end + def hash_valid? + Utils.auth_hash(@auth_data) == @auth_data['hash'] + end - class Result - attr_reader :value + def up_to_date? + Time.at(@auth_data['auth_date'].to_i) > Time.now - AUTH_TIMEOUT + end - def initialize(success, value) - @success, @value = success, value + def success(value) + Result.new(true, value) end - def success? - @success + def failure(value) + Result.new(false, value) end end end diff --git a/lib/redmine_bots/telegram/bot/authenticate_by_token.rb b/lib/redmine_bots/telegram/bot/authenticate_by_token.rb new file mode 100644 index 0000000..dae3c92 --- /dev/null +++ b/lib/redmine_bots/telegram/bot/authenticate_by_token.rb @@ -0,0 +1,70 @@ +module RedmineBots + module Telegram + class Bot::AuthenticateByToken + def self.call(*args) + new(*args).call + end + + def initialize(user, token, context:) + @user, @token, @context = user, token, context + end + + def call + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_logged')) if @user.anonymous? + + case @context + when '2fa_connection' + telegram_account = prepare_telegram_account(model_class: Redmine2FA::TelegramConnection) + when 'account_connection' + telegram_account = prepare_telegram_account(model_class: TelegramAccount) + else + return failure('Invalid context') + end + + return failure(I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account')) unless telegram_account + + if telegram_account.save + success(telegram_account) + else + failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_persisted')) + end + rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::InvalidIatError + failure(I18n.t('redmine_bots.telegram.bot.login.errors.invalid_token')) + end + + def prepare_telegram_account(model_class:) + telegram_data = Jwt.decode_token(@token).first + telegram_id = telegram_data['telegram_id'].to_i + telegram_account = model_class.find_by(user_id: @user.id) + + if telegram_account.present? + if telegram_account.telegram_id + unless telegram_id == telegram_account.telegram_id + return nil + end + else + telegram_account.telegram_id = telegram_id + end + else + telegram_account = model_class.find_or_initialize_by(telegram_id: telegram_id) + if telegram_account.user_id + unless telegram_account.user_id == @user.id + return nil + end + else + telegram_account.user_id = @user.id + end + end + telegram_account + end + + def success(value) + Result.new(true, value) + end + + def failure(value) + Result.new(false, value) + end + end + end +end diff --git a/lib/redmine_bots/telegram/bot/send_sign_in_link.rb b/lib/redmine_bots/telegram/bot/send_sign_in_link.rb new file mode 100644 index 0000000..95b7301 --- /dev/null +++ b/lib/redmine_bots/telegram/bot/send_sign_in_link.rb @@ -0,0 +1,35 @@ +module RedmineBots + class Telegram::Bot::SendSignInLink + include RedmineBots::Telegram::Jwt + + def self.call(*args) + new(*args).call + end + + def initialize(user, context:, params: {}) + @user, @context, @params = user, context, params + end + + def call + telegram_account = + case @context + when '2fa_connection' + @user.telegram_account + when 'account_connection' + @user.telegram_connection + else + nil + end + return unless telegram_account + + token = encode(telegram_id: telegram_account.telegram_id) + message_params = { + chat_id: telegram_account.telegram_id, + message: "#{I18n.t('redmine_bots.telegram.bot.login.follow_link')}: #{Setting.protocol}://#{Setting.host_name}/telegram/check_jwt?#{{ token: token }.merge(@params).to_query}", + bot_token: RedmineBots::Telegram.bot_token + } + + RedmineBots::Telegram::Bot::MessageSender.call(message_params) + end + end +end diff --git a/lib/redmine_bots/telegram/bot_command/help.rb b/lib/redmine_bots/telegram/bot_command/help.rb index 107ac52..c91ff2f 100644 --- a/lib/redmine_bots/telegram/bot_command/help.rb +++ b/lib/redmine_bots/telegram/bot_command/help.rb @@ -9,7 +9,7 @@ def help private def private_commands - %w(start connect help) + %w(start connect help token) end def group_commands diff --git a/lib/redmine_bots/telegram/bot_command/token.rb b/lib/redmine_bots/telegram/bot_command/token.rb new file mode 100644 index 0000000..62f2d95 --- /dev/null +++ b/lib/redmine_bots/telegram/bot_command/token.rb @@ -0,0 +1,10 @@ +module RedmineBots::Telegram + module BotCommand + module Token + def token + token = Jwt.encode(telegram_id: chat_id) + send_message("#{I18n.t('redmine_bots.telegram.bot.login.follow_link')}: #{Setting.protocol}://#{Setting.host_name}/telegram/check_jwt?token=#{token}") + end + end + end +end \ No newline at end of file diff --git a/lib/redmine_bots/telegram/jwt.rb b/lib/redmine_bots/telegram/jwt.rb new file mode 100644 index 0000000..614e939 --- /dev/null +++ b/lib/redmine_bots/telegram/jwt.rb @@ -0,0 +1,24 @@ +module RedmineBots::Telegram + module Jwt + extend self + + def encode(payload) + exp = Time.now.to_i + 300 + JWT.encode({ **payload, iss: issuer, exp: exp }, secret) + end + + def decode_token(token) + JWT.decode(token, secret, true, algorithm: 'HS256', iss: issuer, verify_iss: true) + end + + private + + def secret + Rails.application.config.secret_key_base + end + + def issuer + Setting.host_name + end + end +end diff --git a/lib/redmine_bots/telegram/tdlib/add_bot.rb b/lib/redmine_bots/telegram/tdlib/add_bot.rb index b6600ae..104a6d9 100644 --- a/lib/redmine_bots/telegram/tdlib/add_bot.rb +++ b/lib/redmine_bots/telegram/tdlib/add_bot.rb @@ -2,8 +2,8 @@ module RedmineBots::Telegram::Tdlib class AddBot < Command def call(bot_id) @client.on_ready do |client| - chat = client.broadcast_and_receive('@type' => 'createPrivateChat', 'user_id' => bot_id) - client.broadcast_and_receive('@type' => 'sendBotStartMessage', + chat = client.fetch('@type' => 'createPrivateChat', 'user_id' => bot_id) + client.fetch('@type' => 'sendBotStartMessage', 'bot_user_id' => bot_id, 'chat_id' => chat['id']) end diff --git a/lib/redmine_bots/telegram/tdlib/authenticate.rb b/lib/redmine_bots/telegram/tdlib/authenticate.rb index 4abc87f..990be0d 100644 --- a/lib/redmine_bots/telegram/tdlib/authenticate.rb +++ b/lib/redmine_bots/telegram/tdlib/authenticate.rb @@ -1,6 +1,6 @@ module RedmineBots::Telegram::Tdlib class Authenticate < Command - TIMEOUT = 10 + TIMEOUT = 20 class AuthenticationError < StandardError end @@ -18,17 +18,16 @@ def call(params) auth_state = update.dig('authorization_state', '@type') end - Timeout.timeout(TIMEOUT) do - loop do - case auth_state - when 'authorizationStateWaitPhoneNumber' - set_phone(params['phone_number']) - when 'authorizationStateWaitCode' - return unless params.key?('phone_code') - check_code(params['phone_code']) - when 'authorizationStateReady' - return - end + loop do + case auth_state + when 'authorizationStateWaitPhoneNumber' + set_phone(params['phone_number']) + when 'authorizationStateWaitCode' + return unless params.key?('phone_code') + check_code(params['phone_code']) + when 'authorizationStateReady' + fetch_all_chats + return end end rescue Timeout::Error @@ -40,12 +39,25 @@ def call(params) private + def fetch_all_chats + offset_order = 2**63 - 1 + offset_chat_id = 0 + limit = 100 + + loop do + chat_ids = @client.fetch('@type' => 'getChats', 'offset_order' => offset_order, 'offset_chat_id' => offset_chat_id, 'limit' => limit).tap(&error_handler)['chat_ids'] + break if chat_ids.empty? + last_chat = @client.fetch('@type' => 'getChat', 'chat_id' => chat_ids.last).tap(&error_handler) + offset_chat_id, offset_order = last_chat.values_at('id', 'order') + end + end + def set_phone(phone) params = { '@type' => 'setAuthenticationPhoneNumber', 'phone_number' => phone } - @client.broadcast_and_receive(params).tap(&error_handler) + @client.fetch(params).tap(&error_handler) end def check_code(code) @@ -53,7 +65,7 @@ def check_code(code) '@type' => 'checkAuthenticationCode', 'code' => code } - @client.broadcast_and_receive(params).tap(&error_handler) + @client.fetch(params).tap(&error_handler) end def error_handler diff --git a/lib/redmine_bots/telegram/tdlib/close_chat.rb b/lib/redmine_bots/telegram/tdlib/close_chat.rb index d37c8b6..4fa52a2 100644 --- a/lib/redmine_bots/telegram/tdlib/close_chat.rb +++ b/lib/redmine_bots/telegram/tdlib/close_chat.rb @@ -2,12 +2,12 @@ module RedmineBots::Telegram::Tdlib class CloseChat < Command def call(chat_id) @client.on_ready do |client| - me = client.broadcast_and_receive('@type' => 'getMe') + me = client.fetch('@type' => 'getMe') bot_id = Setting.find_by(name: 'plugin_redmine_bots').value['telegram_bot_id'].to_i - chat = client.broadcast_and_receive('@type' => 'getChat', 'chat_id' => chat_id) + chat = client.fetch('@type' => 'getChat', 'chat_id' => chat_id) - group_info = client.broadcast_and_receive('@type' => 'getBasicGroupFullInfo', + group_info = client.fetch('@type' => 'getBasicGroupFullInfo', 'basic_group_id' => chat.dig('type', 'basic_group_id') ) return if group_info['@type'] == 'error' @@ -24,7 +24,7 @@ def call(chat_id) private def delete_member(chat_id, user_id) - @client.broadcast_and_receive('@type' => 'setChatMemberStatus', + @client.fetch('@type' => 'setChatMemberStatus', 'chat_id' => chat_id, 'user_id' => user_id, 'status' => { '@type' => 'chatMemberStatusLeft' }) diff --git a/lib/redmine_bots/telegram/tdlib/command.rb b/lib/redmine_bots/telegram/tdlib/command.rb index 8948f97..07a9ddd 100644 --- a/lib/redmine_bots/telegram/tdlib/command.rb +++ b/lib/redmine_bots/telegram/tdlib/command.rb @@ -10,15 +10,15 @@ def initialize(client) module Callable def call(*) - begin - tries ||= 3 + tries ||= 3 + Filelock Rails.root.join('tmp', 'redmine_bots', 'tdlib_lock'), wait: 10 do super - rescue Timeout::Error - sleep 2 - retry unless (tries -= 1).zero? - ensure - @client.close end + rescue Timeout::Error + sleep 2 + retry unless (tries -= 1).zero? + ensure + @client.close end end end diff --git a/lib/redmine_bots/telegram/tdlib/create_chat.rb b/lib/redmine_bots/telegram/tdlib/create_chat.rb index c5ca15d..7142a02 100644 --- a/lib/redmine_bots/telegram/tdlib/create_chat.rb +++ b/lib/redmine_bots/telegram/tdlib/create_chat.rb @@ -3,18 +3,18 @@ class CreateChat < Command def call(title, user_ids) @client.on_ready(timeout: 5) do |client| user_ids.each do |id| - client.broadcast_and_receive('@type' => 'getUser', 'user_id' => id) + client.fetch('@type' => 'getUser', 'user_id' => id) end sleep 1 - chat = client.broadcast_and_receive('@type' => 'createNewBasicGroupChat', + chat = client.fetch('@type' => 'createNewBasicGroupChat', 'title' => title, 'user_ids' => user_ids) sleep 1 - client.broadcast_and_receive('@type' => 'toggleBasicGroupAdministrators', + client.fetch('@type' => 'toggleBasicGroupAdministrators', 'basic_group_id' => chat.dig('type', 'basic_group_id'), 'everyone_is_administrator' => false) chat diff --git a/lib/redmine_bots/telegram/tdlib/dependency_providers.rb b/lib/redmine_bots/telegram/tdlib/dependency_providers.rb index e19f6c3..bef9d71 100644 --- a/lib/redmine_bots/telegram/tdlib/dependency_providers.rb +++ b/lib/redmine_bots/telegram/tdlib/dependency_providers.rb @@ -2,22 +2,24 @@ module RedmineBots::Telegram::Tdlib module DependencyProviders module Client def client - settings = Setting.plugin_redmine_bots - TD::Api.set_log_file_path(Rails.root.join('log', 'redmine_bots', 'tdlib.log').to_s) - config = { - api_id: settings['telegram_api_id'], - api_hash: settings['telegram_api_hash'], - database_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'db').to_s, - files_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'files').to_s, - } - proxy = { - '@type' => 'proxySocks5', - 'server' => settings['tdlib_proxy_server'], - 'port' => settings['tdlib_proxy_port'], - 'username' => settings['tdlib_proxy_user'], - 'password' => settings['tdlib_proxy_password'] - } - TD::Client.new(**(settings['tdlib_use_proxy'] ? { proxy: proxy } : {}), **config) + LazyObject.new do + settings = Setting.plugin_redmine_bots + TD::Api.set_log_file_path(Rails.root.join('log', 'redmine_bots', 'tdlib.log').to_s) + config = { + api_id: settings['telegram_api_id'], + api_hash: settings['telegram_api_hash'], + database_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'db').to_s, + files_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'files').to_s, + } + proxy = { + '@type' => 'proxySocks5', + 'server' => settings['tdlib_proxy_server'], + 'port' => settings['tdlib_proxy_port'], + 'username' => settings['tdlib_proxy_user'], + 'password' => settings['tdlib_proxy_password'] + } + TD::Client.new(**(settings['tdlib_use_proxy'] ? { proxy: proxy } : {}), **config) + end end end diff --git a/lib/redmine_bots/telegram/tdlib/get_chat.rb b/lib/redmine_bots/telegram/tdlib/get_chat.rb index 871b619..b877f9b 100644 --- a/lib/redmine_bots/telegram/tdlib/get_chat.rb +++ b/lib/redmine_bots/telegram/tdlib/get_chat.rb @@ -2,7 +2,7 @@ module RedmineBots::Telegram::Tdlib class GetChat < Command def call(id) @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'getChat', 'chat_id' => id) + client.fetch('@type' => 'getChat', 'chat_id' => id) end end end diff --git a/lib/redmine_bots/telegram/tdlib/get_chat_link.rb b/lib/redmine_bots/telegram/tdlib/get_chat_link.rb index d64fd05..b891288 100644 --- a/lib/redmine_bots/telegram/tdlib/get_chat_link.rb +++ b/lib/redmine_bots/telegram/tdlib/get_chat_link.rb @@ -2,7 +2,7 @@ module RedmineBots::Telegram::Tdlib class GetChatLink < Command def call(chat_id) @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'generateChatInviteLink', 'chat_id' => chat_id) + client.fetch('@type' => 'generateChatInviteLink', 'chat_id' => chat_id) end end end diff --git a/lib/redmine_bots/telegram/tdlib/get_me.rb b/lib/redmine_bots/telegram/tdlib/get_me.rb index fd6381d..d92f87e 100644 --- a/lib/redmine_bots/telegram/tdlib/get_me.rb +++ b/lib/redmine_bots/telegram/tdlib/get_me.rb @@ -2,7 +2,7 @@ module RedmineBots::Telegram::Tdlib class GetMe < Command def call @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'getMe') + client.fetch('@type' => 'getMe') end end end diff --git a/lib/redmine_bots/telegram/tdlib/get_user.rb b/lib/redmine_bots/telegram/tdlib/get_user.rb index 93cc9db..63132f9 100644 --- a/lib/redmine_bots/telegram/tdlib/get_user.rb +++ b/lib/redmine_bots/telegram/tdlib/get_user.rb @@ -2,7 +2,7 @@ module RedmineBots::Telegram::Tdlib class GetUser < Command def call(user_id) @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'getUser', 'user_id' => user_id) + client.fetch('@type' => 'getUser', 'user_id' => user_id) end end end diff --git a/lib/redmine_bots/telegram/tdlib/rename_chat.rb b/lib/redmine_bots/telegram/tdlib/rename_chat.rb index 2d4355c..0f335cb 100644 --- a/lib/redmine_bots/telegram/tdlib/rename_chat.rb +++ b/lib/redmine_bots/telegram/tdlib/rename_chat.rb @@ -2,7 +2,7 @@ module RedmineBots::Telegram::Tdlib class RenameChat < Command def call(chat_id, new_title) @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'setChatTitle', + client.fetch('@type' => 'setChatTitle', 'chat_id' => chat_id, 'title' => new_title) end diff --git a/lib/redmine_bots/telegram/tdlib/toggle_chat_admin.rb b/lib/redmine_bots/telegram/tdlib/toggle_chat_admin.rb index c138aee..3cdf71c 100644 --- a/lib/redmine_bots/telegram/tdlib/toggle_chat_admin.rb +++ b/lib/redmine_bots/telegram/tdlib/toggle_chat_admin.rb @@ -15,8 +15,8 @@ def call(chat_id, user_id, admin = true) { '@type' => 'chatMemberStatusMember' } end @client.on_ready do |client| - client.broadcast_and_receive('@type' => 'getUser', 'user_id' => user_id) - client.broadcast_and_receive('@type' => 'setChatMemberStatus', + client.fetch('@type' => 'getUser', 'user_id' => user_id) + client.fetch('@type' => 'setChatMemberStatus', 'chat_id' => chat_id, 'user_id' => user_id, 'status' => status) diff --git a/lib/redmine_bots/telegram/update_manager.rb b/lib/redmine_bots/telegram/update_manager.rb index c503c89..4578d9c 100644 --- a/lib/redmine_bots/telegram/update_manager.rb +++ b/lib/redmine_bots/telegram/update_manager.rb @@ -1,5 +1,5 @@ class RedmineBots::Telegram::UpdateManager - COMMON_COMMANDS = %w[help start connect] + COMMON_COMMANDS = %w[help start connect token] def initialize @handlers = [] diff --git a/test/fixtures/telegram_common_accounts.yml b/test/fixtures/telegram_accounts.yml similarity index 100% rename from test/fixtures/telegram_common_accounts.yml rename to test/fixtures/telegram_accounts.yml diff --git a/test/test_helper.rb b/test/test_helper.rb index 01768b9..32e0979 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,7 +7,7 @@ require "minitest/reporters" Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new -ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', %i[telegram_common_accounts users], telegram_common_accounts: TelegramAccount) +ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', %i[telegram_accounts users]) class ActiveSupport::TestCase extend Minitest::Spec::DSL diff --git a/test/unit/redmine_bots/bot/authenticate_by_token_test.rb b/test/unit/redmine_bots/bot/authenticate_by_token_test.rb new file mode 100644 index 0000000..f404c87 --- /dev/null +++ b/test/unit/redmine_bots/bot/authenticate_by_token_test.rb @@ -0,0 +1,76 @@ +require File.expand_path('../../../../test_helper', __FILE__) + +class RedmineBots::Telegram::Bot::AuthenticateByTokenTest < ActiveSupport::TestCase + fixtures :telegram_accounts, :users + + let(:described_class) { RedmineBots::Telegram::Bot::AuthenticateByToken } + + context 'when user is anonymous' do + it 'returns failure result' do + result = described_class.new(users(:anonymous), 'token', context: 'account_connection').call + + expect(result.success?).must_equal false + expect(result.value).must_equal "You're not logged in" + end + end + + context 'when token is invalid' do + it 'returns failure result' do + result = described_class.new(users(:logged), 'invalid_token', context: 'account_connection').call + + expect(result.success?).must_equal false + expect(result.value).must_equal 'Token is invalid' + end + end + + context 'when telegram account found by user_id' do + context 'when telegram ids do not match' do + it 'returns failure result' do + RedmineBots::Telegram::Jwt.stubs(:decode_token).returns([{ 'telegram_id' => 2 }, {}]) + + result = described_class.new(users(:logged), 'token', context: 'account_connection').call + + expect(result.success?).must_equal false + expect(result.value).must_equal "Wrong Telegram account" + end + end + + context 'when telegram ids match' do + it 'updates attributes and returns successful result' do + RedmineBots::Telegram::Jwt.stubs(:decode_token).returns([{ 'telegram_id' => 1 }, {}]) + + result = described_class.new(users(:logged), 'token', context: 'account_connection').call + + account = TelegramAccount.find(1) + + expect(result.value).must_equal account + expect(result.success?).must_equal true + end + end + end + + context 'when telegram account not found by user_id' do + context 'when user ids do not match' do + it 'returns failure result' do + RedmineBots::Telegram::Jwt.stubs(:decode_token).returns([{ 'telegram_id' => 1 }, {}]) + result = described_class.new(users(:user_3), 'token', context: 'account_connection').call + + expect(result.success?).must_equal false + expect(result.value).must_equal "Wrong Telegram account" + end + end + + context 'when telegram account does not have user_id' do + it 'updates attributes and returns successful result' do + RedmineBots::Telegram::Jwt.stubs(:decode_token).returns([{ 'telegram_id' => 3 }, {}]) + result = described_class.new(users(:user_3), 'token', context: 'account_connection').call + + account = TelegramAccount.last + + expect(result.value).must_equal account + expect(account.user_id).must_equal users(:user_3).id + expect(result.success?).must_equal true + end + end + end +end \ No newline at end of file diff --git a/test/unit/redmine_bots/bot/authenticate_test.rb b/test/unit/redmine_bots/bot/authenticate_test.rb index b9a612d..406ee55 100644 --- a/test/unit/redmine_bots/bot/authenticate_test.rb +++ b/test/unit/redmine_bots/bot/authenticate_test.rb @@ -1,7 +1,7 @@ require File.expand_path('../../../../test_helper', __FILE__) class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase - fixtures :telegram_common_accounts, :users + fixtures :telegram_accounts, :users let(:described_class) { RedmineBots::Telegram::Bot::Authenticate } @@ -11,7 +11,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when user is anonymous' do it 'returns failure result' do - result = described_class.new(users(:anonymous), {}).call + result = described_class.new(users(:anonymous), {}, context: 'account_connection').call expect(result.success?).must_equal false expect(result.value).must_equal "You're not logged in" @@ -20,7 +20,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when hash is not valid' do it 'returns failure result' do - result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'wrong_hash', 'auth_date' => Time.now.to_i }).call + result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'wrong_hash', 'auth_date' => Time.now.to_i }, context: 'account_connection').call expect(result.success?).must_equal false expect(result.value).must_equal "Hash is invalid" @@ -29,7 +29,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when hash is outdated' do it 'returns failure result' do - result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => (Time.now - 61.minutes).to_i }).call + result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => (Time.now - 61.minutes).to_i }, context: 'account_connection').call expect(result.success?).must_equal false expect(result.value).must_equal "Request is outdated" @@ -39,7 +39,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when telegram account found by user_id' do context 'when telegram ids do not match' do it 'returns failure result' do - result = described_class.new(users(:logged), { 'id' => 2, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }).call + result = described_class.new(users(:logged), { 'id' => 2, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }, context: 'account_connection').call expect(result.success?).must_equal false expect(result.value).must_equal "Wrong Telegram account" @@ -48,7 +48,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when telegram ids match' do it 'updates attributes and returns successful result' do - result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }).call + result = described_class.new(users(:logged), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }, context: 'account_connection').call account = TelegramAccount.find(1) @@ -63,7 +63,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when telegram account not found by user_id' do context 'when user ids do not match' do it 'returns failure result' do - result = described_class.new(users(:user_3), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }).call + result = described_class.new(users(:user_3), { 'id' => 1, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }, context: 'account_connection').call expect(result.success?).must_equal false expect(result.value).must_equal "Wrong Telegram account" @@ -72,7 +72,7 @@ class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase context 'when telegram account does not have user_id' do it 'updates attributes and returns successful result' do - result = described_class.new(users(:user_3), { 'id' => 3, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }).call + result = described_class.new(users(:user_3), { 'id' => 3, 'first_name' => 'test', 'last_name' => 'test', 'hash' => 'auth_hash', 'auth_date' => Time.now.to_i }, context: 'account_connection').call account = TelegramAccount.last diff --git a/test/unit/redmine_bots/bot_test.rb b/test/unit/redmine_bots/bot_test.rb index 9f317a0..2cbb4af 100644 --- a/test/unit/redmine_bots/bot_test.rb +++ b/test/unit/redmine_bots/bot_test.rb @@ -107,6 +107,7 @@ class RedmineBots::Telegram::BotTest < ActiveSupport::TestCase /start - #{I18n.t('redmine_bots.telegram.bot.private.help.start')} /connect - #{I18n.t('redmine_bots.telegram.bot.private.help.connect')} /help - #{I18n.t('redmine_bots.telegram.bot.private.help.help')} + /token - #{I18n.t('redmine_bots.telegram.bot.private.help.token')} TEXT message = text.chomp @@ -143,6 +144,7 @@ class RedmineBots::Telegram::BotTest < ActiveSupport::TestCase /start - #{I18n.t('redmine_bots.telegram.bot.private.help.start')} /connect - #{I18n.t('redmine_bots.telegram.bot.private.help.connect')} /help - #{I18n.t('redmine_bots.telegram.bot.private.help.help')} + /token - #{I18n.t('redmine_bots.telegram.bot.private.help.token')} TEXT message = text.chomp