design
This commit is contained in:
commit
6c92f75ca7
166
docs/design.md
Normal file
166
docs/design.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Задание
|
||||
|
||||
Необходимо сделать программу для формирования новостей в 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 задачи будет опубликовано много новостей.
|
||||
Loading…
x
Reference in New Issue
Block a user