├── .gitignore ├── app ├── models │ ├── slack_account.rb │ ├── telegram_account.rb │ └── telegram_proxy.rb ├── views │ ├── slack_sign_in │ │ ├── index.html.erb │ │ └── _widget.erb │ ├── telegram_login │ │ ├── index.html.erb │ │ └── _widget.erb │ ├── users │ │ └── _telegram_account.erb │ ├── settings │ │ ├── redmine_bots │ │ │ ├── _deprecation_warning.erb │ │ │ ├── _slack.erb │ │ │ └── _telegram.erb │ │ └── _redmine_bots.erb │ └── redmine_telegram_setup │ │ ├── step_1.html.erb │ │ ├── step_2.html.erb │ │ └── step_3.html.erb ├── workers │ ├── telegram_handler_worker.rb │ ├── async_bot_handler_worker.rb │ └── telegram_accounts_refresh_worker.rb └── controllers │ ├── slack_sign_in_controller.rb │ ├── telegram_webhook_controller.rb │ ├── telegram_login_controller.rb │ └── redmine_telegram_setup_controller.rb ├── lib ├── redmine_bots │ ├── telegram │ │ ├── bot │ │ │ ├── handlers.rb │ │ │ ├── null_throttle.rb │ │ │ ├── token.rb │ │ │ ├── async_handler.rb │ │ │ ├── help_message.rb │ │ │ ├── handlers │ │ │ │ ├── handler_behaviour.rb │ │ │ │ ├── help_command.rb │ │ │ │ ├── start_command.rb │ │ │ │ └── connect_command.rb │ │ │ ├── user_action.rb │ │ │ └── authenticate.rb │ │ ├── tdlib │ │ │ ├── get_me.rb │ │ │ ├── get_chat.rb │ │ │ ├── get_user.rb │ │ │ ├── rename_chat.rb │ │ │ ├── get_chat_link.rb │ │ │ ├── add_to_chat.rb │ │ │ ├── fetch_all_chats.rb │ │ │ ├── add_bot.rb │ │ │ ├── command.rb │ │ │ ├── toggle_chat_admin.rb │ │ │ ├── create_chat.rb │ │ │ ├── close_chat.rb │ │ │ └── authenticate.rb │ │ ├── exceptions │ │ │ └── telegram.rb │ │ ├── hooks │ │ │ └── views_users_hook.rb │ │ ├── utils.rb │ │ ├── tdlib.rb │ │ ├── patches │ │ │ └── user_patch.rb │ │ ├── jwt.rb │ │ └── bot.rb │ ├── slack │ │ ├── server.rb │ │ ├── commands │ │ │ ├── help.rb │ │ │ ├── connect.rb │ │ │ └── base.rb │ │ ├── event_handler.rb │ │ ├── bot.rb │ │ └── sign_in.rb │ ├── result.rb │ ├── slack.rb │ ├── utils.rb │ └── telegram.rb ├── redmine_bots.rb └── tasks │ ├── slack.rake │ └── telegram.rake ├── test ├── fixtures │ ├── telegram_accounts.yml │ └── users.yml ├── support │ ├── database.yml.travis │ ├── additional_environment.rb │ └── example_telegram_bot_ruby.rb ├── test_helper.rb └── unit │ └── redmine_bots │ ├── account_test.rb │ └── bot │ └── authenticate_test.rb ├── db └── migrate │ ├── 004_change_telegram_id_to_decimal.rb │ ├── 001_create_slack_accounts.rb │ ├── 002_create_telegram_accounts.rb │ └── 003_create_telegram_proxies.rb ├── Gemfile ├── .travis.yml ├── config ├── routes.rb └── locales │ ├── zh-TW.yml │ ├── en.yml │ └── ru.yml ├── travis.sh ├── init.rb ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /app/models/slack_account.rb: -------------------------------------------------------------------------------- 1 | class SlackAccount < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/handlers.rb: -------------------------------------------------------------------------------- 1 | class RedmineBots::Telegram::Bot 2 | module Handlers 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/server.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack 2 | class Server < SlackRubyBot::Server 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/views/slack_sign_in/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
--------------------------------------------------------------------------------
/app/workers/telegram_handler_worker.rb:
--------------------------------------------------------------------------------
1 | class TelegramHandlerWorker
2 | include Sidekiq::Worker
3 | sidekiq_options queue: :telegram
4 |
5 | def perform(params)
6 | RedmineBots::Telegram.bot.handle_update(params)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/redmine_bots/telegram/exceptions/telegram.rb:
--------------------------------------------------------------------------------
1 | module RedmineBots::Telegram
2 | module Exceptions
3 | class Telegram < StandardError
4 | def initialize(msg)
5 | super(msg)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/redmine_bots/telegram/tdlib/rename_chat.rb:
--------------------------------------------------------------------------------
1 | module RedmineBots::Telegram::Tdlib
2 | class RenameChat < Command
3 | def call(chat_id, new_title)
4 | client.set_chat_title(chat_id: chat_id, title: new_title)
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/redmine_bots/telegram/bot/token.rb:
--------------------------------------------------------------------------------
1 | class RedmineBots::Telegram::Bot
2 | class Token
3 | include Singleton
4 |
5 | def to_s
6 | Setting.find_by_name(:plugin_redmine_bots).value['telegram_bot_token']
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/004_change_telegram_id_to_decimal.rb:
--------------------------------------------------------------------------------
1 | class ChangeTelegramIdToDecimal < Rails.version < '5.0' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
2 | def change
3 | change_column :telegram_accounts, :telegram_id, :decimal
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/redmine_bots/telegram/tdlib/get_chat_link.rb:
--------------------------------------------------------------------------------
1 | module RedmineBots::Telegram::Tdlib
2 | class GetChatLink < Command
3 | def call(chat_id)
4 | client.create_chat_invite_link(chat_id: chat_id, expiration_date: 0, member_limit: 0, creates_join_request: false, name: "issue")
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/workers/async_bot_handler_worker.rb:
--------------------------------------------------------------------------------
1 | class AsyncBotHandlerWorker
2 | include Sidekiq::Worker
3 |
4 | sidekiq_options queue: :telegram
5 |
6 | def perform(method, args)
7 | RedmineBots::Telegram.set_locale
8 |
9 | RedmineBots::Telegram.bot.send(method, **args.symbolize_keys)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/slack_sign_in_controller.rb:
--------------------------------------------------------------------------------
1 | class SlackSignInController < ApplicationController
2 | def index
3 | end
4 |
5 | def check
6 | if params[:error].blank? && RedmineBots::Slack::SignIn.(params)
7 | redirect_to my_page_path
8 | else
9 | render_403
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/redmine_bots/telegram/hooks/views_users_hook.rb:
--------------------------------------------------------------------------------
1 | module RedmineBots
2 | module Telegram
3 | module Hooks
4 | class ViewsUsersHook < Redmine::Hook::ViewListener
5 | render_on :view_account_left_bottom, partial: "users/telegram_account", :user => @user
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/redmine_bots/result.rb:
--------------------------------------------------------------------------------
1 | module RedmineBots
2 | class Result
3 | attr_reader :value
4 |
5 | def initialize(success, value)
6 | @success, @value = success, value
7 | end
8 |
9 | def success?
10 | @success
11 | end
12 |
13 | def failure?
14 | !success?
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/users/_telegram_account.erb:
--------------------------------------------------------------------------------
1 | <% if User.current.allowed_to?(:view_telegram_account_info, nil, global: true) && account = TelegramAccount.find_by(user_id: user.id) %>
2 | 1. <%= t 'redmine_bots.settings.telegram.auth_step_1' %> — 2. <%= t 'redmine_bots.settings.telegram.auth_step_2' %> — 3. <%= t 'redmine_bots.settings.telegram.auth_step_3' %>
4 |8 | 11 | <%= text_field_tag 'phone_number' %> 12 | <%= t 'redmine_bots.settings.telegram.phone_number_hint' %> 13 |
14 | 15 | | <%= link_to plugin_settings_path('redmine_bots') do %><%= t 'redmine_bots.settings.telegram.plugin_link' %><% end %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/bot.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack 2 | class Bot < SlackRubyBot::Bot 3 | @@mutex = Mutex.new 4 | @@commands = [] 5 | @@handlers = [] 6 | 7 | cattr_reader :commands, :handlers 8 | 9 | def self.instance 10 | Server.new(token: Setting.plugin_redmine_bots['slack_bot_oauth_token']) 11 | end 12 | 13 | def self.register_commands(*commands) 14 | @@mutex.synchronize { @@commands |= commands } 15 | 16 | commands.each do |command| 17 | command.names.each { |name| command(name, &command) } 18 | end 19 | end 20 | 21 | def self.register_handlers(*handlers) 22 | @@mutex.synchronize { @@handlers |= handlers } 23 | 24 | handlers.each do |handler| 25 | Server.on(handler.event, &handler) 26 | end 27 | end 28 | 29 | register_commands Commands::Connect, Commands::Help 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/redmine_telegram_setup/step_2.html.erb: -------------------------------------------------------------------------------- 1 |<%= link_to telegram_setup_1_path do %>1. <%= t 'redmine_bots.settings.telegram.auth_step_1' %><% end %> — 2. <%= t 'redmine_bots.settings.telegram.auth_step_2' %> — 3. <%= t 'redmine_bots.settings.telegram.auth_step_3' %>
4 |8 | 11 | <%= text_field_tag 'phone_code' %> 12 |
13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/handlers/help_command.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Bot::Handlers 2 | class HelpCommand 3 | include HandlerBehaviour 4 | 5 | def private? 6 | true 7 | end 8 | 9 | def group? 10 | true 11 | end 12 | 13 | def name 14 | 'help' 15 | end 16 | 17 | def description 18 | I18n.t('redmine_bots.telegram.bot.private.help.help') 19 | end 20 | 21 | def allowed?(_user) 22 | true 23 | end 24 | 25 | def command? 26 | true 27 | end 28 | 29 | def call(bot:, action:) 30 | message = RedmineBots::Telegram::Bot::HelpMessage.new(bot, action).to_s 31 | 32 | keyboard = action.user ? bot.default_keyboard : ::Telegram::Bot::Types::ReplyKeyboardRemove.new(remove_keyboard: true) 33 | 34 | bot.async.send_message(chat_id: action.chat_id, text: message, reply_markup: keyboard.to_json) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/redmine_telegram_setup/step_3.html.erb: -------------------------------------------------------------------------------- 1 |<%= link_to telegram_setup_1_path do %>1. <%= t 'redmine_bots.settings.telegram.auth_step_1' %><% end %> — 2. <%= t 'redmine_bots.settings.telegram.auth_step_2' %> — 3. <%= t 'redmine_bots.settings.telegram.auth_step_3' %>
4 |8 | 11 | <%= text_field_tag 'password' %> 12 | <%= t 'redmine_bots.settings.telegram.password_hint' %> 13 |
14 | 15 | 16 | 17 | 18 | 19 | <% end %> 20 | -------------------------------------------------------------------------------- /app/controllers/telegram_login_controller.rb: -------------------------------------------------------------------------------- 1 | class TelegramLoginController < AccountController 2 | def index 3 | end 4 | 5 | def check_auth 6 | user = User.find_by_id(session[:otp_user_id]) || User.current 7 | auth = RedmineBots::Telegram::Bot::Authenticate.(user, login_params, context: context) 8 | 9 | handle_auth_result(auth, user) 10 | end 11 | 12 | private 13 | 14 | def context 15 | session[:otp_user_id] ? '2fa_connection' : 'account_connection' 16 | end 17 | 18 | def handle_auth_result(auth, user) 19 | if auth.success? 20 | if user != User.current 21 | user.update!(two_fa: 'telegram') 22 | successful_authentication(user) 23 | else 24 | redirect_to my_page_path, notice: t('redmine_bots.telegram.bot.login.success') 25 | end 26 | else 27 | render_403(message: auth.value) 28 | end 29 | end 30 | 31 | def login_params 32 | params.permit(:id, :first_name, :last_name, :username, :photo_url, :auth_date, :hash) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/handlers/start_command.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Bot::Handlers 2 | class StartCommand 3 | include HandlerBehaviour 4 | 5 | def private? 6 | true 7 | end 8 | 9 | def command? 10 | true 11 | end 12 | 13 | def name 14 | 'start' 15 | end 16 | 17 | def allowed?(_user) 18 | true 19 | end 20 | 21 | def description 22 | I18n.t('redmine_bots.telegram.bot.private.help.start') 23 | end 24 | 25 | def call(bot:, action:) 26 | message = action.user ? hello_message : instruction_message 27 | 28 | keyboard = action.user ? bot.default_keyboard : ::Telegram::Bot::Types::ReplyKeyboardRemove.new(remove_keyboard: true) 29 | bot.async.send_message(chat_id: action.chat_id, text: message, reply_markup: keyboard.to_json) 30 | end 31 | 32 | private 33 | 34 | def hello_message 35 | I18n.t('redmine_bots.telegram.bot.start.hello') 36 | end 37 | 38 | def instruction_message 39 | I18n.t('redmine_bots.telegram.bot.start.instruction_html') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/handlers/connect_command.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Bot::Handlers 2 | class ConnectCommand 3 | include HandlerBehaviour 4 | 5 | def private? 6 | true 7 | end 8 | 9 | def command? 10 | true 11 | end 12 | 13 | def name 14 | 'connect' 15 | end 16 | 17 | def allowed?(user) 18 | user.anonymous? 19 | end 20 | 21 | def description 22 | I18n.t('redmine_bots.telegram.bot.private.help.connect') 23 | end 24 | 25 | def call(bot:, action:) 26 | if action.user.active? 27 | message = I18n.t('redmine_bots.telegram.bot.connect.already_connected') 28 | else 29 | message = I18n.t('redmine_bots.telegram.bot.connect.login_link', link: "#{Setting.protocol}://#{Setting.host_name}/telegram/login") 30 | end 31 | 32 | bot.async.send_message(chat_id: action.chat_id, text: message) 33 | end 34 | 35 | private 36 | 37 | def hello_message 38 | I18n.t('redmine_bots.telegram.bot.start.hello') 39 | end 40 | 41 | def instruction_message 42 | I18n.t('redmine_bots.telegram.bot.start.instruction_html') 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/command.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class Command 3 | include TD::Types 4 | include Concurrent 5 | 6 | module Callable 7 | def call(*) 8 | return super unless auto_connect? 9 | connect.then { super }.flat 10 | end 11 | end 12 | 13 | private_class_method :new 14 | 15 | class << self 16 | def call(*args) 17 | Filelock(Rails.root.join('tmp', 'redmine_bots', 'tdlib_lock'), wait: 10) do 18 | begin 19 | client = RedmineBots::Telegram.tdlib_client 20 | new(client).call(*args).wait 21 | ensure 22 | client.dispose if defined?(client) && client 23 | end 24 | end 25 | end 26 | 27 | def inherited(klass) 28 | klass.prepend(Callable) 29 | end 30 | end 31 | 32 | 33 | def initialize(client) 34 | @client = client 35 | end 36 | 37 | def call(*) 38 | Concurrent::Promises.reject(NotImplementedError) 39 | end 40 | 41 | protected 42 | 43 | def auto_connect? 44 | true 45 | end 46 | 47 | attr_reader :client 48 | 49 | def connect 50 | client.connect 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/redmine_bots/utils.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots 2 | module Utils 3 | def self.daemonize(name, logger: Logger.new(STDOUT)) 4 | tries ||= 0 5 | tries += 1 6 | 7 | Process.daemon(true, false) if Rails.env.production? 8 | if ENV['PID_DIR'] 9 | pid_dir = ENV['PID_DIR'] 10 | PidFile.new(piddir: pid_dir, pidfile: "#{name}.pid") 11 | else 12 | PidFile.new(piddir: Rails.root.join('tmp', 'pids'), pidfile: "#{name}.pid") 13 | end 14 | 15 | Signal.trap('TERM') do 16 | at_exit { logger.error 'Aborted with TERM signal' } 17 | abort 18 | end 19 | 20 | Signal.trap('HUP') do 21 | at_exit { logger.error 'Aborted with HUP signal' } 22 | abort 23 | end 24 | 25 | yield 26 | 27 | rescue PidFile::DuplicateProcessError => e 28 | logger.error "#{e.class}: #{e.message}" 29 | pid = e.message.match(/Process \(.+ - (\d+)\) is already running./)[1].to_i 30 | 31 | logger.info "Kill process with pid: #{pid}" 32 | 33 | Process.kill('HUP', pid) 34 | if tries < 4 35 | logger.info 'Waiting for 5 seconds...' 36 | sleep 5 37 | logger.info 'Retry...' 38 | retry 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/telegram_proxy.rb: -------------------------------------------------------------------------------- 1 | class TelegramProxy < ActiveRecord::Base 2 | enum protocol: %i[http socks5] 3 | 4 | validates_presence_of :host, :port, :protocol 5 | 6 | scope :alive, -> { where(alive: true) } 7 | 8 | def assign_attributes(new_attributes) 9 | attributes = new_attributes.stringify_keys 10 | url = attributes.delete('url') 11 | return super unless url 12 | 13 | uri = URI(url) 14 | 15 | protocol = uri.scheme.in?(self.class.protocols.keys) ? uri.scheme : nil 16 | 17 | super(attributes.merge(user: uri.user, password: uri.password, host: uri.host, port: uri.port, protocol: protocol)) 18 | end 19 | 20 | def url 21 | return '' unless host.present? 22 | "#{protocol}://#{user ? "#{[user, password.to_s].join(':')}@" : ''}#{[host, port].join(':')}" 23 | end 24 | 25 | def check! 26 | status = 27 | begin 28 | connection.get('/').status 29 | rescue Faraday::ClientError 30 | nil 31 | end 32 | update(alive: status == 200) 33 | end 34 | 35 | private 36 | 37 | def connection 38 | Faraday.new('https://telegram.org') do |conn| 39 | conn.adapter(:patron) do |adapter| 40 | adapter.proxy = url 41 | adapter.force_ipv4 = true 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/sign_in.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack 2 | class SignIn 3 | def self.call(*args) 4 | new.call(*args) 5 | end 6 | 7 | def initialize(client = RedmineBots::Slack.client) 8 | @client = client 9 | end 10 | 11 | def call(params) 12 | return false if current_user.anonymous? 13 | 14 | oauth = @client.oauth_access(client_id: client_id, client_secret: client_secret, code: params[:code]) 15 | 16 | user_client = ::Slack::Web::Client.new(token: oauth.access_token) 17 | user_identity = user_client.users_identity 18 | 19 | slack_account = SlackAccount.find_or_initialize_by(user_id: current_user.id) 20 | 21 | attrs = { slack_id: user_identity.user.id, team_id: user_identity.team.id } 22 | 23 | if slack_account.new_record? 24 | slack_account.assign_attributes(attrs) 25 | else 26 | return false unless slack_account.slice(:slack_id, :team_id).symbolize_keys == attrs 27 | end 28 | 29 | slack_account.name = user_identity.user.name 30 | 31 | slack_account.save 32 | end 33 | 34 | private 35 | 36 | def current_user 37 | User.current 38 | end 39 | 40 | def client_id 41 | Setting.plugin_redmine_bots['slack_client_id'] 42 | end 43 | 44 | def client_secret 45 | Setting.plugin_redmine_bots['slack_client_secret'] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | scope :slack do 2 | scope :sign_in do 3 | get '/' => 'slack_sign_in#index', as: 'slack_sign_in' 4 | get 'check' => 'slack_sign_in#check', as: 'check_slack_sign_in' 5 | end 6 | end 7 | 8 | scope :telegram do 9 | scope :setup do 10 | get 'step_1' => 'redmine_telegram_setup#step_1', as: :telegram_setup_1 11 | post 'step_2' => 'redmine_telegram_setup#step_2', as: :telegram_setup_2 12 | post 'step_3' => 'redmine_telegram_setup#step_3', as: :telegram_setup_3 13 | post 'authorize' => 'redmine_telegram_setup#authorize', as: :telegram_setup_authorize 14 | delete 'reset' => 'redmine_telegram_setup#reset', as: :telegram_setup_reset 15 | end 16 | 17 | scope :api do 18 | post 'web_hook/:secret', to: TelegramWebhookController.action(:update), as: 'telegram_common_webhook' 19 | post 'bot_init' => 'redmine_telegram_setup#bot_init', as: 'telegram_bot_init' 20 | delete 'bot_deinit' => 'redmine_telegram_setup#bot_deinit', as: 'telegram_bot_deinit' 21 | end 22 | 23 | get 'login' => 'telegram_login#index', as: 'telegram_login' 24 | get 'check_auth' => 'telegram_login#check_auth' 25 | end 26 | 27 | scope :telegram_proxies do 28 | get '/' => 'telegram_proxies#index', as: :telegram_proxies 29 | get '/new' => 'telegram_proxies#new', as: :telegram_proxy_new 30 | post '/' => 'telegram_proxies#update', as: :telegram_proxies_update 31 | delete '/:id' => 'telegram_proxies#destroy', as: :telegram_proxy_destroy 32 | end 33 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/toggle_chat_admin.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class ToggleChatAdmin < Command 3 | def call(chat_id, user_id, admin = true) 4 | status = 5 | if admin 6 | TD::Types::ChatMemberStatus::Administrator.new( 7 | rights: rights, 8 | can_be_edited: true, 9 | custom_title: 'Redmine admin' 10 | ) 11 | else 12 | TD::Types::ChatMemberStatus::Member.new 13 | end 14 | client.get_user(user_id: user_id).then { client.set_chat_member_status(chat_id: chat_id, member_id: message_sender(user_id), status: status) }.flat 15 | end 16 | 17 | private 18 | 19 | def rights 20 | TD::Types::ChatAdministratorRights.new( 21 | can_manage_topics: true, 22 | can_manage_chat: true, 23 | can_change_info: true, 24 | can_post_messages: true, 25 | can_edit_messages: true, 26 | can_delete_messages: true, 27 | can_invite_users: true, 28 | can_restrict_members: true, 29 | can_pin_messages: true, 30 | can_promote_members: true, 31 | can_manage_video_chats: true, 32 | can_post_stories: false, 33 | can_edit_stories: false, 34 | can_delete_stories: false, 35 | is_anonymous: false 36 | ) 37 | end 38 | 39 | def message_sender(user_id) 40 | TD::Types::MessageSender::User.new(user_id: user_id) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/create_chat.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class CreateChat < Command 3 | def call(title, user_ids) 4 | Promises.zip(*user_ids.map { |id| client.get_user(user_id: id) }).then do 5 | client.create_new_supergroup_chat(**supergroup_chat_params(title)).then do |chat| 6 | client.add_chat_members(chat_id: chat.id, user_ids: user_ids).then do 7 | client.set_chat_permissions(chat_id: chat.id, permissions: permissions).then { chat } 8 | end.flat 9 | end.flat 10 | end.flat 11 | end 12 | 13 | private 14 | 15 | def permissions 16 | ChatPermissions.new( 17 | can_send_basic_messages: true, 18 | can_send_audios: true, 19 | can_send_documents: true, 20 | can_send_photos: true, 21 | can_send_videos: true, 22 | can_send_video_notes: true, 23 | can_send_voice_notes: true, 24 | can_send_polls: true, 25 | can_send_other_messages: true, 26 | can_add_link_previews: true, 27 | can_send_media_messages: true, 28 | can_change_info: false, 29 | can_invite_users: false, 30 | can_pin_messages: false, 31 | can_create_topics: false 32 | ) 33 | end 34 | 35 | def supergroup_chat_params(title) 36 | { 37 | title: title, 38 | is_channel: false, 39 | description: '', 40 | location: nil, 41 | is_forum: false, 42 | message_auto_delete_time: 0, 43 | for_import: true 44 | } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/views/settings/redmine_bots/_slack.erb: -------------------------------------------------------------------------------- 1 |2 | 5 | <%= text_field_tag 'settings[slack_client_id]', @settings['slack_client_id'] %> 6 |
7 | 8 |9 | 12 | <%= text_field_tag 'settings[slack_client_secret]', @settings['slack_client_secret'], type: 'password' %> 13 | [Показать] 14 |
15 | 16 |17 | 20 | <%= text_field_tag 'settings[slack_verification_token]', @settings['slack_verification_token'], type: 'password' %> 21 | [Показать] 22 |
23 | 24 |25 | 28 | <%= text_field_tag 'settings[slack_oauth_token]', @settings['slack_oauth_token'], type: 'password' %> 29 | [Показать] 30 |
31 | 32 |33 | 36 | <%= text_field_tag 'settings[slack_bot_oauth_token]', @settings['slack_bot_oauth_token'], type: 'password' %> 37 | [Показать] 38 |
-------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | set -e 4 | 5 | if [[ ! "$TESTSPACE" = /* ]] || 6 | [[ ! "$PATH_TO_REDMINE" = /* ]] || 7 | [[ ! "$REDMINE_VER" = * ]] || 8 | [[ ! "$NAME_OF_PLUGIN" = * ]] || 9 | [[ ! "$PATH_TO_PLUGIN" = /* ]]; 10 | then 11 | echo "You should set"\ 12 | " TESTSPACE, PATH_TO_REDMINE, REDMINE_VER"\ 13 | " NAME_OF_PLUGIN, PATH_TO_PLUGIN"\ 14 | " environment variables" 15 | echo "You set:"\ 16 | "$TESTSPACE"\ 17 | "$PATH_TO_REDMINE"\ 18 | "$REDMINE_VER"\ 19 | "$NAME_OF_PLUGIN"\ 20 | "$PATH_TO_PLUGIN" 21 | exit 1; 22 | fi 23 | 24 | export RAILS_ENV=test 25 | 26 | export REDMINE_GIT_REPO=git://github.com/redmine/redmine.git 27 | export REDMINE_GIT_TAG=$REDMINE_VER 28 | export BUNDLE_GEMFILE=$PATH_TO_REDMINE/Gemfile 29 | 30 | # checkout redmine 31 | git clone $REDMINE_GIT_REPO $PATH_TO_REDMINE 32 | cd $PATH_TO_REDMINE 33 | if [ ! "$REDMINE_GIT_TAG" = "master" ]; 34 | then 35 | git checkout -b $REDMINE_GIT_TAG origin/$REDMINE_GIT_TAG 36 | fi 37 | 38 | mv $TESTSPACE/database.yml.travis config/database.yml 39 | mv $TESTSPACE/additional_environment.rb config/ 40 | 41 | # create a link to the backlogs plugin 42 | ln -sf $PATH_TO_PLUGIN plugins/$NAME_OF_PLUGIN 43 | 44 | # install gems 45 | bundle install 46 | 47 | # run redmine database migrations 48 | bundle exec rake db:migrate 49 | 50 | # run plugin database migrations 51 | bundle exec rake redmine:plugins:migrate 52 | 53 | # install redmine database 54 | #bundle exec rake redmine:load_default_data REDMINE_LANG=en 55 | 56 | bundle exec rake db:structure:dump 57 | 58 | # run tests 59 | bundle exec rake redmine:plugins:test NAME=$NAME_OF_PLUGIN 60 | 61 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | log_dir = Rails.root.join('log/redmine_bots') 2 | tmp_dir = Rails.root.join('tmp/redmine_bots') 3 | 4 | FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir) 5 | FileUtils.mkdir_p(tmp_dir) unless Dir.exist?(tmp_dir) 6 | 7 | require 'telegram/bot' 8 | 9 | register_after_redmine_initialize_proc = 10 | if Redmine::VERSION::MAJOR >= 5 11 | Rails.application.config.public_method(:after_initialize) 12 | else 13 | reloader = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader 14 | reloader.public_method(:to_prepare) 15 | end 16 | register_after_redmine_initialize_proc.call do 17 | paths = '/lib/redmine_bots/telegram/{patches/*_patch,hooks/*_hook}.rb' 18 | 19 | Dir.glob(File.dirname(__FILE__) + paths).each do |file| 20 | require_dependency file 21 | end 22 | end 23 | 24 | Rails.application.config.eager_load_paths += Dir.glob("#{Rails.application.config.root}/plugins/redmine_bots/{lib,app/workers,app/models,app/controllers,lib/redmine_bots/telegram/{patches/*_patch,hooks/*_hook}}") 25 | 26 | Sidekiq::Logging.logger = Logger.new(Rails.root.join('log', 'sidekiq.log')) 27 | 28 | Redmine::Plugin.register :redmine_bots do 29 | name 'Redmine Bots' 30 | url 'https://github.com/southbridgeio/redmine_bots' 31 | description 'This is a platform for building Redmine bots' 32 | version '0.5.7' 33 | author 'Southbridge' 34 | author_url 'https://github.com/southbridgeio' 35 | 36 | settings( 37 | default: { 38 | 'slack_oauth_token' => '', 39 | 'slack_bot_oauth_token' => '', 40 | 'slack_client_id' => '', 41 | 'slack_client_secret' => '', 42 | 'slack_verification_token' => '', 43 | }, 44 | partial: 'settings/redmine_bots' 45 | ) 46 | 47 | permission :view_telegram_account_info, {} 48 | end 49 | 50 | RedmineBots::Telegram.init 51 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/close_chat.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class CloseChat < Command 3 | def call(chat_id) 4 | client.get_chat(chat_id: chat_id).then do |chat| 5 | case chat.type 6 | when TD::Types::ChatType::BasicGroup 7 | fetch_robot_ids.then { |*robot_ids| close_basic_group(chat, robot_ids) }.flat 8 | when TD::Types::ChatType::Supergroup 9 | close_super_group(chat.id) 10 | else 11 | raise 'Unsupported chat type' 12 | end 13 | end.flat 14 | end 15 | 16 | private 17 | 18 | def fetch_robot_ids 19 | Promises.zip( 20 | Promises.future do 21 | ActiveRecord::Base.connection_pool.with_connection do 22 | Setting.find_by(name: 'plugin_redmine_bots').value['telegram_bot_id'].to_i 23 | end 24 | end, 25 | client.get_me.then(&:id) 26 | ) 27 | end 28 | 29 | def close_basic_group(chat, robot_ids) 30 | client.get_basic_group_full_info(basic_group_id: chat.type.basic_group_id).then do |group_info| 31 | bot_id, robot_id = robot_ids 32 | bot_member_ids, regular_member_ids = group_info.members.partition { |m| m.member_id.in?(robot_ids) }.map do |arr| 33 | arr.map(&:member_id) 34 | end 35 | member_ids = (regular_member_ids + (bot_member_ids & [bot_id]) + (bot_member_ids & [robot_id])) 36 | member_ids.reduce(Promises.fulfilled_future(nil)) do |promise, member_id| 37 | promise.then { delete_member(chat.id, member_id) }.flat 38 | end 39 | end.flat 40 | end 41 | 42 | def close_super_group(chat_id) 43 | client.delete_chat(chat_id: chat_id) 44 | end 45 | 46 | def delete_member(chat_id, user_id) 47 | client.set_chat_member_status(chat_id: chat_id, member_id: user_id, status: ChatMemberStatus::Left.new) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/authenticate.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class Authenticate < Command 3 | TIMEOUT = 20 4 | 5 | class AuthenticationError < StandardError 6 | end 7 | 8 | def call(params) 9 | mutex = Mutex.new 10 | condition = ConditionVariable.new 11 | error = nil 12 | result = nil 13 | 14 | client.on(Update::AuthorizationState) do |update| 15 | promise = Promises.fulfilled_future(true) 16 | 17 | case update.authorization_state 18 | when AuthorizationState::WaitPhoneNumber 19 | promise = client.set_authentication_phone_number(phone_number: params[:phone_number], settings: nil) 20 | when AuthorizationState::WaitCode 21 | promise = client.check_authentication_code(code: params[:phone_code]) if params[:phone_code] 22 | when AuthorizationState::WaitPassword 23 | promise = client.check_authentication_password(password: params[:password]) if params[:password] 24 | when AuthorizationState::Ready 25 | promise = Promises.fulfilled_future(true) 26 | else 27 | next 28 | end 29 | 30 | mutex.synchronize do 31 | promise.then do |res| 32 | result = res 33 | condition.broadcast 34 | end.rescue do |err| 35 | error = err 36 | condition.broadcast 37 | end 38 | end 39 | end 40 | 41 | connect.then do 42 | Promises.future do 43 | mutex.synchronize do 44 | condition.wait(mutex, TIMEOUT) 45 | raise TD::Error.new(error) if error 46 | error = TD::Types::Error.new(code: 0, message: 'Unknown error. Please, see TDlib logs.') if result.nil? 47 | raise TD::Error.new(error) if error 48 | result 49 | end 50 | end 51 | end.flat 52 | end 53 | 54 | private 55 | 56 | def auto_connect? 57 | false 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.6 2 | 3 | * Add compatibility with Redmine 5.1 4 | * Fix showing errors 5 | 6 | # 0.5.5 7 | 8 | * Add functionality for processing photos from telegram 9 | 10 | # 0.5.4 11 | 12 | * Fix set bot permissions 13 | * Remove Sidekiq Cron job for Telegram proxy worker 14 | 15 | # 0.5.3 16 | 17 | * Add step 3 to Telegram client authorization 18 | * Adapt tdlib commands for new version 19 | 20 | # 0.5.2 21 | 22 | * Merge old 'update' branch 23 | 24 | # 0.5.1 25 | 26 | * Fix problem with telegram_id exceeding int. 27 | 28 | # 0.5.0 29 | 30 | * Use supergroup chats 31 | * Refactor telegram commands 32 | * Support persistent commands 33 | * Update tdlib, remove proxy support 34 | 35 | # 0.4.1 36 | 37 | * Fix tdlib proxy 38 | * Fix AddBot command 39 | * Fix robot_id detection 40 | * Release ActiveRecord connections in concurrent-ruby threads 41 | * Remove ruby 2.7.0 from build matrix 42 | * Update sidekiq-rate-limiter 43 | 44 | # 0.4.0 45 | 46 | * Handle Faraday::ClientError in MessageSender 47 | * Handle deactivated user error in message sender 48 | * Force ipv4 when using proxy 49 | * Improve proxy availability check 50 | * Close supergroups properly 51 | * Rescue from chat not found errors 52 | * Increase sleep time for flood error 53 | * Handle forbidden errors in message sender 54 | * Use sleep time from error data 55 | * Add zh-TW locale 56 | * Handle "message not modified errors" 57 | * Increase rate limits, add exponential retry 58 | * Support tdlib 1.6 59 | 60 | # 0.3.1 61 | 62 | * Fix Rails 4 support 63 | 64 | # 0.3.0 65 | 66 | * Hidden secret fields in settings 67 | * Adapt tdlib commands for new version 68 | * Add plugins deprecation warning 69 | * Add proxy pool 70 | * Add bot to robot contacts automatically 71 | * Use unified proxies for bot and tdlib 72 | 73 | # 0.2.0 74 | 75 | * Add view telegram account permission 76 | * Add webhook secret param 77 | * Adapt for Redmine 4 78 | * Fix locales in telegram client authentication 79 | * Fix getUpdate hint on settings page 80 | 81 | # 0.1.0 82 | 83 | * Initial release 84 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/user_action.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RedmineBots::Telegram::Bot 4 | class UserAction 5 | delegate :from, to: :message 6 | delegate :chat, to: :message 7 | delegate :id, to: :from, prefix: true, allow_nil: true 8 | delegate :id, to: :user, prefix: true 9 | delegate :id, to: :chat, prefix: true 10 | delegate :title, to: :chat, prefix: true 11 | delegate :message_id, to: :message 12 | 13 | attr_reader :message 14 | 15 | def self.from_payload(payload) 16 | new(Telegram::Bot::Types::Update.new(payload).current_message) 17 | end 18 | 19 | def initialize(message) 20 | @message = message 21 | end 22 | 23 | def telegram_account 24 | return @telegram_account if defined?(@telegram_account) 25 | 26 | @telegram_account = 27 | begin 28 | telegram_id = message.from.id 29 | TelegramAccount.find_by(telegram_id: telegram_id) 30 | end 31 | end 32 | 33 | def message? 34 | message.is_a?(::Telegram::Bot::Types::Message) 35 | end 36 | 37 | def callback_query? 38 | message.is_a?(::Telegram::Bot::Types::CallbackQuery) 39 | end 40 | 41 | def text 42 | if message? && !message.media_group_id 43 | if has_photo? 44 | message.caption.to_s 45 | else 46 | message.text.to_s 47 | end 48 | else 49 | '' 50 | end 51 | end 52 | 53 | def photo 54 | message.photo.max_by { |photo| photo.file_size } 55 | end 56 | 57 | def has_photo? 58 | message.photo.present? && !message.media_group_id 59 | end 60 | 61 | def command? 62 | message.is_a?(::Telegram::Bot::Types::Message) && message.text&.start_with?('/') 63 | end 64 | 65 | def command 66 | if command? && message.text.match(/^\/(\w+)/).present? 67 | return [message.text.match(/^\/(\w+)/)[1], message.text.match(/^\/\w+ (.+)$/).try(:[], 1)] 68 | end 69 | 70 | [] 71 | end 72 | 73 | def private? 74 | message? && message.chat.type == 'private' 75 | end 76 | 77 | def group? 78 | !private? 79 | end 80 | 81 | def user 82 | telegram_account&.user || User.anonymous 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/tasks/telegram.rake: -------------------------------------------------------------------------------- 1 | def init_bot 2 | LOG.info 'Start daemon...' 3 | 4 | token = Setting.plugin_redmine_bots['telegram_bot_token'] 5 | 6 | unless token.present? 7 | LOG.error 'Telegram Bot Token not found. Please set it in the plugin config web-interface.' 8 | exit 9 | end 10 | 11 | LOG.info 'Telegram Bot: Connecting to telegram...' 12 | 13 | require 'telegram/bot' 14 | 15 | bot = RedmineBots::Telegram.init_bot 16 | bot.api.setWebhook(url: '') # reset webhook 17 | bot 18 | end 19 | 20 | namespace :redmine_bots do 21 | # bundle exec rake telegram_common:bot PID_DIR='tmp/pids' 22 | desc "Runs telegram bot process (options: default PID_DIR='tmp/pids')" 23 | task telegram: :environment do 24 | LOG = Rails.env.production? ? Logger.new(Rails.root.join('log/redmine_bots', 'bot.log')) : Logger.new(STDOUT) 25 | 26 | RedmineBots::Utils.daemonize(:telegram_bot, logger: LOG) do 27 | bot = init_bot 28 | begin 29 | bot.listen do |message| 30 | RedmineBots::Telegram.update_manager.handle_message(message) 31 | end 32 | rescue => e 33 | ExceptionNotifier.notify_exception(e) if defined?(ExceptionNotifier) 34 | LOG.error "GLOBAL #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" 35 | end 36 | end 37 | end 38 | 39 | task migrate_from_telegram_common: :environment do 40 | class TelegramCommonAccount < ActiveRecord::Base 41 | end 42 | 43 | TelegramAccount.transaction do 44 | TelegramCommonAccount.all.each do |old_account| 45 | next if TelegramAccount.find_by(telegram_id: old_account.telegram_id) 46 | TelegramAccount.create!(old_account.slice(:telegram_id, :user_id, :username, :first_name, :last_name)) 47 | end 48 | 49 | telegram_common_settings = Setting.find_by_name(:plugin_redmine_telegram_common) 50 | 51 | settings = Setting.find_or_initialize_by(name: 'plugin_redmine_bots') 52 | 53 | settings.value = settings.value.to_h.merge(%w[bot_token api_id api_hash].map { |name| { "telegram_#{name}" => YAML.load(telegram_common_settings[:value])[name] } }.reduce(:merge)) 54 | settings.save! 55 | 56 | FileUtils.copy_entry(Rails.root.join('tmp', 'redmine_telegram_common', 'tdlib'), Rails.root.join('tmp', 'redmine_bots', 'tdlib')) 57 | 58 | puts 'Successfully transferred accounts and settings' 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/commands/base.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack::Commands 2 | class Base 3 | class << self 4 | def inherited(klass) 5 | klass.prepend( 6 | Module.new do 7 | def call 8 | I18n.locale = Setting['default_language'] 9 | (not_authorized && return) unless authorized? 10 | (private_only && return) if private_only? && !private? 11 | (group_only && return) if group_only? && !group? 12 | super 13 | end 14 | end 15 | ) 16 | end 17 | 18 | def to_proc 19 | ->(client, data, match) { new(client, data, match).call } 20 | end 21 | 22 | def responds_to(*names) 23 | define_singleton_method(:names) { names.map(&:to_s) } 24 | end 25 | 26 | def described_as(text) 27 | define_singleton_method(:description) { text } 28 | end 29 | 30 | def private_only 31 | define_singleton_method(:private_only?) { true } 32 | end 33 | 34 | def group_only 35 | define_singleton_method(:group_only?) { true } 36 | end 37 | 38 | def private_only? 39 | false 40 | end 41 | 42 | def group_only? 43 | false 44 | end 45 | 46 | def private? 47 | !group_only? 48 | end 49 | 50 | def group? 51 | !private_only? 52 | end 53 | 54 | def description 55 | I18n.t("redmine_bots.slack.commands.#{name.demodulize.underscore}") 56 | end 57 | end 58 | 59 | def initialize(client, data, match) 60 | @client, @data, @match = client, data, match 61 | end 62 | 63 | protected 64 | 65 | attr_reader :client, :data, :match 66 | 67 | def reply(**attrs) 68 | client.say(attrs.merge(channel: data.channel)) 69 | end 70 | 71 | def current_user 72 | SlackAccount.find_by(slack_id: data.user)&.user || User.anonymous 73 | end 74 | 75 | def authorized? 76 | true 77 | end 78 | 79 | def not_authorized 80 | client.say(text: 'Not authorized', channel: data.channel) 81 | end 82 | 83 | def private_only 84 | client.say(text: 'Private only', channel: data.channel) 85 | end 86 | 87 | def group_only 88 | client.say(text: 'Group only', channel: data.channel) 89 | end 90 | 91 | def channel 92 | @channel ||= client.web_client.conversations_info(channel: data.channel).channel 93 | end 94 | 95 | def private? 96 | channel.is_im 97 | end 98 | 99 | def group? 100 | !private? 101 | end 102 | 103 | def private_only? 104 | self.class.private_only? 105 | end 106 | 107 | def group_only? 108 | self.class.group_only? 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /app/controllers/redmine_telegram_setup_controller.rb: -------------------------------------------------------------------------------- 1 | class RedmineTelegramSetupController < ApplicationController 2 | include RedmineBots::Telegram::Tdlib 3 | 4 | def step_1 5 | end 6 | 7 | def step_2 8 | RedmineBots::Telegram::Tdlib.wrap do 9 | promise = RedmineBots::Telegram::Tdlib::Authenticate.(params).rescue do |error| 10 | redirect_to plugin_settings_path('redmine_bots'), alert: error.message 11 | end 12 | 13 | RedmineBots::Telegram::Tdlib.permit_concurrent_loads { promise.wait! } 14 | end 15 | end 16 | 17 | def step_3 18 | RedmineBots::Telegram::Tdlib.wrap do 19 | promise = RedmineBots::Telegram::Tdlib::Authenticate.(params).rescue do |error| 20 | redirect_to plugin_settings_path('redmine_bots'), alert: error.message 21 | end 22 | 23 | RedmineBots::Telegram::Tdlib.permit_concurrent_loads { promise.wait! } 24 | end 25 | end 26 | 27 | def authorize 28 | RedmineBots::Telegram::Tdlib.wrap do 29 | promise = RedmineBots::Telegram::Tdlib::Authenticate.(params).then do 30 | RedmineBots::Telegram::Tdlib::FetchAllChats.call 31 | end.flat.then do 32 | ActiveRecord::Base.connection_pool.with_connection { save_phone_settings(phone_number: params['phone_number']) } 33 | redirect_to plugin_settings_path('redmine_bots'), notice: t('redmine_bots.telegram.authorize.success') 34 | end 35 | 36 | RedmineBots::Telegram::Tdlib.permit_concurrent_loads { promise.wait! } 37 | end 38 | rescue TD::Error => error 39 | redirect_to plugin_settings_path('redmine_bots'), alert: error.message 40 | end 41 | 42 | def reset 43 | save_phone_settings(phone_number: nil) 44 | FileUtils.rm_rf(Rails.root.join('tmp', 'redmine_bots', 'tdlib')) 45 | redirect_to plugin_settings_path('redmine_bots') 46 | end 47 | 48 | def bot_init 49 | web_hook_url = "https://#{Setting.host_name}/telegram/api/web_hook/#{RedmineBots::Telegram.webhook_secret}" 50 | 51 | bot = RedmineBots::Telegram.init_bot 52 | bot.api.setWebhook(url: web_hook_url) 53 | 54 | redirect_to plugin_settings_path('redmine_bots'), notice: t('redmine_2chat.bot.authorize.success') 55 | 56 | rescue Telegram::Bot::Exceptions::ResponseError => error 57 | redirect_to plugin_settings_path('redmine_bots'), alert: parsed_error(error)['description'] 58 | end 59 | 60 | def bot_deinit 61 | token = Setting.plugin_redmine_bots['telegram_bot_token'] 62 | bot = Telegram::Bot::Client.new(token) 63 | bot.api.setWebhook(url: '') 64 | redirect_to plugin_settings_path('redmine_bots'), notice: t('redmine_2chat.bot.deauthorize.success') 65 | end 66 | 67 | private 68 | 69 | def save_phone_settings(phone_number:) 70 | Setting.send('plugin_redmine_bots=', Setting.plugin_redmine_bots.merge({'telegram_phone_number' => phone_number.to_s}).to_h) 71 | end 72 | 73 | def parsed_error(error) 74 | JSON.parse(error.response.body) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram 2 | BOT_MUTEX = Mutex.new 3 | INIT_MUTEX = Mutex.new 4 | 5 | # @return [Bot] 6 | def self.bot 7 | BOT_MUTEX.synchronize do 8 | @bot ||= Bot.new(api: ::Telegram::Bot::Api.new(Bot::Token.instance), 9 | async_handler_class: Bot::AsyncHandler, 10 | throttle: Bot::NullThrottle.new) 11 | end 12 | end 13 | 14 | def self.init 15 | INIT_MUTEX.synchronize do 16 | return if @bot_initialized 17 | 18 | bot.register_handler(Bot::Handlers::StartCommand.new) 19 | bot.register_handler(Bot::Handlers::HelpCommand.new) 20 | bot.register_handler(Bot::Handlers::ConnectCommand.new) 21 | 22 | @bot_initialized = true 23 | end 24 | end 25 | 26 | def self.set_locale 27 | I18n.locale = Setting['default_language'] 28 | end 29 | 30 | def self.bot_token 31 | Setting.find_by_name(:plugin_redmine_bots).value['telegram_bot_token'] 32 | end 33 | 34 | def self.webhook_secret 35 | Digest::SHA256.hexdigest(Rails.application.secrets[:secret_key_base]) 36 | end 37 | 38 | def self.tdlib_client 39 | settings = Setting.find_by_name(:plugin_redmine_bots)&.value || {} 40 | TD::Api.set_log_file_path(Rails.root.join('log', 'redmine_bots', 'tdlib.log').to_s) 41 | config = { 42 | api_id: settings['telegram_api_id'], 43 | api_hash: settings['telegram_api_hash'], 44 | use_test_dc: false, 45 | database_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'db').to_s, 46 | files_directory: Rails.root.join('tmp', 'redmine_bots', 'tdlib', 'files').to_s, 47 | use_file_database: true, 48 | use_chat_info_database: true, 49 | use_secret_chats: true, 50 | use_message_database: true, 51 | system_language_code: 'en', 52 | device_model: 'Ruby TD client', 53 | system_version: 'Unknown', 54 | application_version: '1.0', 55 | } 56 | 57 | client = TD::Client.new(timeout: 300, **config) 58 | client.set_tdlib_parameters(**config) 59 | 60 | client 61 | end 62 | 63 | def self.init_bot 64 | token = Setting.plugin_redmine_bots['telegram_bot_token'] 65 | self_info = {} 66 | 67 | if Setting.plugin_redmine_bots['telegram_phone_number'].present? 68 | self_info = Tdlib::GetMe.call.rescue { {} }.value!.to_h 69 | end 70 | 71 | robot_id = self_info[:id] 72 | 73 | bot = Telegram::Bot::Client.new(token) 74 | bot_info = bot.api.get_me['result'] 75 | bot_name = bot_info['username'] 76 | 77 | RedmineBots::Telegram::Tdlib::AddBot.(bot_name) if robot_id 78 | 79 | plugin_settings = Setting.find_by(name: 'plugin_redmine_bots') 80 | 81 | plugin_settings_hash = plugin_settings.value 82 | plugin_settings_hash['telegram_bot_name'] = bot_name 83 | plugin_settings_hash['telegram_bot_id'] = bot_info['id'] 84 | plugin_settings_hash['telegram_robot_id'] = robot_id 85 | plugin_settings.value = plugin_settings_hash 86 | 87 | plugin_settings.save 88 | 89 | bot 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'telegram/bot' 4 | 5 | module RedmineBots::Telegram 6 | class Bot 7 | class IgnoredError 8 | IDENTIFIERS = ['bot was blocked', 'bot was kicked', 'user is deactivated', "bot can't initiate conversation with a user", 'chat not found'].freeze 9 | 10 | def self.===(error) 11 | error.is_a?(Telegram::Bot::Exceptions::ResponseError) && IDENTIFIERS.any? { |i| i.in?(error.message) } 12 | end 13 | end 14 | 15 | attr_accessor :default_keyboard 16 | 17 | def initialize(api:, throttle:, async_handler_class: AsyncHandler) 18 | @api = api 19 | @throttle = throttle 20 | @async_handler_class = async_handler_class 21 | @handlers = Set.new 22 | @persistent_commands = Set.new 23 | self.default_keyboard = ::Telegram::Bot::Types::ReplyKeyboardRemove.new(remove_keyboard: true) 24 | end 25 | 26 | def handle_update(payload) 27 | RedmineBots::Telegram.set_locale 28 | 29 | action = UserAction.from_payload(payload) 30 | 31 | persistent_commands.each do |command_class| 32 | command = command_class.retrieve(action.from_id) if action.message? 33 | next unless command 34 | 35 | command.resume!(action: action) 36 | return 37 | end 38 | 39 | handlers.each { |h| h.call(action: action, bot: self) if h.match?(action) } 40 | end 41 | 42 | def send_message(chat_id:, **params) 43 | params = { parse_mode: 'HTML', disable_web_page_preview: true }.merge(params) 44 | handle_errors { throttle.apply(chat_id) { api.send_message(chat_id: chat_id, **params) } } 45 | end 46 | 47 | def get_chat(chat_id:) 48 | handle_errors { throttle.apply(chat_id) { api.get_chat(chat_id: chat_id) } } 49 | end 50 | 51 | def edit_message_text(chat_id:, **params) 52 | handle_errors { throttle.apply(chat_id) { api.edit_message_text(chat_id: chat_id, **params) } } 53 | end 54 | 55 | def promote_chat_member(chat_id:, **params) 56 | handle_errors { throttle.apply(chat_id) { api.promote_chat_member(chat_id: chat_id, **params) } } 57 | end 58 | 59 | def get_file(chat_id:, file_id:) 60 | handle_errors { throttle.apply(chat_id) { api.get_file(file_id: file_id) } } 61 | end 62 | 63 | def set_webhook 64 | webhook_url = "https://#{Setting.host_name}/telegram/api/web_hook/#{webhook_secret}" 65 | api.set_webhook(url: webhook_url) 66 | end 67 | 68 | def webhook_secret 69 | Digest::SHA256.hexdigest(Rails.application.secrets[:secret_key_base]) 70 | end 71 | 72 | def async 73 | async_handler_class.new(self) 74 | end 75 | 76 | def register_handler(handler) 77 | @handlers << handler 78 | end 79 | 80 | def register_persistent_command(command_class) 81 | @persistent_commands << command_class 82 | end 83 | 84 | def commands 85 | handlers.select(&:command?) 86 | end 87 | 88 | private 89 | 90 | attr_reader :api, :throttle, :async_handler_class, :handlers, :persistent_commands 91 | 92 | def log(message) 93 | Rails.logger.info("RedmineBots: #{message}") 94 | end 95 | 96 | def handle_errors 97 | yield 98 | rescue IgnoredError => e 99 | log("Ignored error #{e.class}: #{e.message}") 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /config/locales/zh-TW.yml: -------------------------------------------------------------------------------- 1 | zh-TW: 2 | redmine_bots: 3 | label: 4 | slack: Slack 5 | telegram: Telegram 6 | settings: 7 | deprecated_plugins: '警告!安裝了棄用的外掛:' 8 | slack_oauth_token: OAuth Access Token 9 | slack_bot_oauth_token: Bot User OAuth Access Token 10 | slack_client_id: Client ID 11 | slack_client_secret: Client Secret 12 | slack_verification_token: 驗證 Token 13 | telegram: 14 | phone_number: "電話號碼" 15 | phone_number_hint: "以台灣手機 0912-345-678 為例,需輸入:886912345678" 16 | phone_code: "Telegram 代碼" 17 | password: "Telegram 密碼" 18 | password_hint: "如果您沒有在 Telegram 應用程式中啟用雙重認證,請按一下「授權」按鈕。" 19 | authorize_button_code: "接收代碼" 20 | authorize_button: "授權" 21 | authorize_client: "授權給 Telegram 用戶端" 22 | authorize_hint: "請按下面的按鈕授權給 Telegram。目前的授權則會被清除。" 23 | plugin_link: "回到外掛設定" 24 | auth_step_1: "接收 Telegram 代碼" 25 | auth_step_2: "代碼授權" 26 | auth_step_3: "密碼授權" 27 | reset: "重設快取及授權" 28 | bot_init: 初始化機器人 29 | bot_deinit: 停用機器人 30 | get_updates_hint: bundle exec rake redmine_bots:telegram 31 | web_hooks_warning: 設定 WebHooks 需要 HTTPS 32 | tdlib_use_proxy: tdlib 使用 Proxy 33 | bot_use_proxy: 機器人使用 Proxy 34 | proxy_settings: Proxy 設定 35 | proxy_list: Proxy 列表 36 | proxy_alive: Proxy 可用 37 | proxy_dead: Proxy 不可用 38 | requirements: 39 | title: 需求 40 | telegram: 41 | valid: 有效 42 | description: 說明 43 | no: 否 44 | yes: 是 45 | rails_env: "請確定您的 RAILS_ENV 環境變數設定為 production(目前為:%{rails_env})" 46 | redmine_host: "請確定您在 Redmine 的設定中有正確設定 Host(目前為:%{host})" 47 | tdlib_installation: "您應該安裝 libtdjson 到 Redmine 目錄下的 vendor 目錄,或者安裝到 ldconfig。安裝教學。" 48 | telegram: 49 | authorize: 50 | success: 驗證完成! 51 | bot: 52 | start: 53 | instruction_html: | 54 | 現在我們必須連結您的 Redmine 和 Telegram 帳號。 55 | 請輸入 /connect 指令。 56 | hello: 您好! 57 | connect: 58 | already_connected: 您的帳戶已經連結。 59 | wait_for_email: 我們寄了一封 Email 到 "%{email}",請依照信中的指示操作。 60 | wrong_email: 錯誤的信箱!找不到信箱對應的使用者。再失敗 %{attempts} 次之後,這個帳號將被鎖定一個小時。 61 | blocked: 您的帳號已被鎖定,請於 %{unblock} 分鐘後再嘗試。 62 | login_link: "使用這個連結來連結這個帳號 和 Redmine:%{link}" 63 | group: 64 | no_commands: "沒有群組聊天用的指令。私聊指令:" 65 | private_command: '這個指令只能用在私聊。' 66 | private: 67 | group_command: '這個指令只能用在群組聊天。' 68 | help: 69 | start: "開始使用機器人" 70 | connect: "連結 Redmine 和 Telegram 帳號" 71 | help: "指令的幫助" 72 | token: 取得驗證連結 73 | login: 74 | success: 驗證成功 75 | errors: 76 | not_logged: 您尚未登入 77 | hash_invalid: Hash 不合法 78 | hash_outdated: 請求已過期 79 | wrong_account: 錯誤的 Telegram 帳號 80 | account_exists: 此 Telegram 帳戶已連結到另一個帳戶 81 | not_persisted: Failed to persist Telegram account data 82 | invalid_token: Token 不合法 83 | follow_link: 請根據連結 84 | send_to_telegram: 傳送驗證連結到 Telegram 85 | widget_not_visible: 如果小工具沒有顯示 86 | write_to_bot: "如果小工具沒有顯示,請發送 /token 訊息給機器人 @%{bot}。" 87 | slack: 88 | commands: 89 | connect: 連結 Slack 帳號和 Redmine 90 | help: 指令的幫助訊息 91 | -------------------------------------------------------------------------------- /app/views/settings/redmine_bots/_telegram.erb: -------------------------------------------------------------------------------- 1 | <% if @settings['telegram_phone_number'].present? %> 2 || 18 | | <%= t 'redmine_bots.requirements.telegram.valid' %> | 19 |<%= t 'redmine_bots.requirements.telegram.description' %> | 20 |
|---|
36 | 37 | <%= text_field_tag 'settings[telegram_bot_token]', @settings['telegram_bot_token'], type: 'password', size: 50 %> 38 | [Показать] 39 |
40 | 41 |42 | 43 | <%= text_field_tag 'settings[telegram_api_id]', @settings['telegram_api_id'], type: 'password', size: 50 %> 44 | [Показать] 45 |
46 | 47 |48 | 49 | <%= text_field_tag 'settings[telegram_api_hash]', @settings['telegram_api_hash'], type: 'password', size: 50 %> 50 | [Показать] 51 |
52 | 53 |54 | API ID and API Hash can be obtained here 55 |
56 | 57 |<%= t 'redmine_bots.settings.telegram.get_updates_hint' %>76 |