├── .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 | <%= render partial: 'slack_sign_in/widget' %> 3 |
-------------------------------------------------------------------------------- /test/fixtures/telegram_accounts.yml: -------------------------------------------------------------------------------- 1 | with_user: 2 | id: 1 3 | user_id: 1 4 | telegram_id: 1 5 | 6 | without_user: 7 | id: 2 8 | telegram_id: 2 9 | -------------------------------------------------------------------------------- /test/support/database.yml.travis: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: 5 5 | database: travis_ci_test 6 | user: postgres 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/null_throttle.rb: -------------------------------------------------------------------------------- 1 | class RedmineBots::Telegram::Bot 2 | class NullThrottle 3 | def apply(*) 4 | yield 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/support/additional_environment.rb: -------------------------------------------------------------------------------- 1 | # for travis debugging 2 | # config.logger = Logger.new(STDOUT) 3 | # config.logger.level = Logger::INFO 4 | # config.log_level = :info 5 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/get_me.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class GetMe < Command 3 | def call 4 | client.get_me 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | logged: 2 | id: 1 3 | type: User 4 | 5 | anonymous: 6 | id: 2 7 | type: AnonymousUser 8 | 9 | user_3: 10 | id: 3 11 | type: User 12 | -------------------------------------------------------------------------------- /lib/redmine_bots.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots 2 | def self.deprecated_plugins 3 | Redmine::Plugin.all.map(&:id) & %i[redmine_telegram_common redmine_chat_telegram] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/telegram_login/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render partial: 'telegram_login/widget', locals: { context: 'account_connection' } %> 3 |
4 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/get_chat.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class GetChat < Command 3 | def call(id) 4 | client.get_chat(chat_id: id) 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/get_user.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class GetUser < Command 3 | def call(user_id) 4 | client.get_user(user_id: user_id) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/telegram_account.rb: -------------------------------------------------------------------------------- 1 | class TelegramAccount < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | def name 5 | [first_name, last_name, username&.tap { |n| n.insert(0, '@') }].compact.join(' ') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/slack_sign_in/_widget.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 |

Telegram

3 | 7 | <% end %> 8 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/utils.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram 2 | module Utils 3 | def self.auth_hash(data) 4 | check_string = data.except('hash').map { |k, v| "#{k}=#{v}" }.join("\n") 5 | secret_key = Digest::SHA256.digest(Bot::Token.instance.to_s) 6 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, check_string) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | module_function 3 | 4 | def wrap(&block) 5 | Rails::VERSION::MAJOR < 5 ? yield : Rails.application.executor.wrap(&block) 6 | end 7 | 8 | def permit_concurrent_loads(&block) 9 | Rails::VERSION::MAJOR < 5 ? yield : ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/telegram_login/_widget.erb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/commands/help.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack::Commands 2 | class Help < Base 3 | responds_to :help 4 | 5 | def call 6 | reply(text: help_message) 7 | end 8 | 9 | private 10 | 11 | def help_message 12 | RedmineBots::Slack::Bot.commands.map { |command| "#{command.names.join(', ')} - #{command.description}" }.join("\n") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/tasks/slack.rake: -------------------------------------------------------------------------------- 1 | namespace :redmine_bots do 2 | # bundle exec rake redmine_bots:slack PID_DIR='tmp/pids' 3 | desc "Runs slack bot process (options: default PID_DIR='tmp/pids')" 4 | task slack: :environment do 5 | $stdout.reopen(Rails.root.join('log', 'redmine_bots', 'slack.log'), "a") if Rails.env.production? 6 | 7 | RedmineBots::Utils.daemonize(:slack_bot, &RedmineBots::Slack::Bot.method(:run)) 8 | end 9 | end -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/add_to_chat.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class AddToChat < Command 3 | def call(chat_id, user_name) 4 | client.search_public_chat(username: user_name).then do |user_chat| 5 | client.get_user(user_id: user_chat.id).then { client.add_chat_member(chat_id: chat_id, user_id: user_chat.id, forward_limit: nil) }.flat 6 | end.flat 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/async_handler.rb: -------------------------------------------------------------------------------- 1 | class RedmineBots::Telegram::Bot 2 | class AsyncHandler 3 | def initialize(_bot) 4 | ; 5 | end 6 | 7 | def send_message(*args) 8 | AsyncBotHandlerWorker.perform_async('send_message', *args) 9 | end 10 | 11 | def promote_chat_member(*args) 12 | AsyncBotHandlerWorker.perform_async('promote_chat_member', *args) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/001_create_slack_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateSlackAccounts < Rails.version < '5.0' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :slack_accounts do |t| 4 | t.string :slack_id, index: true 5 | t.string :name 6 | t.string :team_id, index: true 7 | t.string :token, index: true 8 | t.belongs_to :user, foreign_key: true, index: true 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/002_create_telegram_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateTelegramAccounts < Rails.version < '5.0' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :telegram_accounts do |t| 4 | t.integer :telegram_id, index: true 5 | t.string :username 6 | t.string :first_name 7 | t.string :last_name 8 | t.belongs_to :user, foreign_key: true, index: true 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/patches/user_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots 2 | module Telegram 3 | module Patches 4 | module UserPatch 5 | def self.included(base) 6 | base.class_eval do 7 | has_one :telegram_account, dependent: :destroy, class_name: '::TelegramAccount' 8 | end 9 | end 10 | end 11 | end 12 | end 13 | end 14 | User.send(:include, RedmineBots::Telegram::Patches::UserPatch) 15 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack 2 | def self.configured? 3 | false 4 | end 5 | 6 | def self.client 7 | ::Slack::Web::Client.new 8 | end 9 | 10 | def self.robot_client 11 | ::Slack::Web::Client.new(token: Setting.plugin_redmine_bots['slack_oauth_token']) 12 | end 13 | 14 | def self.bot_client 15 | ::Slack::Web::Client.new(token: Setting.plugin_redmine_bots['slack_bot_oauth_token']) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/commands/connect.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack::Commands 2 | class Connect < Base 3 | private_only 4 | responds_to :connect, :start 5 | 6 | def call 7 | reply(text: current_user.logged? ? 'Already connected' : sign_in_link) 8 | end 9 | 10 | private 11 | 12 | def sign_in_link 13 | "Please, follow link: #{Setting.protocol}://#{Setting.host_name}/slack/sign_in" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/003_create_telegram_proxies.rb: -------------------------------------------------------------------------------- 1 | class CreateTelegramProxies < Rails.version < '5.0' ? ActiveRecord::Migration : ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :telegram_proxies do |t| 4 | t.string :host, null: false 5 | t.integer :port, null: false 6 | t.integer :protocol, null: false 7 | t.string :user 8 | t.string :password 9 | t.boolean :alive, default: false, null: false 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/support/example_telegram_bot_ruby.rb: -------------------------------------------------------------------------------- 1 | require 'telegram/bot' 2 | 3 | token = 'token' 4 | 5 | bot = Telegram::Bot::Client.new(token) 6 | 7 | puts bot 8 | 9 | bot.listen do |message| 10 | puts message.to_compact_hash 11 | case message.text 12 | when '/start' 13 | bot.api.send_message(chat_id: message.chat.id, text: "Hello, #{message.from.first_name}") 14 | when '/stop' 15 | bot.api.send_message(chat_id: message.chat.id, text: "Bye, #{message.from.first_name}") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $VERBOSE = nil # for hide ruby warnings 2 | 3 | # Load the Redmine helper 4 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 5 | 6 | require 'minitest/autorun' 7 | require "minitest/reporters" 8 | 9 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 10 | ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', %i[telegram_accounts users]) 11 | 12 | class ActiveSupport::TestCase 13 | extend Minitest::Spec::DSL 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/telegram_webhook_controller.rb: -------------------------------------------------------------------------------- 1 | class TelegramWebhookController < ActionController::Metal 2 | def update 3 | unless secret_valid? 4 | self.status = 403 5 | self.response_body = 'Forbidden' 6 | return 7 | end 8 | 9 | TelegramHandlerWorker.perform_async(params) 10 | 11 | self.status = 200 12 | self.response_body = '' 13 | end 14 | 15 | private 16 | 17 | def secret_valid? 18 | params[:secret] == RedmineBots::Telegram.webhook_secret 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/settings/redmine_bots/_deprecation_warning.erb: -------------------------------------------------------------------------------- 1 | <% if Redmine::Plugin.installed?('redmine_bots') %> 2 | <% if RedmineBots.deprecated_plugins.present? %> 3 |

4 | !!! <%= "#{t 'redmine_bots.settings.deprecated_plugins'}#{RedmineBots.deprecated_plugins.join(', ')}" %> !!! 5 |

6 | <% end %> 7 | <% else %> 8 |

9 | !!! redmine_bots not found !!! 10 |

11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/jwt.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram 2 | module Jwt 3 | extend self 4 | 5 | def encode(payload) 6 | exp = Time.now.to_i + 300 7 | JWT.encode({ **payload, iss: issuer, exp: exp }, secret) 8 | end 9 | 10 | def decode_token(token) 11 | JWT.decode(token, secret, true, algorithm: 'HS256', iss: issuer, verify_iss: true) 12 | end 13 | 14 | private 15 | 16 | def secret 17 | Rails.application.config.secret_key_base 18 | end 19 | 20 | def issuer 21 | Setting.host_name 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/redmine_bots/account_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class TelegramAccountTest < ActiveSupport::TestCase 4 | def setup 5 | @telegram_account = TelegramAccount.new first_name: 'John', last_name: 'Smith', telegram_id: 999_999_999_999 6 | end 7 | 8 | def test_name_without_username 9 | assert_equal 'John Smith', @telegram_account.name 10 | end 11 | 12 | def test_name_with_username 13 | @telegram_account.username = 'john_smith' 14 | assert_equal 'John Smith @john_smith', @telegram_account.name 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workers/telegram_accounts_refresh_worker.rb: -------------------------------------------------------------------------------- 1 | class TelegramAccountsRefreshWorker 2 | include Sidekiq::Worker 3 | 4 | sidekiq_options queue: :telegram 5 | 6 | def perform 7 | TelegramAccount.where.not(telegram_id: nil).find_each do |account| 8 | new_data = RedmineBots::Telegram.bot.get_chat(chat_id: account.telegram_id) 9 | account.update(**new_data['result'].slice('username', 'first_name', 'last_name').symbolize_keys) 10 | sleep 1 11 | rescue => e 12 | logger.warn("Error during telegram accounts refresh: #{e.message}") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/help_message.rb: -------------------------------------------------------------------------------- 1 | class RedmineBots::Telegram::Bot 2 | class HelpMessage 3 | def initialize(bot, action) 4 | @bot = bot 5 | @action = action 6 | end 7 | 8 | def to_s 9 | commands = action.private? ? bot.commands.select(&:private?) : bot.commands.select(&:group?) 10 | 11 | commands.select { |command| command.allowed?(action.user) }.map do |command| 12 | %[/#{command.name} - #{command.description}].chomp 13 | end.join("\n") 14 | end 15 | 16 | private 17 | 18 | attr_reader :bot, :action 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'pidfile', git: 'https://github.com/arturtr/pidfile.git' 2 | gem 'sidekiq-cron' 3 | gem 'sidekiq-rate-limiter', '0.1.3', require: 'sidekiq-rate-limiter/server' 4 | 5 | gem 'telegram-bot-ruby', '>= 0.11', '< 1.0' 6 | gem 'slack-ruby-bot' 7 | gem 'celluloid-io' 8 | gem 'tdlib-ruby', '~> 3.1.0' 9 | gem 'tdlib-schema', '~> 1.7.0' 10 | gem 'jwt' 11 | gem 'filelock' 12 | gem 'patron' 13 | 14 | group :test do 15 | gem 'timecop' 16 | gem 'spy' 17 | gem 'database_cleaner', '1.5.1' 18 | gem 'minitest-around' 19 | gem 'minitest-reporters', '<= 1.3.0' 20 | gem 'shoulda', '~> 3.6' 21 | end 22 | -------------------------------------------------------------------------------- /lib/redmine_bots/slack/event_handler.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Slack 2 | class EventHandler 3 | def self.event 4 | raise NotImplementedError 5 | end 6 | 7 | def self.to_proc 8 | ->(client, data) { new(client, data).call } 9 | end 10 | 11 | def initialize(client, data) 12 | @client = client 13 | @data = data 14 | end 15 | 16 | def call 17 | raise NotImplementedError 18 | end 19 | 20 | protected 21 | 22 | attr_reader :client, :data 23 | 24 | def reply(**attrs) 25 | client.say(attrs.merge(channel: data.channel)) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/settings/_redmine_bots.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'settings/redmine_bots/deprecation_warning' %> 2 | 3 | <% tabs = [ 4 | {name: 'slack', partial: 'settings/redmine_bots/slack', label: 'redmine_bots.label.slack'}, 5 | {name: 'telegram', partial: 'settings/redmine_bots/telegram', label: 'redmine_bots.label.telegram'}, 6 | ] %> 7 | 8 | <%= render_tabs tabs %> 9 | 10 | 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4.9 4 | - 2.6.5 5 | - 2.7.2 6 | 7 | branches: 8 | only: 9 | - master 10 | - develop 11 | 12 | addons: 13 | postgresql: "9.4" 14 | 15 | env: 16 | - REDMINE_VER=3.4-stable 17 | - REDMINE_VER=4.1-stable 18 | - REDMINE_VER=5.1-stable 19 | 20 | install: "echo skip bundle install" 21 | 22 | before_script: 23 | - psql -c 'create database travis_ci_test;' -U postgres 24 | services: 25 | - redis-server 26 | 27 | script: 28 | - export TESTSPACE=`pwd`/testspace 29 | - export NAME_OF_PLUGIN=redmine_bots 30 | - export PATH_TO_PLUGIN=`pwd` 31 | - export PATH_TO_REDMINE=$TESTSPACE/redmine 32 | - mkdir $TESTSPACE 33 | - cp test/support/* $TESTSPACE/ 34 | - bash -x ./travis.sh 35 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/handlers/handler_behaviour.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Bot::Handlers 2 | module HandlerBehaviour 3 | def match?(action) 4 | return false unless (action.private? && private?) || (action.group? && group?) 5 | 6 | if command? 7 | command, * = action.command 8 | 9 | return false unless command == name 10 | end 11 | 12 | true 13 | end 14 | 15 | def command? 16 | false 17 | end 18 | 19 | def name 20 | raise NotImplementedError 21 | end 22 | 23 | def private? 24 | false 25 | end 26 | 27 | def group? 28 | false 29 | end 30 | 31 | def description 32 | raise NotImplementedError 33 | end 34 | 35 | def allowed?(_user) 36 | false 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/fetch_all_chats.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class FetchAllChats < Command 3 | def initialize(*) 4 | super 5 | 6 | @chat_list = ChatList::Main.new 7 | @limit = 100 8 | @chat_futures = [] 9 | end 10 | 11 | def call 12 | fetch 13 | end 14 | 15 | private 16 | 17 | attr_reader :limit 18 | attr_accessor :chat_list 19 | 20 | def fetch 21 | client.load_chats(chat_list: chat_list, limit: limit).then do |update| 22 | case update 23 | when TD::Types::Ok 24 | fetch.wait! 25 | else 26 | next Concurrent::Promises.fulfilled_future(nil) 27 | end 28 | end.flat.rescue do |error| 29 | if error.code != 404 30 | raise error 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/tdlib/add_bot.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots::Telegram::Tdlib 2 | class AddBot < Command 3 | def call(bot_name) 4 | client.search_public_chat(username: bot_name).then do |chat| 5 | message = TD::Types::InputMessageContent::Text.new(text: TD::Types::FormattedText.new(text: '/start', entities: []), 6 | link_preview_options: nil, 7 | clear_draft: false) 8 | client.send_message(chat_id: chat.id, 9 | message_thread_id: nil, 10 | reply_to: nil, 11 | options: nil, 12 | reply_markup: nil, 13 | input_message_content: message) 14 | end.flat 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/redmine_telegram_setup/step_1.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t 'redmine_bots.settings.telegram.authorize_client' %>

2 |
3 |

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 |
5 | 6 | <%= form_tag telegram_setup_2_path, method: :post do %> 7 |

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 |

<%= t 'redmine_bots.settings.telegram.authorize_client' %>

2 |
3 |

<%= 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 |
5 | 6 | <%= form_tag telegram_setup_3_path, method: :post do %> 7 |

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 |

<%= t 'redmine_bots.settings.telegram.authorize_client' %>

2 |
3 |

<%= 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 |
5 | 6 | <%= form_tag telegram_setup_authorize_path, method: :post do %> 7 |

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 |
<%= t 'redmine_bots.settings.telegram.phone_number' %>: <%= @settings['telegram_phone_number'] %>
3 |
4 | <% end %> 5 | <%= t 'redmine_bots.settings.telegram.authorize_hint' %> 6 | <%= link_to telegram_setup_1_path do %><% end %> 7 | 8 | <%= link_to telegram_setup_reset_path, method: 'delete' do %><% end %> 9 | 10 |

11 | 12 |

<%= t 'redmine_bots.requirements.title' %>

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <%# if tdlib_path = (TD::Api::Dl.find_lib rescue nil) %> 26 | <%#= t 'redmine_bots.requirements.yes' %> 27 | <%# else %> 28 | <%#= t 'redmine_bots.requirements.no' %> 29 | <%# end %> 30 | 31 | 32 | 33 |
<%= t 'redmine_bots.requirements.telegram.valid' %><%= t 'redmine_bots.requirements.telegram.description' %>
34 | 35 |

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 |
58 | 59 |
60 |
61 |

WebHooks

62 | 63 | <% if Setting['protocol'] == 'https' %> 64 | <%= link_to telegram_bot_init_path, method: 'post' do %><% end %> 65 | 66 | <%= link_to telegram_bot_deinit_path, method: 'delete' do %><% end %> 67 | <% else %> 68 | <%= t 'redmine_bots.settings.telegram.web_hooks_warning' %> 69 | <% end %> 70 |
71 | 72 |
73 |

getUpdates

74 | 75 |
<%= t 'redmine_bots.settings.telegram.get_updates_hint' %>
76 |
77 |
78 | 79 | <%= hidden_field_tag 'settings[telegram_bot_name]', @settings['telegram_bot_name'] %> 80 | <%= hidden_field_tag 'settings[telegram_bot_id]', @settings['telegram_bot_id'] %> 81 | <%= hidden_field_tag 'settings[telegram_robot_id]', @settings['telegram_robot_id'] %> 82 | <%= hidden_field_tag 'settings[telegram_phone_number]', @settings['telegram_phone_number'] %> 83 | -------------------------------------------------------------------------------- /lib/redmine_bots/telegram/bot/authenticate.rb: -------------------------------------------------------------------------------- 1 | module RedmineBots 2 | module Telegram 3 | class Bot::Authenticate 4 | AUTH_TIMEOUT = 60.minutes 5 | 6 | attr_reader :login_error 7 | 8 | def self.call(user, auth_data, context:) 9 | new(user, auth_data, context: context).call 10 | end 11 | 12 | def initialize(user, auth_data, context:) 13 | @user, @auth_data, @context = user, Hash[auth_data.to_h.sort_by { |k, _| k }], context 14 | end 15 | 16 | def call 17 | return failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_logged')) unless @user.logged? 18 | return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_invalid')) unless hash_valid? 19 | return failure(I18n.t('redmine_bots.telegram.bot.login.errors.hash_outdated')) unless up_to_date? 20 | 21 | case @context 22 | when '2fa_connection' 23 | telegram_account = prepare_telegram_account(model_class: RedmineTwoFa::TelegramConnection) 24 | return failure(login_error) unless telegram_account 25 | when 'account_connection' 26 | telegram_account = prepare_telegram_account(model_class: TelegramAccount) 27 | return failure(login_error) unless telegram_account 28 | telegram_account.assign_attributes(@auth_data.slice('first_name', 'last_name', 'username')) 29 | else 30 | return failure('Invalid context') 31 | end 32 | 33 | TelegramAccount.transaction do 34 | return failure(I18n.t('redmine_bots.telegram.bot.login.errors.not_persisted')) unless telegram_account.save 35 | 36 | if create_account_after_2fa(telegram_account) 37 | return success(telegram_account) 38 | end 39 | 40 | raise ActiveRecord::Rollback 41 | end 42 | 43 | failure(login_error) 44 | end 45 | 46 | private 47 | 48 | attr_writer :login_error 49 | 50 | def prepare_telegram_account(model_class:) 51 | telegram_account = model_class.find_by(user_id: @user.id) 52 | 53 | if telegram_account.present? 54 | if telegram_account.telegram_id 55 | if @auth_data['id'].to_i != telegram_account.telegram_id 56 | self.login_error = I18n.t('redmine_bots.telegram.bot.login.errors.wrong_account') 57 | return nil 58 | end 59 | else 60 | telegram_account.telegram_id = @auth_data['id'] 61 | end 62 | else 63 | telegram_account = model_class.find_or_initialize_by(telegram_id: @auth_data['id']) 64 | if telegram_account.user_id 65 | if telegram_account.user_id != @user.id 66 | self.login_error = (I18n.t('redmine_bots.telegram.bot.login.errors.account_exists')) 67 | return nil 68 | end 69 | else 70 | telegram_account.user_id = @user.id 71 | end 72 | end 73 | telegram_account 74 | end 75 | 76 | def create_account_after_2fa(telegram_account) 77 | return true if telegram_account.is_a?(TelegramAccount) 78 | 79 | telegram_account_2fa = prepare_telegram_account(model_class: TelegramAccount) 80 | 81 | return false if telegram_account_2fa.blank? 82 | 83 | telegram_account_2fa.assign_attributes(@auth_data.slice('first_name', 'last_name', 'username')) 84 | 85 | telegram_account_2fa.save 86 | end 87 | 88 | def hash_valid? 89 | Utils.auth_hash(@auth_data) == @auth_data['hash'] 90 | end 91 | 92 | def up_to_date? 93 | Time.at(@auth_data['auth_date'].to_i) > Time.now - AUTH_TIMEOUT 94 | end 95 | 96 | def success(value) 97 | Result.new(true, value) 98 | end 99 | 100 | def failure(value) 101 | Result.new(false, value) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/unit/redmine_bots/bot/authenticate_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../../test_helper', __FILE__) 2 | 3 | class RedmineBots::Telegram::Bot::AuthenticateTest < ActiveSupport::TestCase 4 | fixtures :telegram_accounts, :users 5 | 6 | let(:described_class) { RedmineBots::Telegram::Bot::Authenticate } 7 | 8 | before do 9 | RedmineBots::Telegram::Utils.stubs(:auth_hash).returns('auth_hash') 10 | end 11 | 12 | describe 'when user is anonymous' do 13 | it 'returns failure result' do 14 | result = described_class.new(users(:anonymous), {}, context: 'account_connection').call 15 | 16 | expect(result.success?).must_equal false 17 | expect(result.value).must_equal "You're not logged in" 18 | end 19 | end 20 | 21 | describe 'when hash is not valid' do 22 | it 'returns failure result' do 23 | 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 24 | 25 | expect(result.success?).must_equal false 26 | expect(result.value).must_equal "Hash is invalid" 27 | end 28 | end 29 | 30 | describe 'when hash is outdated' do 31 | it 'returns failure result' do 32 | 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 33 | 34 | expect(result.success?).must_equal false 35 | expect(result.value).must_equal "Request is outdated" 36 | end 37 | end 38 | 39 | describe 'when telegram account found by user_id' do 40 | describe 'when telegram ids do not match' do 41 | it 'returns failure result' do 42 | 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 43 | 44 | expect(result.success?).must_equal false 45 | expect(result.value).must_equal "Wrong Telegram account" 46 | end 47 | end 48 | 49 | describe 'when telegram ids match' do 50 | it 'updates attributes and returns successful result' do 51 | 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 52 | 53 | account = TelegramAccount.find(1) 54 | 55 | expect(result.value).must_equal account 56 | expect(account.first_name).must_equal 'test' 57 | expect(account.last_name).must_equal 'test' 58 | expect(result.success?).must_equal true 59 | end 60 | end 61 | end 62 | 63 | describe 'when telegram account not found by user_id' do 64 | describe 'when user ids do not match' do 65 | it 'returns failure result' do 66 | 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 67 | 68 | expect(result.success?).must_equal false 69 | expect(result.value).must_equal "Wrong Telegram account" 70 | end 71 | end 72 | 73 | describe 'when telegram account does not have user_id' do 74 | it 'updates attributes and returns successful result' do 75 | 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 76 | 77 | account = TelegramAccount.last 78 | 79 | expect(result.value).must_equal account 80 | expect(account.user_id).must_equal users(:user_3).id 81 | expect(account.first_name).must_equal 'test' 82 | expect(account.last_name).must_equal 'test' 83 | expect(result.success?).must_equal true 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | redmine_bots: 3 | label: 4 | slack: Slack 5 | telegram: Telegram 6 | settings: 7 | deprecated_plugins: 'Warning! Deprecated plugins installed:' 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: Verification Token 13 | telegram: 14 | phone_number: "Phone Number" 15 | phone_number_hint: "Format: 78005553535" 16 | phone_code: "Telegram code" 17 | password: "Telegram password" 18 | password_hint: "If you do not have two-factor authentication enabled in the Telegram application, click the «Authorize» button." 19 | authorize_button_code: "Receive code" 20 | authorize_button: "Authorize" 21 | authorize_client: "Authorize Telegram client" 22 | authorize_hint: "To authorize Telegram, press the button below. Current authorization will be drop." 23 | plugin_link: "Back to the plugin settings" 24 | auth_step_1: "Receiving Telegram code" 25 | auth_step_2: "Code authorization" 26 | auth_step_3: "Password authorization" 27 | reset: "Reset cache and authorization" 28 | bot_init: Initialize bot 29 | bot_deinit: Deinitialize bot 30 | get_updates_hint: bundle exec rake redmine_bots:telegram 31 | web_hooks_warning: Need https to setup WebHooks 32 | tdlib_use_proxy: Use proxy for tdlib 33 | bot_use_proxy: Use proxy for bot 34 | proxy_settings: Proxy settings 35 | proxy_list: Proxy list 36 | proxy_alive: Proxy is accessible 37 | proxy_dead: Proxy is not accessible 38 | requirements: 39 | title: Requirements 40 | telegram: 41 | valid: Valid 42 | description: Description 43 | no: No 44 | yes: Yes 45 | rails_env: "Make sure your RAILS_ENV is production (current: %{rails_env})" 46 | redmine_host: "Make sure that you have correct host name in Redmine settings (current: %{host})" 47 | tdlib_installation: "You should place libtdjson in redmine_root/vendor or add it to ldconfig. Build instructions." 48 | telegram: 49 | authorize: 50 | success: Authentication complete! 51 | bot: 52 | start: 53 | instruction_html: | 54 | Now we need to connect your Redmine & Telegram accounts. 55 | Please, use /connect command for it. 56 | hello: Hello! 57 | connect: 58 | already_connected: Your accounts already connected 59 | wait_for_email: We sent email to address "%{email}". Please follow instructions from it. 60 | wrong_email: Wrong email. User with this email not found. Left %{attempts} attempts before the account will locked for an hour. 61 | blocked: Your account was blocked. Try after %{unblock} minutes. 62 | login_link: "Use this link to connect account to Redmine: %{link}" 63 | group: 64 | no_commands: "No commands for a group chat. Private chat commands:" 65 | private_command: 'This command is for private chat.' 66 | private: 67 | group_command: 'This command is for group chat.' 68 | help: 69 | start: "Start work with bot" 70 | connect: "Connect Redmine and Telegram account" 71 | help: "Help about commands" 72 | token: Get auth link 73 | login: 74 | success: Authentication succeed 75 | errors: 76 | not_logged: You're not logged in 77 | hash_invalid: Hash is invalid 78 | hash_outdated: Request is outdated 79 | wrong_account: Wrong Telegram account 80 | account_exists: This Telegram account is linked to another account 81 | not_persisted: Failed to persist Telegram account data 82 | invalid_token: Token is invalid 83 | follow_link: Please, follow link 84 | send_to_telegram: Send auth link to Telegram 85 | widget_not_visible: if widget isn not visible 86 | write_to_bot: "Please, write command /token to bot @%{bot} if widget is not visible" 87 | slack: 88 | commands: 89 | connect: connect Slack account with Redmine 90 | help: help information about commands 91 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | permission_view_telegram_account_info: Просмотр информации о профиле Telegram 3 | redmine_bots: 4 | label: 5 | slack: Slack 6 | telegram: Telegram 7 | settings: 8 | deprecated_plugins: 'Внимание! Установлены устаревшие плагины:' 9 | slack_oauth_token: OAuth Access Token 10 | slack_bot_oauth_token: Bot User OAuth Access Token 11 | slack_client_id: Client ID 12 | slack_client_secret: Client Secret 13 | slack_verification_token: Verification Token 14 | telegram: 15 | phone_number: "Номер телефона" 16 | phone_number_hint: "В формате: 78005553535" 17 | phone_code: "Код из Telegram" 18 | password: "Пароль из Telegram" 19 | password_hint: "Если у вас не включена двухфакторная аутентификация в приложении Telegram, нажмите кнопку «Авторизовать»." 20 | authorize_button_code: "Получить код авторизации" 21 | authorize_client: "Авторизовать клиент Telegram" 22 | authorize_hint: "Чтобы авторизировать Telegram, нажмите кнопку ниже. Текущая авторизация сбросится." 23 | authorize_button: "Авторизовать" 24 | plugin_link: "К настройкам плагина" 25 | auth_step_1: "Получение кода авторизации" 26 | auth_step_2: "Авторизация кода" 27 | auth_step_3: "Авторизация пароля" 28 | reset: "Сбросить кеш и авторизацию" 29 | bot_init: Инициализировать бота 30 | bot_deinit: Деинициализировать бота 31 | get_updates_hint: bundle exec rake redmine_bots:telegram 32 | web_hooks_warning: Нужен https протокол, чтобы настроить WebHooks 33 | tdlib_use_proxy: Использовать прокси для tdlib 34 | bot_use_proxy: Использовать прокси для бота 35 | proxy_settings: Настройки прокси 36 | proxy_list: Список прокси 37 | proxy_alive: Прокси доступен 38 | proxy_dead: Прокси недоступен 39 | requirements: 40 | title: Требования 41 | telegram: 42 | title: Требования 43 | valid: ОК 44 | description: Описание 45 | no: Нет 46 | yes: Да 47 | rails_env: "Убедитесь, что ваш RAILS_ENV — production (сейчас: %{rails_env})" 48 | redmine_host: "Убедитесь, что установлен правильный хост в настройках Redmine (сейчас: %{host})" 49 | tdlib_installation: "Необходимо поместить libtdjson в директорию redmine_root/vendor или добавить в ldconfig. Инструкции по сборке." 50 | telegram: 51 | authorize: 52 | success: Аутентификация прошла успешно! 53 | bot: 54 | start: 55 | instruction_html: | 56 | Чтобы связать аккаунты Redmine и Telegram, пожалуйста, введите команду /connect. 57 | hello: Здравствуйте! 58 | connect: 59 | already_connected: Ваши аккаунты уже связаны 60 | wait_for_email: Мы отправили подтверждение на адрес "%{email}". Пожалуйста, следуйте инструкциям из письма. 61 | wrong_email: Неверный email-адрес. Пользователь с таким адресом не найден. Осталось %{attempts} попытки, после чего аккаунт будет заблокирован на час. 62 | blocked: Ваш аккаунт заблокирован. Попробуйте через %{unblock} мин. 63 | login_link: "Для привязки аккаунта к Redmine проследуйте по ссылке: %{link}" 64 | group: 65 | no_commands: "Нет команд для группового чата. В приватном чате доступны следующие команды:" 66 | private_command: 'Данная команда обрабатывается в личных сообщениях.' 67 | private: 68 | group_command: 'Данная команда обрабатывается в групповых чатах.' 69 | help: 70 | start: "Начало работы с ботом" 71 | connect: "Связывание аккаунтов Redmine и Telegram" 72 | help: "Справка по командам" 73 | token: Получить ссылку для аутентификации 74 | login: 75 | success: Аутентификация прошла успешно 76 | errors: 77 | not_logged: Вы не залогинены в Redmine 78 | hash_invalid: Неверный Hash 79 | hash_outdated: Истекло время ожидания 80 | wrong_account: Неверный аккаунт Telegram 81 | account_exists: Этот аккаунт Telegram привязан к другой учётной записи 82 | not_persisted: Не удалось сохранить данные о Telegram-аккаунте 83 | invalid_token: Неверный token 84 | follow_link: Пожалуйста, проследуйте по ссылке 85 | send_to_telegram: Отправить ссылку в Telegram 86 | widget_not_visible: если виджет недоступен 87 | write_to_bot: "Пожалуйста, напишите команду /token боту @%{bot}, если не видно виджет" 88 | slack: 89 | commands: 90 | connect: связывание аккаунтов Slack и Redmine 91 | help: помощь по командам 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/centosadmin/redmine_bots.svg?branch=master)](https://travis-ci.org/centosadmin/redmine_bots) 2 | [![Rate at redmine.org](http://img.shields.io/badge/rate%20at-redmine.org-blue.svg?style=flat)](http://www.redmine.org/plugins/redmine_bots) 3 | 4 | # redmine_bots 5 | 6 | This plugin provides common stuff to build redmine plugins that involve Slack/Telegram API usage. 7 | 8 | ## Requirements 9 | 10 | * Ruby 2.4+ 11 | * Redmine 3.4+ 12 | * [redmine_sidekiq](https://github.com/southbridgeio/redmine_sidekiq) (also requires Redis) 13 | 14 | ### Upgrade from 0.2.0 to 0.3+ 15 | 16 | From 0.3.0 proxy settings are unified for tdlib and bot and there is support for multiple proxies. 17 | If you have used proxies before, you should reassign it on plugin settings page. 18 | 19 | ### Upgrade from 0.1.0 to 0.2+ 20 | 21 | Webhook URL is changed in 0.2.0 because of webhook secret. You need to reinitialize webhook from plugin settings page. 22 | 23 | ## Telegram 24 | 25 | Telegram support includes: 26 | 27 | * Common components to interact with Bot API 28 | * Common client commands that utilize [tdlib-ruby](https://github.com/centosadmin/tdlib-ruby) 29 | * [Telegram Login](https://core.telegram.org/widgets/login) to connect Redmine and Telegram accounts 30 | 31 | ### Tdlib 32 | In order to use tdlib client you need compiled [TDLib](https://github.com/tdlib/td). 33 | 34 | It should be placed it in `redmine_root/vendor` or added to [ldconfig](https://www.systutorials.com/docs/linux/man/8-ldconfig/). 35 | 36 | For CentOS you can use our repositories: 37 | 38 | http://rpms.southbridge.ru/rhel7/stable/x86_64/ 39 | 40 | http://rpms.southbridge.ru/rhel6/stable/x86_64/ 41 | 42 | And also SRPMS: 43 | 44 | http://rpms.southbridge.ru/rhel7/stable/SRPMS/ 45 | 46 | http://rpms.southbridge.ru/rhel6/stable/SRPMS/ 47 | 48 | To make telegram client working you should follow steps: 49 | 50 | * Be sure you set correct host in Redmine settings 51 | * Go to the plugin settings page 52 | * Press "Authorize Telegram client" button and follow instructions 53 | 54 | **IMPORTANT:** 2FA is not supported at the moment 55 | 56 | ### Bot API 57 | 58 | It is necessary to register a bot and get its token. 59 | There is a [@BotFather](https://telegram.me/botfather) bot used in Telegram for this purpose. 60 | Type `/start` to get a complete list of available commands. 61 | 62 | Type `/newbot` command to register a new bot. 63 | [@BotFather](https://telegram.me/botfather) will ask you to name the new bot. The bot's name must end with the "bot" word. 64 | On success @BotFather will give you a token for your new bot and a link so you could quickly add the bot to the contact list. 65 | You'll have to come up with a new name if registration fails. 66 | 67 | Set the Privacy mode to disabled with `/setprivacy`. This will let the bot listen to all group chats and write its logs to Redmine chat archive. 68 | 69 | Set bot domain with `/setdomain` for account connection via Telegram Login. Otherwise, you will receive `Bot domain invalid` error on account connection page. 70 | 71 | Enter the bot's token on the Plugin Settings page to add the bot to your chat. 72 | 73 | To add hints for commands for the bot, use command `/setcommands`. You need to send list of commands with descriptions. You can get this list from command `/help`. 74 | 75 | **Bot should be added to contacts of account used to authorize telegram client in plugin.** 76 | 77 | Bot can work in two [modes](https://core.telegram.org/bots/api#getting-updates) — getUpdates or WebHooks. 78 | 79 | #### getUpdates 80 | 81 | To work via getUpdates, you should run bot process `bundle exec rake redmine_bots:telegram`. 82 | This will drop bot WebHook setting. 83 | 84 | 85 | #### WebHooks 86 | 87 | To work via WebHooks, you should go to plugin settings and press button "Initialize bot" 88 | (bot token should be saved earlier, and notice that redmine should work on https) 89 | 90 | ## Slack 91 | 92 | Slack support includes: 93 | 94 | * Bot components 95 | * Rake task to daemonize slack bot 96 | * [Sign in with Slack](https://api.slack.com/docs/sign-in-with-slack) to connect Redmine and Slack accounts 97 | 98 | In order to use Slack integrations you need to follow these steps: 99 | 100 | * Create your own Slack app [here](https://api.slack.com/apps?new_app=1). 101 | 102 | * Go to your app page (you can view the list of all your apps [here](https://api.slack.com/apps)) and copy data from *App Credentials* block to plugin settings. 103 | 104 | * Create bot user on *Bot users* page. 105 | 106 | * Obtain your oauth tokens on *OAuth & Permissions* page and copy it to plugin settings. 107 | 108 | * Add redirect url for *Sign in with Slack*: `https://your.redmine.host/slack/sign_in/check` 109 | 110 | * Configure app scopes on demand. 111 | 112 | * Run bot with `bundle exec rake redmine_bots:slack` 113 | 114 | ## Migration from redmine_telegram_common 115 | 116 | You can transparently migrate your old data (telegram accounts, settings and tdlib db/files) to new DB structure if you used *redmine_telegram_common* before with `bundle exec rake redmine_bots:migrate_from_telegram_common`. 117 | 118 | 119 | ## Author of the Plugin 120 | 121 | The plugin is designed by [Southbridge](https://southbridge.io) 122 | --------------------------------------------------------------------------------