Skip to content
Merged
2 changes: 1 addition & 1 deletion app/controllers/concerns/telegram/subscription_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def confirm_remove_callback_query(channel_id)

# Построить текст списка подписок
def build_subscriptions_list(subscriptions)
text = "#{I18n.t('telegram_bot.channels.list.title')}\n\n"
text = "#{I18n.t('telegram_bot.channels.list.title', count: subscriptions.count)}\n\n"
text += "#{I18n.t('telegram_bot.channels.list.total', count: subscriptions.count)}\n\n"

subscriptions.each do |subscription|
Expand Down
32 changes: 22 additions & 10 deletions app/jobs/channels/bot_join_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,31 @@ def perform(channel_id)
channel.start_joining!

# Пытаемся вступить в канал
success = attempt_to_join_channel(channel)
join_result = attempt_to_join_channel(channel)

if success
if join_result == true
channel.mark_as_joined!
notify_admins_success(channel)
Rails.logger.info "Bot successfully joined channel #{channel.username}"
else
error_message = extract_error_message(success)
channel.mark_as_join_failed!(error_message)
notify_admins_failure(channel, error_message)
Rails.logger.error "Bot failed to join channel #{channel.username}: #{error_message}"
error_info = Channels::BotJoinErrorHandler.classify_error(join_result[:error_description])
error_context = Channels::BotJoinErrorHandler.get_error_context(error_info, channel)

channel.mark_as_join_failed!(join_result[:error_description])
notify_admins_failure(channel, join_result[:error_description])

# Логируем с детальным контекстом
Rails.logger.error "Bot failed to join channel #{channel.username} - #{error_info[:type]}: #{join_result[:error_description]}"
Rails.logger.error "Error context: #{error_context.to_json}"

# Отправляем уведомление в Bugsnag с полным контекстом
Bugsnag.notify(
StandardError.new("Bot join failed: #{error_info[:type]}"),
error_info[:admin_message]
) do |b|
b.metadata = error_context
b.severity = Channels::BotJoinErrorHandler.get_severity_level(error_info)
end
end
end
end
Expand Down Expand Up @@ -70,12 +84,10 @@ def extract_error_message(result)
end

def notify_admins_success(channel)
# TODO: Implement admin notifications in stage 4
Rails.logger.info "Bot successfully joined channel: #{channel.username} (#{channel.title})"
Channels::BotJoinNotificationService.new.notify_success(channel)
end

def notify_admins_failure(channel, error_message)
# TODO: Implement admin notifications in stage 4
Rails.logger.error "Bot failed to join channel #{channel.username}: #{error_message}"
Channels::BotJoinNotificationService.new.notify_failure(channel, error_message)
end
end
7 changes: 1 addition & 6 deletions app/models/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ class Channel < ApplicationRecord
validates :username, presence: true, uniqueness: true

# Enums
enum bot_join_status: {
not_joined: 0,
joining: 1,
joined: 2,
join_failed: 3
}
enum :bot_join_status, %w[not_joined joining joined join_failed]

# Scopes
scope :active, -> { where(deactivated_at: nil) }
Expand Down
105 changes: 105 additions & 0 deletions app/services/channels/bot_join_error_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Сервис для детальной обработки ошибок вступления бота в каналы
module Channels
class BotJoinErrorHandler
ERROR_TYPES = {
'bad request: chat not found' => {
type: :channel_not_found,
user_message: 'Канал не найден или был удален',
admin_message: 'Канал не существует в Telegram',
severity: :high,
retry_possible: false
},
'forbidden: bot was kicked from the channel' => {
type: :bot_kicked,
user_message: 'Бот был удален из канала',
admin_message: 'Бота исключили из канала',
severity: :high,
retry_possible: false
},
'forbidden: bot is not a member' => {
type: :bot_not_member,
user_message: 'Бот не является участником канала',
admin_message: 'Бот не добавлен в канал',
severity: :high,
retry_possible: false
},
'forbidden: user is deactivated' => {
type: :user_deactivated,
user_message: 'Пользователь деактивирован',
admin_message: 'Аккаунт пользователя деактивирован',
severity: :high,
retry_possible: false
},
'too many requests: retry after' => {
type: :rate_limit,
user_message: 'Слишком много запросов. Попробуйте позже.',
admin_message: 'Превышен лимит запросов к Telegram API',
severity: :medium,
retry_possible: true
},
'timeout' => {
type: :timeout,
user_message: 'Время ожидания истекло',
admin_message: 'Тайм-аут при подключении к Telegram API',
severity: :medium,
retry_possible: true
},
'network error' => {
type: :network_error,
user_message: 'Ошибка сети',
admin_message: 'Проблемы с сетевым подключением',
severity: :medium,
retry_possible: true
},
'invalid token' => {
type: :invalid_token,
user_message: 'Ошибка аутентификации бота',
admin_message: 'Неверный токен бота',
severity: :critical,
retry_possible: false
}
}.freeze

def self.classify_error(error_message)
error_lower = error_message.to_s.downcase

ERROR_TYPES.each do |pattern, info|
return info if error_lower.include?(pattern)
end

# Если не найдено совпадение, возвращаем ошибку по умолчанию
{
type: :unknown,
user_message: 'Неизвестная ошибка',
admin_message: error_message,
severity: :medium,
retry_possible: false
}
end

def self.get_error_context(error_info, channel)
{
channel: {
id: channel.id,
username: channel.username,
title: channel.title,
telegram_id: channel.telegram_id
},
error: error_info,
timestamp: Time.current,
environment: Rails.env,
bot_info: {
username: ApplicationConfig.bot_username
}
}
end

def self.should_retry?(error_info)
error_info[:retry_possible]
end

def self.get_severity_level(error_info)
error_info[:severity]
end
end
end
87 changes: 87 additions & 0 deletions app/services/channels/bot_join_notification_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Сервис для отправки уведомлений администраторам о результатах вступления бота в каналы
module Channels
class BotJoinNotificationService
def initialize
@bot = Telegram.bots[:default]
end

# Отправляет уведомление об успешном вступлении бота в канал
def notify_success(channel)
return unless should_notify?

admins.each do |admin|
send_message_to_admin(
admin.telegram_id,
success_message(channel)
)
end

Rails.logger.info "Sent bot join success notifications to #{admins.count} admins for channel #{channel.username}"
end

# Отправляет уведомление о неудачном вступлении бота в канал
def notify_failure(channel, error)
return unless should_notify?

admins.each do |admin|
send_message_to_admin(
admin.telegram_id,
failure_message(channel, error)
)
end

Rails.logger.info "Sent bot join failure notifications to #{admins.count} admins for channel #{channel.username}"
end

private

def admins
@admins ||= TelegramUser.where(is_admin: true)
end

def should_notify?
admins.any?
end

def send_message_to_admin(telegram_id, text)
begin
@bot.send_message(
chat_id: telegram_id,
text: text,
parse_mode: 'Markdown'
)
rescue StandardError => e
Rails.logger.error "Failed to send notification to admin #{telegram_id}: #{e.message}"
Bugsnag.notify(e) do |b|
b.metadata = {
service: self.class.name,
admin_telegram_id: telegram_id,
action: 'send_notification'
}
end
end
end

def success_message(channel)
I18n.t(
'telegram_bot.channels.bot_join.success',
channel_title: channel.title,
channel_username: channel.username,
channel_id: channel.telegram_id,
subscribers_count: channel.subscribers_count || 0,
joined_at: I18n.l(channel.bot_join_at, format: :short)
)
end

def failure_message(channel, error)
I18n.t(
'telegram_bot.channels.bot_join.failure',
channel_title: channel.title,
channel_username: channel.username,
channel_id: channel.telegram_id,
error_message: error,
failed_at: I18n.l(Time.current, format: :short)
)
end
end
end
5 changes: 5 additions & 0 deletions app/services/telegram/channel_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ def add_channel_to_database(channel_username)
subscribers_count: channel_info[:member_count]
)

# Если канал был деактивирован - активируем его
if !channel.active?
channel.activate!
end

# Если канал еще не вступал, запускаем задачу для вступления
if channel.not_joined?
Channels::BotJoinJob.perform_later(channel.id)
Expand Down
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ en:
Unfortunately, I don't understand this type of message yet.
Please send me a text link to the channel you want to follow
to receive announcements about important news only, without the fluff.

channels:
add:
success: "✅ Channel %{channel} added!\n\nTotal channels: %{count}"
updated: "✅ Channel %{channel} updated!"
22 changes: 22 additions & 0 deletions config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ ru:
success: "✅ Канал %{channel} добавлен!\n\nВсего каналов: %{count}"
suggest_another: "💪 Отлично!\nДобавляй следующий канал - присылай ссылку или имя"
already_subscribed: "ℹ️ Ты уже подписан на канал %{channel}. Присылай ссылку на другой"
updated: "✅ Канал %{channel} обновлён!"
not_found: "❌ Канал %{channel} не найден или недоступен"
invalid_format: "❌ Неверный формат. Отправь имя канала (@channelname) или ссылку (t.me/channelname)"
error: "❌ Ошибка при добавлении канала: %{error}"
Expand Down Expand Up @@ -306,3 +307,24 @@ ru:
📤 Отправлено: %{sent_count}
❌ Ошибок: %{error_count}
👥 Всего подписчиков: %{total_subscribers}

bot_join:
success: |
✅ Бот успешно вступил в канал!

📺 *Канал:* %{channel_title} (@%{channel_username})
🆔 *ID:* %{channel_id}
👥 *Подписчиков:* %{subscribers_count}
⏰ *Время вступления:* %{joined_at}

Теперь бот будет получать посты из этого канала.
failure: |
❌ Не удалось вступить в канал!

📺 *Канал:* %{channel_title} (@%{channel_username})
🆔 *ID:* %{channel_id}
❌ *Ошибка:* %{error_message}
⏰ *Время ошибки:* %{failed_at}

Бот не сможет получать посты из этого канала.
Проверьте настройки приватности канала или права бота.
Loading
Loading