jabnews/docs/design.md
2026-03-22 02:20:02 +03:00

167 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Задание
Необходимо сделать программу для формирования новостей в jabber конференции.
Принцип работы программы: подписывается на RSS ленту и все появляющиеся новости транслирует в jabber конференцию.
## Интерфейс взаимодействия
1. Это изначально jabber бот. Управление - администратором только.
2. Управление может происходить личными сообщениями и через сообщения в конференции.
3. Из личных сообщений доступны 5 команд (формат ниже такой: <COMMAND> - <DESCRIPTION>):
а) join <room@conference.jabber.org> - зайти в указанную конферению
б) exit <room@conference.jabber.org> - выйти из указанной конференции (в БД запись становится is_enabled=false, а повторный join в комнату включает запись обратно)
в) list - выдает список комнат, в которые бот заведен текущим пользователем (от которого принял команду) - только комнаты, куда именно этот JID отправил команду join
г) list-all - выдает общий список комнат и кто бота в них завел, с признаком is_enabled (команда доступна и показывается в help только суперадмину - владельцу бота)
д) help - показать справку, состояющую из всех команд выше (разный для обычного пользователя и владельца бота)
е) если получено что-то другое, то выдавать в ответ справку, как при команде help
4. Из конференции (комнаты) доступны команды ниже. Команда - это сообщение, начинающееся с определенного набора символов. Формат описания команд: <COMMAND_SYMBOLS> <COMMAND> - <DESCRIPTION>:
а) <COMMAND_SYMBOLS> subscribe <RSS> <INTERVAL_MINUTES> (default 15 min) - подписаться в этой <room@conference.jabber.org> на указанную <RSS>
б) <COMMAND_SYMBOLS> list - вывести список <RSS>, на которые оформлена подписка в этот <room@conference.jabber.org>
в) <COMMAND_SYMBOLS> unsubscribe <RSS> - отписаться в этой <room@conference.jabber.org> от указанной <RSS>
г) <COMMAND_SYMBOLS> cmd <NEW_COMMAND_SYMBOLS> - сменить командные символы на новые
д) <COMMAND_SYMBOLS> help - вывести справку, состояющую из всех команд выше (для комнаты, не для личных сообщений) - причем вместо <COMMAND_SYMBOLS> нужно подставить текущее значение символов.
5. Из конференции можно обратиться к боту - тогда бот должен выдать необходимую информацию для общения с ним.
а) если сообщение начинается с ника бота, то в ответ нужно написать: <COMMAND_SYMBOLS> help - справка по командам
- в <COMMAND_SYMBOLS> нужно подставить текущее значение символов
6. COMMAND_SYMBOLS по-умолчанию задать - '!' (просто восклицательный знак)
### Уточнения реализации интерфейса
1. Если комната была выключена долго, при повторном join last_seen не сбрасывается. По запросу из п.7 технической реализации все накопившиеся за это время новости уйдут разом. Это желаемое поведение.
2. П.5 (ответ на ник бота) — для всех или только для админа? П.1 говорит "управление — администратором только", но отвечать на 'botname, help' логично для любого участника комнаты. Явного исключения нет.
- 'botname, help' приравнять к команде '! help'
- вызов команды '! help' из комнаты доступен всем. если вызвал не админ, то в возвращаемом тексте приписать: админ - JID админа
3. Что делать, если команду в комнате пишет не-админ? Описано только "принимать только от админа", но не описана реакция: молча игнорировать или отвечать что-то вроде "недостаточно прав"?
- если это не команда help - игнорировать.
4. Один админ на комнату — что при повторном join другим JID? Уточнено "один админ", но не описано поведение при коллизии: если JID_A уже является админом room1, а JID_B отправляет join room1 — ошибка? Перезапись? Молча игнорируется?
- написать, что "я уже присутствую в комнате, админ - JID админа"
5. Нельзя выполнять exit из "чужой" комнаты (если я не админ - я не могу вывести бота из комнаты)
6. Сейчас команды subscribe/unsubscribe используются в контексте RSS - это сделано намеренно, при появлении других источников будем адаптировать команды управления
## Техническая реализация
1. Контейнеризация - docker compose
2. Стек - python, библиотека slixmpp (https://pypi.org/project/slixmpp/).
3. Пример использования библиотеки
```python
import asyncio
import feedparser
from slixmpp import ClientXMPP
RECIPIENTS = ["user@example.com"]
RSS_URL = "https://lenta.ru/rss/news"
class NewsBot(ClientXMPP):
def __init__(self, jid, password):
super().__init__(jid, password)
self.add_event_handler("session_start", self.on_start)
async def on_start(self, event):
self.send_presence()
await self.get_roster()
while True:
feed = feedparser.parse(RSS_URL)
for entry in feed.entries[:3]:
for recipient in RECIPIENTS:
self.send_message(
mto=recipient,
mbody=f"{entry.title}\n{entry.link}",
mtype="chat"
)
await asyncio.sleep(300) # каждые 5 минут
bot = NewsBot("bot@your-server.com", "password")
bot.connect()
bot.process(forever=True)
```
4. БД для хранения настроек - SQLite, должна подключаться внутрь docker контейнера, а храниться на хосте
- таблица для хранения COMMAND_SYMBOLS: комната и COMMAND_SYMBOL - но сами символы нужно кешировать, чтобы не искать в БД каждый раз
- таблица для подписок subscriptions: комната, subscription_type (значения: rss), source (URL, etc.), интервал опроса в минутах (но не меньше 5, валиден только для URL-based источников - не телеграм), время последней проверки (timestamp, тоже валидно только для URL-based)
- таблица комнат rooms (админом считается тот, кто добавил бота в комнату): комната, jid админа (команды из комнаты можно принимать только от админа), признак включенности комнаты is_enabled
- таблица отправленных новостей в комнаты: комната, subscription_id, ID отправленного сообщения (каждого, в RSS, например, возможны вставки)
5. сборка питон - с использованием uv
6. структура проекта
```
src/
domain/
subscriptions/
entities.py — Subscription (комната, source, subscription_type,
interval_minutes, last_seen)
ports.py — SubscriptionRepository (add, update, remove,
list_by_room, get_due)
news/
entities.py — NewsItem (id, title, link)
ports.py — NewsFetcher, NewsPublisher, SentNewsRepository
usecases.py — FetchAndPublishNews
admin/
entities.py — Room (jid, admin_jid, is_enabled), Admin
ports.py — CommandResponder, RoomRepository,
CommandSymbolsRepository
usecases.py — HandleJoin, HandleExit, HandleSubscribe,
HandleUnsubscribe, HandleList, HandleListAll,
HandleCmd, HandleHelp
adapters/
jabber/
connection.py — XMPP-соединение (единственное, владелец бота)
news_publisher.py — реализует domain/news/ports.NewsPublisher
command_responder.py — реализует domain/admin/ports.CommandResponder
command_handler.py — входящий адаптер: слушает сообщения →
вызывает usecases admin
sources/
rss/
fetcher.py — реализует domain/news/ports.NewsFetcher
db/
rooms.py — реализует domain/admin/ports.RoomRepository
subscriptions.py — реализует domain/subscriptions/ports.SubscriptionRepository
sent_news.py — реализует domain/news/ports.SentNewsRepository
command_symbols.py — реализует domain/admin/ports.CommandSymbolsRepository
(+ кэш в памяти)
scheduled/
news_checker.py — входящий адаптер: таймер каждые 5 мин →
вызывает domain/news/usecases
main.py — composition root: сборка зависимостей, запуск бота
docs/ — документация для разработки
pyproject.toml — файл сборки uv
Dockerfile — файл с описанием контейнера
docker-compose.yaml — описание запуска контейнера
env.example — примеры секретов (настоящий .env подключается
в docker-compose.yaml)
```
Направления зависимостей:
domain/admin → domain/subscriptions
domain/news → domain/subscriptions
adapters/* → domain/*
main.py → adapters/* + domain/*
domain/admin ↛ domain/news
domain/news ↛ domain/admin
ВАЖНО: важно реализовать модульность внутри кода. модульность подразумевает чистый код, возможность расширения (возможно, появится парсинг сайтов или трансляция из телеграм)
7. бот каждые 5 минут должен инициировать проверку новостей (scheduled task запускается каждые 5 минут) - только для URL-based источников (телеграм - по-другому будет, когда будет)
7.1. Алгоритм работы с RSS:
- собрать все RSS, которые должны быть опрошены - псевдокод для запроса: select * from subscriptions as subs left join rooms as room on subs.room_id = room.id where last_seen + check_interval::interval <= now() and room.is_enabled AND subs.subscription_type = 'rss'
- дедуплицировать RSS, чтобы не делать опросы дважды (из результата запроса на предыдущем пункте выбираем все distinct rss - используем set)
- опросить каждую RSS
- если в RSS появились новые сообщения, то отправить в комнаты сообщения
- если отправить в комнату не удалось по причине, что бот уже не в комнате (вдруг его удалили), то эта комната выключается в настройках и больше не участвует в сборе списка RSS для опроса
- после отправки каждого сообщения оно фиксируется в таблице отправленных новостей
- после отправки всех новостей из конкретной RSS в конкретную комнату в настройках обновляется время последней проверки
7.2. Источники, отличные от RSS, будут добавлены позднее.
8. в env.example необходимо указать JID_OWNER - это JID владельца бота, только ему будет доступна команда list-all
### Уточнения технической реализации
1. Таблица rooms — один или несколько админов на комнату? Схема: (комната, jid_админа) — допускает несколько записей на одну комнату. Что происходит если два разных JID выполнили join в одну комнату? Добавляются два админа, или второй join получает ошибку?
- на комнату один админ
2. subscribe на уже существующую RSS в комнате. Команда вызвана повторно, возможно с другим интервалом. Обновить интервал? Вернуть ошибку? Не описано.
- обновить интервал
3. last_seen для новой подписки ставить текущей датой now()
- поскольку таблица sent_news пустая, то ожидается, что при первом срабатывании scheduled задачи будет опубликовано много новостей.