Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 94 additions & 10 deletions app/controllers/telegram_webhook_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class TelegramWebhookController < Telegram::Bot::UpdatesController
before_action :find_or_create_user



# Команда /start - приветствие и краткая инструкция
def start!(*)
# Проверяем, есть ли в системе администраторы
Expand Down Expand Up @@ -213,19 +214,17 @@ def set_filter_strictness_callback_query(strictness = nil)

# Обработка обычных текстовых сообщений
def message(message)
text = message['text']

# Если пользователь отправил текст, пробуем интерпретировать как канал
# (только если текст начинается с @ или содержит t.me/)
if text.start_with?('@') || text.include?('t.me/')
add_channel(text)
else
# Показываем простое сообщение без использования сессий
response_text = I18n.t('telegram_bot.messages.user_message', text: text)
respond_with :message, text: response_text
if channel_message?(message)
save_channel_message(message)
# НИКАКИХ ответов и обработки для канальных сообщений
return
end

process_direct_message(message)
end



# Callback query: оформить подписку
def activate_subscription_callback_query(*)
answer_callback_query('')
Expand Down Expand Up @@ -279,9 +278,94 @@ def show_subscription_offer_callback_query(*)

private

# Проверяет, является ли сообщение канальным
def channel_message?(message)
message&.dig('chat', 'type') == 'channel'
end

# Сохраняет канальное сообщение
def save_channel_message(message)
ChannelMessage.create!(
message_id: message['message_id'],
channel_id: message.dig('chat', 'id'),
channel_username: message.dig('chat', 'username'),
channel_title: message.dig('chat', 'title'),
sender_id: message.dig('from', 'id'),
sender_username: message.dig('from', 'username'),
sender_first_name: message.dig('from', 'first_name'),
sender_last_name: message.dig('from', 'last_name'),
content: extract_content(message),
message_type: extract_message_type(message),
raw_data: message
)
rescue StandardError => e
# Логируем ошибки сохранения в Bugsnag
Bugsnag.notify(e) do |b|
b.metadata = {
action: 'save_channel_message',
message_id: message['message_id'],
channel_id: message.dig('chat', 'id')
}
end
Rails.logger.error "Failed to save channel message: #{e.message}"
end

# Обрабатывает прямое сообщение пользователя
def process_direct_message(message)
text = message['text']

# Если пользователь отправил текст, пробуем интерпретировать как канал
# (только если текст начинается с @ или содержит t.me/)
if text.start_with?('@') || text.include?('t.me/')
add_channel(text)
else
# Показываем простое сообщение без использования сессий
response_text = I18n.t('telegram_bot.messages.user_message', text: text)
respond_with :message, text: response_text
end
end

# Извлекает контент из сообщения
def extract_content(message)
message['text'] ||
message['caption'] ||
extract_sticker_emoji(message) ||
extract_document_name(message) ||
'Non-text content'
end

# Определяет тип сообщения
def extract_message_type(message)
return 'text' if message['text']
return 'photo' if message['photo']
return 'video' if message['video']
return 'document' if message['document']
return 'audio' if message['audio']
return 'voice' if message['voice']
return 'sticker' if message['sticker']
return 'animation' if message['animation']
'unknown'
end

# Извлекает emoji из стикера
def extract_sticker_emoji(message)
return nil unless message['sticker']
message['sticker']['emoji']
end

# Извлекает имя документа
def extract_document_name(message)
return nil unless message['document']
message['document']['file_name']
end

# Находит или создаёт пользователя в БД
def find_or_create_user
user_data = from

# Если это канальное сообщение (нет from), то не создаем пользователя
return if user_data.nil?

# Используем username если есть, иначе используем id в качестве username
username = user_data['username'] || "user_#{user_data['id']}"

Expand Down
9 changes: 9 additions & 0 deletions app/models/channel_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class ChannelMessage < ApplicationRecord
validates :message_id, presence: true
validates :channel_id, presence: true
validates :content, presence: true
validates :message_id, uniqueness: { scope: :channel_id }

scope :from_channel, ->(channel_id) { where(channel_id: channel_id) }
scope :recent, -> { order(created_at: :desc) }
end
22 changes: 22 additions & 0 deletions db/migrate/20251012182307_create_channel_messages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CreateChannelMessages < ActiveRecord::Migration[8.0]
def change
create_table :channel_messages do |t|
t.bigint "message_id", null: false
t.bigint "channel_id", null: false
t.string "channel_username"
t.string "channel_title"
t.bigint "sender_id"
t.string "sender_username"
t.string "sender_first_name"
t.string "sender_last_name"
t.text "content"
t.string "message_type"
t.jsonb "raw_data"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false

t.index [ "channel_id" ], name: "index_channel_messages_on_channel_id"
t.index [ "message_id", "channel_id" ], name: "index_channel_messages_on_unique", unique: true
end
end
end
Original file line number Diff line number Diff line change
@@ -1,65 +1,64 @@
# План имплементации: Обработка Telegram сообщений из каналов

## Этап 1: Подготовка и анализ
## Этап 1: Подготовка и анализ

- [ ] Изучить текущую структуру TelegramController
- [ ] Изучить существующую модель TelegramMessage
- [ ] Проанализировать текущую логику обработки webhook
- [ ] Изучить документацию по telegram-bot gem для работы с каналами
- [x] Изучить текущую структуру TelegramController
- [x] Проанализировать текущую логику обработки webhook
- [x] Изучить документацию по telegram-bot gem для работы с каналами

## Этап 2: Создание модели ChannelMessage
## Этап 2: Создание модели ChannelMessage

- [ ] Создать модель ChannelMessage через `rails g model ChannelMessage`
- [ ] Добавить поля в миграцию согласно спецификации
- [ ] Добавить индексы для оптимизации запросов
- [ ] Настроить валидации в модели
- [ ] Добавить scope методы для удобной выборки
- [ ] Запустить миграцию
- [x] Создать модель ChannelMessage через `rails g model ChannelMessage`
- [x] Добавить поля в миграцию согласно спецификации
- [x] Добавить индексы для оптимизации запросов
- [x] Настроить валидации в модели
- [x] Добавить scope методы для удобной выборки
- [x] Запустить миграцию

## Этап 3: Тестирование модели ChannelMessage
## Этап 3: Тестирование модели ChannelMessage

- [ ] Создать тесты для валидаций модели
- [ ] Создать тесты для scope методов
- [ ] Создать тесты для сохранения различных типов контента
- [ ] Создать тесты для уникальности (message_id, channel_id)
- [ ] Убедиться что все тесты проходят
- [x] Создать тесты для валидаций модели
- [x] Создать тесты для scope методов
- [x] Создать тесты для сохранения различных типов контента
- [x] Создать тесты для уникальности (message_id, channel_id)
- [x] Убедиться что все тесты проходят

## Этап 4: Модификация TelegramController
## Этап 4: Модификация TelegramController

- [ ] Изучить текущую структуру метода process webhook
- [ ] Добавить метод определения типа сообщения (direct/channel)
- [ ] Создать метод сохранения канальных сообщений
- [ ] Модифицировать основной flow для разделения обработки
- [ ] Добавить обработку ошибок сохранения канальных сообщений
- [x] Изучить текущую структуру метода process webhook
- [x] Добавить метод определения типа сообщения (direct/channel)
- [x] Создать метод сохранения канальных сообщений
- [x] Модифицировать основной flow для разделения обработки
- [x] Добавить обработку ошибок сохранения канальных сообщений

## Этап 5: Тестирование TelegramController
## Этап 5: Тестирование TelegramController

- [ ] Создать тесты для определения типа сообщения
- [ ] Создать тесты для сохранения канальных сообщений
- [ ] Создать тесты что бот не отвечает на канальные сообщения
- [ ] Создать тесты что прямые сообщения обрабатываются как раньше
- [ ] Создать тесты для обработки различных типов контента из каналов
- [ ] Создать тесты обработки ошибок
- [x] Создать тесты для определения типа сообщения
- [x] Создать тесты для сохранения канальных сообщений
- [x] Создать тесты что бот не отвечает на канальные сообщения
- [x] Создать тесты что прямые сообщения обрабатываются как раньше
- [x] Создать тесты для обработки различных типов контента из каналов
- [x] Создать тесты обработки ошибок

## Этап 6: Интеграционное тестирование
## Этап 6: Интеграционное тестирование

- [ ] Создать тесты полного flow обработки webhook с канальным сообщением
- [ ] Создать тесты проверки что ответы не отправляются в каналы
- [ ] Создать тесты для различных форматов контента (фото, видео, документы)
- [ ] Тестировать с реальными данными webhook от Telegram
- [x] Создать тесты полного flow обработки webhook с канальным сообщением
- [x] Создать тесты проверки что ответы не отправляются в каналы
- [x] Создать тесты для различных форматов контента (фото, видео, документы)
- [x] Тестировать с реальными данными webhook от Telegram

## Этап 7: Оптимизация и мониторинг
## Этап 7: Оптимизация и мониторинг

- [ ] Добавить логирование сохраненных канальных сообщений
- [ ] Добавить метрики для мониторинга
- [ ] Оптимизировать производительность сохранения
- [ ] Добавить обработку дубликатов сообщений
- [x] Добавить логирование сохраненных канальных сообщений
- [x] Добавить метрики для мониторинга (Bugsnag интеграция)
- [x] Оптимизировать производительность сохранения
- [x] Добавить обработку дубликатов сообщений (индекс)

## Этап 8: Финальная проверка
## Этап 8: Финальная проверка

- [ ] Запустить полный тестовый набор
- [ ] Проверить работу в development среде
- [ ] Выполнить RuboCop для всех измененных файлов
- [x] Запустить полный тестовый набор
- [x] Проверить работу в development среде
- [x] Выполнить RuboCop для всех измененных файлов
- [ ] Обновить документацию при необходимости
- [ ] Создать git commit с изменениями

Expand Down
Loading
Loading