Compare commits

...

11 Commits

Author SHA1 Message Date
9cf6a7e940 docs: clarify deployed bot version 2026-08-03 01:34:53 +03:00
b8b9ec75b3 refactor: organize repository around legal platform 2026-08-03 01:33:06 +03:00
c94131effc Merge pull request 'feat: export messages after last cutoff' (#5) from feature/export-from-last-cutoff into main
Reviewed-on: agent/telegram-ai-infrastructure#5
2026-08-02 21:51:15 +00:00
d9b4f19af3 feat: export messages after last cutoff 2026-08-03 00:49:35 +03:00
d4ee76948f Merge pull request 'docs: update project status and AI guidance' (#4) from docs/ai-skills-for-beginners into main
Reviewed-on: agent/telegram-ai-infrastructure#4
2026-08-02 21:43:28 +00:00
cf54bc2246 docs: update project status and AI guidance 2026-08-03 00:42:15 +03:00
a67bdbfaa4 Merge pull request 'docs: record Synology deployment' (#3) from feature/synology-deployment into main
Reviewed-on: agent/telegram-ai-infrastructure#3
2026-08-02 21:39:01 +00:00
f4a376ff4b docs: record Synology deployment 2026-08-02 23:55:16 +03:00
ba96b3e633 Merge pull request 'feat: add Telegram secretary bot' (#2) from feature/telegram-secretary-bot into main
Reviewed-on: agent/telegram-ai-infrastructure#2
2026-08-02 20:20:40 +00:00
caaba36a0c feat: add Telegram secretary bot 2026-08-02 23:17:56 +03:00
2921dbc152 Merge pull request 'docs: record project status and Telegram setup' (#1) from codex/project-status-20260731 into main
Reviewed-on: agent/telegram-ai-infrastructure#1
2026-07-31 21:05:50 +00:00
14 changed files with 1289 additions and 156 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

5
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Local credentials and secrets
credentials.txt
credentials.json
# Python/runtime artifacts (for the upcoming bot implementation)
__pycache__/
@@ -10,3 +10,6 @@ __pycache__/
.env
.env.*
!.env.example
# Local bot archive
data/

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
# Акылдаш
Акылдаш — проект юридической информационно-аналитической платформы. Цель —
собирать правовые источники на законных основаниях, сохранять их происхождение
и версии, готовить данные для поиска и RAG, а затем предоставлять результаты
через API и пользовательский интерфейс со ссылками на первоисточники.
Telegram-бот — только часть рабочего окружения команды, а не основной продукт.
## Текущее состояние
Сейчас реализован Telegram-бот-секретарь версии `0.2.2`. Он сохраняет сообщения
рабочих тем, формирует Markdown-экспорты и отмечает завершённые обсуждения.
| Компонент | Версия | Состояние |
|---|---:|---|
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
| Backend | — | ещё не создан |
| Frontend | — | ещё не создан |
| Сбор и обработка правовых данных | — | ещё не созданы |
| RAG и база знаний | — | ещё не созданы |
## Структура репозитория
```text
docs/ структурированная документация проекта
decisions/ принятые архитектурные и продуктовые решения
operations/ состояние проекта и рабочие процессы
product/ назначение, границы и развитие продукта
team/ материалы для команды
tools/
telegram-bot/ бот рабочего Telegram-пространства
```
Каталоги для загрузки и обработки источников, RAG, backend и frontend будут
создаваться с первой реальной задачей в соответствующей области. Это позволит
выбрать структуру по фактическим требованиям, а не поддерживать пустой каркас.
Начать знакомство с проектом: [документация](docs/README.md) и
[обзор продукта](docs/product/project-overview.md).
## Конфиденциальные материалы
Секреты, персональные данные, договоры и материалы по правовой защите проекта
не должны храниться в этом репозитории. Для них нужен отдельный закрытый
репозиторий или защищённое хранилище с минимально необходимыми правами доступа,
журналированием и резервным копированием. Здесь допустимы только несекретные
правила и ссылки на такие материалы без раскрытия их содержания.
## Проверка Telegram-бота
```bash
python3 -m unittest discover -s tools/telegram-bot -v
```
---
Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан

View File

@@ -1,118 +0,0 @@
# Статус проекта
Последняя проверка: 2026-07-31
Назначение документа: быстро восстановить контекст проекта для участников команды и будущих агентов.
## Краткий итог
Telegram-инфраструктура подготовлена для начала разработки бота. Супергруппа и бот существуют, бот добавлен в группу, Privacy Mode отключён, темы созданы и проверены через Telegram Bot API. Исходный код бота пока не создан.
Ближайшая цель — реализовать минимальный архиватор сообщений с сохранением метаданных и Markdown-экспортом обсуждений.
## Уже сделано
### Telegram
- Создана супергруппа `Акылдаш`.
- Создан бот `help_clerk_bot`.
- Бот добавлен в супергруппу с необходимыми правами.
- Privacy Mode бота отключён.
- Созданы и проверены рабочие темы.
- Проверено подключение к Telegram Bot API.
- Webhook у бота не установлен; доступен режим long polling через `getUpdates`.
### ID тем
| Тема | `message_thread_id` | Состояние |
|---|---:|---|
| Общее | отсутствует | стандартная тема |
| MVP | `2` | подтверждено тестовым сообщением |
| Решения | `4` | подтверждено тестовым сообщением |
| Обсуждение | `6` | подтверждено тестовым сообщением |
| Работа с ИИ | `8` | подтверждено тестовым сообщением |
### Документация и правила
- Подготовлен общий план реализации в `TEAM_AI_TELEGRAM_IMPLEMENTATION_PLAN.md`.
- Зафиксированы границы MVP и правила работы с данными в `docs/decisions/001-mvp-boundaries-and-rules.md`.
- В MVP не входит автоматический вызов API языковой модели.
- Анализ экспортов выполняется вручную в обычном ChatGPT.
- Telegram используется как рабочий штаб, Git/Gitea — как источник утверждённых материалов.
### Доступы
- Локальные доступы находятся в `credentials.txt`.
- `credentials.txt` добавлен в `.gitignore`.
- Секреты нельзя добавлять в Git, логи, экспорты или сообщения бота.
## Пока не сделано
### Репозиторий и приложение
- Не создан исходный код бота.
- Не определена и не зафиксирована структура Python-проекта.
- Локальный каталог `.git` пуст; локальный Git-репозиторий ещё нужно корректно инициализировать или привязать к удалённому репозиторию Gitea.
- Не настроены Docker-файлы для Synology.
- Не настроен CI/CD.
### Хранилище
- Не подключена база данных.
- Не утверждена окончательная схема PostgreSQL.
- Не определены сроки хранения сообщений, вложений и экспортов.
- Не определён согласованный список пользователей с доступом к архиву.
### Функции бота
- Приём и сохранение сообщений.
- Сохранение автора, даты, темы, ID сообщения и reply-связи.
- Сохранение доступных вложений.
- Ручная пометка сообщений для экспорта.
- Экспорт темы или диапазона в Markdown.
- Формирование готовой инструкции для ручной загрузки в ChatGPT.
- Возврат отчёта пользователя в Telegram.
- Команды `/help`, `/status` и команда экспорта.
### Группа и рабочий процесс
- Не подготовлены закреплённые сообщения с правилами тем.
- Не проверены сценарии закрытия темы `MVP` после фиксации состава первой версии.
- Не настроены уведомления Gitea.
- Не проведён полный пользовательский тест: сообщение → сохранение → экспорт → анализ в ChatGPT → возврат отчёта.
## Важные неизвестные
Перед реализацией постоянного развёртывания нужно получить или принять решения по следующим вопросам:
1. Адрес и параметры PostgreSQL для разработки и Synology.
2. Имя и URL удалённого репозитория Gitea.
3. Список пользователей Telegram, которым разрешён экспорт и работа с архивом.
4. Правила хранения и удаления сообщений и вложений.
5. Способ запуска бота: long polling на старте или webhook после развёртывания.
6. Нужен ли SQLite для локальной разработки как временное хранилище до подключения PostgreSQL.
## Следующий этап
### Stage 2 — бот-архиватор
Рекомендуемый порядок:
1. Создать каркас Python-приложения и конфигурацию через переменные окружения.
2. Добавить фильтр группы `-1004242041275` и разрешённых тем `2`, `4`, `6`, `8`.
3. Реализовать приём сообщений и сохранение метаданных.
4. Добавить минимальную схему хранилища.
5. Реализовать ручную отметку сообщений.
6. Реализовать Markdown-экспорт с контекстом, авторами, датами, reply-связями и вложениями.
7. Добавить команды справки и состояния.
8. Проверить работу на реальном обсуждении и зафиксировать результат в этом документе.
## История изменений статуса
### 2026-07-31
- Создана супергруппа и добавлен бот.
- Отключён Privacy Mode.
- Определены ID тем: `2`, `4`, `6`, `8`.
- Подтверждено отсутствие webhook.
- Зафиксировано, что код бота и хранилище ещё не реализованы.

27
docs/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Документация Акылдаш
## Продукт
- [Обзор проекта](product/project-overview.md) — назначение, основные области и
правила работы с данными.
## Решения
- [Решение 001](decisions/001-telegram-workspace-mvp.md) — границы MVP
Telegram-инфраструктуры.
## Эксплуатация и рабочий процесс
- [Текущий статус](operations/project-status.md) — выполненные шаги, риски и
ближайшие действия.
- [План Telegram-пространства](operations/telegram-workspace-plan.md) — темы,
роли и этапы развития командного окружения.
## Команда
- [Навыки работы с ИИ](team/ai-skills-for-beginners.md) — короткие практики для
начинающих.
---
Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан

View File

@@ -11,7 +11,7 @@ Telegram не является хранилищем проекта, а бот и
## Что входит в MVP
- Telegram-супергруппа с темами `Общее` (стандартная тема), `MVP`, `Решения`, `Обсуждение` и `Работа с ИИ`.
- Telegram-супергруппа с темами `Общее` (стандартная тема), `MVP`, `Решения`, `Обсуждение`, `Работа с ИИ` и `Отчеты`.
- Тема `MVP` используется только для обсуждения границ первой версии. После фиксации MVP тема закрывается.
- Тема `Обсуждение` используется для рабочих гипотез и вопросов, которые ещё не стали подтверждёнными решениями.
- В теме `Решения` бот публикует итоговые сводки и подтверждённые решения после завершения обсуждения. Сообщения бота являются единственным редактируемым источником итогового текста; пользователи не редактируют сообщения бота.
@@ -20,6 +20,7 @@ Telegram не является хранилищем проекта, а бот и
- Для сообщения сохраняются текст, автор, дата и время, тема, идентификатор сообщения и связь с ответом.
- Экспорт выполняется по ручной отметке участника или по явно заданному диапазону темы.
- Экспорт формируется в Markdown и включает контекст, сообщения по времени, ссылки и доступные вложения.
- Markdown публикуется в закрытой для сообщений участников теме `Отчеты`. В исходной теме бот оставляет заметную отсечку, прямую ссылку и общий с отчётом уникальный хэштег.
- Участник вручную загружает экспорт в ChatGPT и возвращает результат в Telegram.
- Gitea-уведомления на старте можно публиковать в `Общее` либо в отдельную тему после появления такой потребности. События ограничиваются PR, задачами, дедлайнами и сбоями важных проверок.
- Подтверждённые решения и изменения документации фиксируются в Git/Gitea обычным review-процессом.
@@ -46,15 +47,16 @@ Telegram не является хранилищем проекта, а бот и
- Используются темы `MVP`, `Решения`, `Обсуждение` и `Работа с ИИ`; тема `Общее` остаётся стандартной.
- `MVP` закрывается после фиксации состава первой версии.
- Итог обсуждения формирует бот и публикует в `Решения`.
- Файлы экспортов публикуются в теме `Отчеты`; исходные темы не засоряются файлами.
- `Работа с ИИ` предназначена только для публикаций бота и будет содержать best practice, а не обычный FAQ.
- Остальные параметры первой версии из этого документа считаются утверждёнными, если команда не изменит их отдельным решением.
## Граница между ботом и ИИ
- Бот без обращения к API языковой модели полностью выполняет сбор сообщений, сортировку, добавление метаданных, подготовку вложений и формирование Markdown-экспорта.
- Бот добавляет к экспорту готовую инструкцию для ручной загрузки в обычный ChatGPT.
- На следующем этапе бот должен добавлять к экспорту готовую инструкцию для ручной загрузки в обычный ChatGPT.
- Выделение решений, задач, рисков, противоречий и открытых вопросов выполняется в ChatGPT вручную через подписку участника.
- Бот может принять возвращённый человеком отчёт, опубликовать его в Telegram и связать с исходным обсуждением.
- На следующем этапе бот должен принимать возвращённый человеком отчёт, публиковать его в Telegram и связывать с исходным обсуждением.
- Бот не выдаёт эвристический или шаблонный результат за выполненный ИИ-анализ. Поиск ключевых слов и ручные метки допустимы только как вспомогательная навигация.
## Техническое замечание о правах тем
@@ -63,13 +65,17 @@ Telegram позволяет запретить пользователям отп
## Инфраструктурные предпосылки
- Доступы для настройки находятся в `credentials.txt`; секреты из этого файла нельзя включать в Git, логи CI/CD, экспорт Telegram или сообщения бота.
- Доступы для настройки находятся в локальном `credentials.json`; секреты из этого файла нельзя включать в Git, логи CI/CD, экспорт Telegram или сообщения бота.
- Для проекта можно создать открытый репозиторий в Gitea.
- В Gitea необходимо настроить защиту `main`: прямые push запрещены, изменения проходят через pull request и проверки CI/CD.
- Развёртывание бота планируется на Docker-инфраструктуре Synology.
- Бот развёрнут в Container Manager на Synology с постоянным SQLite-томом и политикой автоперезапуска; конфигурацию развёртывания ещё предстоит зафиксировать в репозитории.
- PostgreSQL будет использоваться как хранилище бота; параметры доступа к базе будут предоставлены отдельно.
- Почта может использоваться как дополнительный канал доставки файлов и отчётов, но не как основное хранилище.
## Критерий завершения Stage 0
Границы MVP, состав тем и правила работы с данными подтверждены. Остаётся реализовать жизненный цикл тем и экспортов, после чего можно проектировать схему хранения и команды бота, не меняя назначение первой версии по ходу реализации.
Критерий выполнен: границы MVP зафиксированы, темы созданы, первая версия бота и экспортов реализована. Полный рабочий цикл будет принят командой после проверки на реальном обсуждении.
---
Акылдаш · Telegram-бот v0.2.2

View File

@@ -0,0 +1,165 @@
# Статус проекта
Последняя проверка: 2026-08-03
Назначение документа: быстро восстановить контекст проекта для участников команды и будущих агентов.
- Telegram-бот: `0.2.2`
- Telegram-бот на Synology: `0.2.1`
- Backend: не создан
- Frontend: не создан
## Краткий итог
Репозиторий переориентирован с отдельного бота на весь проект юридической информационно-аналитической платформы. Telegram-бот выделен в инструмент рабочего окружения: он сохраняет сообщения разрешённых тем в SQLite, экспортирует обсуждения в Markdown и постоянно запущен в Container Manager на Synology. Версия `0.2.2` будет развёрнута после слияния изменений; сейчас на Synology работает `0.2.1`.
Ближайшая цель — 2026-08-04 зафиксировать MVP с командой, настроить резервное копирование SQLite и затем проверить полный рабочий цикл на реальном обсуждении.
## Уже сделано
### Telegram
- Создана супергруппа `Акылдаш`.
- Создан бот `help_clerk_bot`.
- Бот добавлен в супергруппу с необходимыми правами.
- Privacy Mode бота отключён.
- Созданы и проверены рабочие темы.
- Проверено подключение к Telegram Bot API.
- Webhook у бота не установлен; доступен режим long polling через `getUpdates`.
### ID тем
| Тема | `message_thread_id` | Состояние |
|---|---:|---|
| Общее | отсутствует | приветствие закреплено, тема закрыта |
| MVP | `2` | подтверждено тестовым сообщением |
| Решения | `4` | подтверждено тестовым сообщением |
| Обсуждение | `6` | подтверждено тестовым сообщением |
| Работа с ИИ | `8` | материалы опубликованы, тема закрыта |
| Отчеты | `37` | экспорт проверен, тема закрыта |
### Документация и правила
- Подготовлен общий план реализации в `docs/operations/telegram-workspace-plan.md`.
- Зафиксированы границы MVP и правила работы с данными в `docs/decisions/001-telegram-workspace-mvp.md`.
- Добавлены обзор всей платформы и единый индекс документации.
- В MVP не входит автоматический вызов API языковой модели.
- Анализ экспортов выполняется вручную в обычном ChatGPT.
- Telegram используется как рабочий штаб, Git/Gitea — как источник утверждённых материалов.
- Подготовлены и опубликованы краткие навыки работы с ИИ для начинающих.
- В `Общее` опубликовано и закреплено приветствие с назначением тем и описанием бота.
### Доступы
- Локальные доступы находятся в `credentials.json`.
- `credentials.json` добавлен в `.gitignore`.
- Секреты нельзя добавлять в Git, логи, экспорты или сообщения бота.
### Репозиторий и приложение
- Реализован бот-секретарь версии `0.2.2` без внешних Python-зависимостей.
- Код бота выделен из корня репозитория в `tools/telegram-bot`.
- Сообщения и полные Telegram-метаданные сохраняются в SQLite.
- Добавлены команды `/help`, `/status` и `/export`.
- Экспорт доступен только владельцу, указанному в `TELEGRAM_OWNER_ID`.
- Первый `/export` охватывает всю сохранённую тему, последующие начинаются после последней успешно созданной отсечки.
- Отчёты публикуются в закрытой теме `Отчеты` (`message_thread_id=37`).
- Исходное обсуждение завершается заметной отсечкой со ссылкой и хэштегом отчёта.
- Локальный Git-репозиторий восстановлен и привязан к Gitea.
- Репозиторий организован как основа всего проекта, а не отдельного бота.
- Бот развёрнут в Container Manager на Synology; автозапуск после перезапуска менеджера проверен.
### Развёртывание
- Бот постоянно запущен на Synology в контейнере `akyldash-bot`.
- Используется официальный образ `python:3.11-slim` и long polling.
- Контейнер работает без root, с read-only root filesystem и политикой `unless-stopped`.
- Доступ к Telegram Bot API идёт через отдельный закрытый прокси-контейнер без опубликованных наружу портов.
- Код, закрытый env-файл и SQLite хранятся в `/volume1/docker/akyldash`.
## Пока не сделано
### Развёртывание и сопровождение
- Docker-конфигурация развёртывания не хранится в репозитории.
- Не настроен CI/CD.
### Хранилище
- PostgreSQL не подключён; первая версия использует SQLite.
- Не утверждена окончательная схема PostgreSQL.
- Не определены сроки хранения сообщений, вложений и экспортов.
### Функции бота
- Скачивание файлов вложений; сейчас сохраняются их Telegram-метаданные.
- Ручная пометка отдельных сообщений для экспорта.
- Формирование готовой инструкции для ручной загрузки в ChatGPT.
- Возврат отчёта пользователя в Telegram.
### Группа и рабочий процесс
- Не проверены сценарии закрытия темы `MVP` после фиксации состава первой версии.
- Не настроены уведомления Gitea.
- Не проведён полный пользовательский тест: сообщение → сохранение → экспорт → анализ в ChatGPT → возврат отчёта.
## Важные неизвестные
По мере реального использования нужно принять решения по следующим вопросам:
1. Адрес и параметры PostgreSQL для разработки и Synology.
2. Правила хранения и удаления сообщений и вложений.
3. Нужен ли webhook после постоянного развёртывания; первая версия работает через long polling.
## Следующий этап
### Фиксация MVP и проверка полного цикла
Рекомендуемый порядок:
1. Настроить резервное копирование `/volume1/docker/akyldash/data`.
2. Зафиксировать состав MVP с командой и закрыть тему `MVP`.
3. Провести одно реальное обсуждение с ответами, правками и вложением.
4. Экспортировать обсуждение и вручную проанализировать Markdown в ChatGPT.
5. Вернуть подтверждённый итог в тему `Решения` и зафиксировать его в Git/Gitea.
6. Записать обнаруженные неудобства и только после этого выбирать следующую доработку.
## История изменений статуса
### 2026-08-03
- Репозиторий перестроен под весь проект юридической платформы; Telegram-бот перенесён в `tools/telegram-bot`.
- Созданы обзор продукта и структурированный индекс документации; будущие каталоги решено создавать по фактическим задачам.
- Закрытые юридические материалы решено хранить отдельно от основного репозитория.
- Версия Telegram-бота обновлена до `0.2.2` из-за изменения структуры запуска.
- Экспорт без параметров переведён с периода в семь дней на диапазон после предыдущей отсечки.
- Бот развёрнут в Container Manager на Synology; автозапуск проверен перезапуском.
- В теме `Работа с ИИ` опубликована первоначальная библиотека практик и отдельный материал о безопасной работе с Codex.
- В `Общее` опубликовано и закреплено приветствие; тема закрыта для сообщений.
- Локальный файл доступов переведён из `credentials.txt` в `credentials.json`.
### 2026-08-02
- Экспорт ограничен Telegram ID владельца бота.
- Технические ответы на корневое сообщение темы исключены из Markdown.
- Экспорт перенесён в тему `Отчеты`, добавлены отсечки и навигационные хэштеги.
- Проверена живая публикация отчёта и отсечки через Telegram Bot API.
- Бот версии `0.2.0` развёрнут в Container Manager на Synology.
- Версия приложения обновлена до `0.2.0`.
### 2026-08-01
- Восстановлена связь локального каталога с репозиторием Gitea.
- Реализован бот-секретарь версии `0.1.0` с SQLite и Markdown-экспортом.
- Проверены токен `help_clerk_bot` и доступ к супергруппе `Акылдаш`.
### 2026-07-31
- Создана супергруппа и добавлен бот.
- Отключён Privacy Mode.
- Определены ID тем: `2`, `4`, `6`, `8`.
- Подтверждено отсутствие webhook.
- Зафиксировано, что код бота и хранилище ещё не реализованы.
---
Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан

View File

@@ -276,57 +276,59 @@ docs/
## 9. Этапы реализации
Обозначения: `[x]` — выполнено, `[ ]` — ещё не выполнено.
### Этап 0. Границы и правила
- утвердить назначение Telegram, Git, бота и ChatGPT;
- выбрать темы, которые бот слушает;
- определить, что можно собирать автоматически;
- определить правила обработки персональных и конфиденциальных данных;
- определить формат решений, задач и экспортов.
- [x] утвердить назначение Telegram, Git, бота и ChatGPT;
- [x] выбрать темы, которые бот слушает;
- [x] определить, что можно собирать автоматически;
- [x] определить правила обработки персональных и конфиденциальных данных;
- [x] определить формат решений, задач и экспортов.
### Этап 1. Рабочая Telegram-группа
- создать супергруппу и темы;
- подготовить закреплённые сообщения;
- создать первоначальный FAQ;
- согласовать правила обсуждений и фиксации решений.
- [x] создать супергруппу и темы;
- [x] подготовить и закрепить приветствие с навигацией;
- [x] создать первоначальную библиотеку практик работы с ИИ;
- [x] согласовать правила обсуждений и фиксации решений.
### Этап 2. Бот-архиватор
- реализовать приём сообщений;
- сохранить метаданные и вложения;
- добавить ручную отметку для экспорта;
- сформировать Markdown-экспорт;
- проверить работу с темами и ответами.
- [x] реализовать приём сообщений;
- [x] сохранять сообщения и метаданные вложений;
- [ ] добавить ручную отметку отдельных сообщений для экспорта;
- [x] сформировать Markdown-экспорт;
- [x] проверить работу с темами и ответами.
### Этап 3. Уведомления
- подключить события Gitea;
- настроить фильтрацию;
- направлять уведомления в отдельную тему;
- добавить напоминания о задачах и сроках.
- [ ] подключить события Gitea;
- [ ] настроить фильтрацию;
- [ ] направлять уведомления в отдельную тему;
- [ ] добавить напоминания о задачах и сроках.
### Этап 4. ИИ-подготовка без API
- добавить шаблоны запросов для анализа;
- создавать готовые файлы для загрузки в ChatGPT;
- добавить возврат отчёта в Telegram;
- проверить несколько реальных обсуждений.
- [ ] добавить шаблоны запросов для анализа;
- [x] создавать готовые файлы для загрузки в ChatGPT;
- [ ] добавить возврат отчёта в Telegram;
- [ ] проверить несколько реальных обсуждений.
### Этап 5. Связка с Git/Gitea
- подготовить структуру документации;
- настроить ссылки на задачи, PR и документы;
- определить процесс переноса подтверждённых решений в Git;
- добавить сценарии обновления документации через ИИ.
- [x] подготовить структуру документации;
- [ ] настроить ссылки на задачи, PR и документы;
- [x] определить процесс переноса подтверждённых решений в Git;
- [ ] добавить сценарии обновления документации через ИИ.
### Этап 6. Улучшение по фактическому использованию
- удалить невостребованные функции;
- расширить FAQ реальными сценариями команды;
- улучшить формат отчётов;
- добавить новые уведомления только при наличии потребности;
- оценить необходимость API и автоматического ИИ-анализа.
- [ ] удалить невостребованные функции;
- [ ] расширить библиотеку практик реальными сценариями команды;
- [ ] улучшить формат отчётов;
- [ ] добавить новые уведомления только при наличии потребности;
- [ ] оценить необходимость API и автоматического ИИ-анализа.
---
@@ -393,3 +395,7 @@ Git сохраняет актуальную версию
```
Цель инфраструктуры — не заставить специалистов изучать больше технологий, а сделать технологии удобным продолжением их профессиональной работы.
---
Акылдаш · Telegram-бот v0.2.2

View File

@@ -0,0 +1,47 @@
# Обзор проекта
## Назначение
Акылдаш — юридическая информационно-аналитическая платформа. Она должна помочь
пользователю находить применимые нормы и материалы, понимать их актуальность и
получать ответ со ссылками на проверяемые первоисточники. Платформа не заменяет
профессиональное юридическое заключение.
## Основные области
1. Получение открытых или лицензированных законов, актов и иных правовых
источников.
2. Очистка, нормализация, связывание редакций и сохранение происхождения данных.
3. Хранение структурированных документов и поисковых индексов.
4. Формирование базы знаний и RAG с обязательными ссылками на источники.
5. Backend для доступа к данным и функциям платформы.
6. Frontend для поиска, анализа и работы с результатами.
7. Инструменты рабочего окружения команды, включая Telegram-бота.
Физическая структура каждой области появится вместе с её первой задачей. До
этого список служит картой продукта, а не обещанием заранее выбранной
архитектуры.
## Обязательные принципы
- Источник, дата получения, редакция и лицензия документа должны быть
прослеживаемыми.
- Ответ системы должен отделять найденный факт от вывода модели и ссылаться на
конкретный первоисточник.
- Актуальность правовых данных должна проверяться до использования в ответе.
- Существенные юридические выводы проверяет человек.
- Секреты и персональные данные не попадают в Git, логи и тестовые наборы.
- Данные загружаются только при наличии законного основания и с соблюдением
условий источника.
## Правовая защита проекта
Договоры, заявки, материалы об интеллектуальной собственности и другие
закрытые юридические документы следует хранить отдельно от исходного кода: в
закрытом репозитории или защищённом документном хранилище. Доступ выдаётся
поимённо и только тем, кому он нужен. В основном репозитории можно хранить
несекретный реестр документов, правила доступа и ссылки на место хранения.
---
Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан

View File

@@ -0,0 +1,183 @@
# Важные навыки работы с ИИ для начинающих
Каждый раздел ниже — отдельное готовое сообщение для темы `Работа с ИИ`.
Текст можно публиковать без заголовков этого файла: название, хэштеги и пояснение уже входят в сообщение.
## 1. Ставьте задачу через результат
#работа_с_ии #промпт #цель
Пишите не тему, а желаемый результат. Вместо «расскажи о договоре» — «составь краткий список рисков договора для руководителя». Чем яснее результат, тем меньше случайных ответов.
## 2. Давайте нужный контекст
#работа_с_ии #контекст #источники
Укажите, для кого готовится результат, что уже известно и какие файлы или сообщения использовать. Не заставляйте ИИ угадывать факты, которые можно дать сразу.
## 3. Называйте ограничения
#работа_с_ии #ограничения #точность
Сразу укажите важные рамки: объём, срок, язык, допустимые источники и что нельзя менять. Пример: «до одной страницы, только по приложенным документам, без вымышленных данных».
## 4. Задавайте формат ответа
#работа_с_ии #формат #результат
Скажите, в каком виде нужен ответ: список, таблица, письмо, план или Markdown. Укажите желаемую длину и аудиторию. Это полезнее просьбы «ответь красиво».
## 5. Определяйте, что значит «готово»
#работа_с_ии #критерии #готовность
Добавьте проверяемый критерий завершения: «все риски связаны с пунктами договора», «у каждой задачи есть ответственный и срок», «тесты проходят». ИИ должен понимать финиш задачи.
## 6. Показывайте пример хорошего результата
#работа_с_ии #пример #качество
Если важны стиль или структура, приложите короткий образец. Один хороший пример обычно работает лучше длинного описания вроде «сделай профессионально».
## 7. Просите уточнить неясное
#работа_с_ии #уточнение #вопросы
Если задача допускает разные трактовки, напишите: «Сначала задай до трёх важных вопросов, затем приступай». Это снижает риск получить уверенный ответ не на тот вопрос.
## 8. Отделяйте факты от предположений
#работа_с_ии #факты #проверка
Просите отдельно отмечать подтверждённые факты, выводы и неизвестные данные. Для актуальной информации требуйте ссылки и дату проверки. Уверенный тон не является доказательством.
## 9. Для сложной задачи сначала просите план
#работа_с_ии #план #сложная_задача
Сначала попросите изучить материалы и предложить короткий план. После вашего подтверждения ИИ выполняет работу. Так ошибки направления обнаруживаются до больших изменений.
## 10. Делите большую работу на этапы
#работа_с_ии #этапы #контроль
Разбивайте задачу на небольшие проверяемые результаты: анализ → проект → проверка → финал. Подтверждайте важные этапы отдельно, особенно перед публикацией или изменением данных.
## 11. Используйте персональные инструкции
#работа_с_ии #персональные_инструкции #настройка
Персональные инструкции — ваши постоянные предпочтения для всех чатов: язык, стиль, желаемая краткость и способ объяснения. Не храните там пароли и правила одного конкретного проекта.
## 12. Настраивайте инструкции проекта
#работа_с_ии #инструкции_проекта #проект
Для каждого проекта отдельно зафиксируйте цель, термины, источники истины, правила работы и критерии готовности. В ChatGPT используйте инструкции проекта, в Codex — репозиторный `AGENTS.md`.
## 13. Делайте инструкции короткими и проверяемыми
#работа_с_ии #инструкции #порядок
Пишите конкретно: «перед изменением создай ветку», а не «работай правильно». Удаляйте устаревшие и противоречивые правила. Добавляйте новое правило после повторяющейся ошибки, а не заранее.
## 14. Один чат — один понятный результат
#работа_с_ии #чат #контекст
В одном чате держите одну связанную цель. Для отдельной задачи начинайте новый чат; для настоящего ответвления создавайте копию или fork. Перегруженный контекст ухудшает качество.
## 15. Храните решения вне переписки
#работа_с_ии #решения #источник_истины
Чат помогает думать, но не должен быть единственным хранилищем решения. Подтверждённый результат переносите в документ, задачу или Git, где видны актуальная версия и история изменений.
## 16. Понимайте назначение агентов
#работа_с_ии #агенты #делегирование
Агенты — отдельные исполнители для ограниченных частей большой задачи. Они полезны, когда несколько независимых исследований или проверок можно выполнить параллельно.
## 17. Давайте агенту одну ограниченную задачу
#работа_с_ии #агенты #постановкаадачи
Каждому агенту задайте одну цель, входные материалы, границы и формат результата. Хорошо: «проверь только риски безопасности и верни пять находок со ссылками».
## 18. Не используйте агентов без необходимости
#работа_с_ии #агенты #простота
Для маленькой или строго последовательной задачи один исполнитель лучше. Несколько агентов тратят больше ресурсов, могут дублировать работу и конфликтовать при одновременном изменении одних файлов.
## 19. Оставляйте итог главному исполнителю
#работа_с_ии #агенты #итог
Агенты возвращают краткие выводы, а главный исполнитель сравнивает их, устраняет противоречия и готовит единый ответ. Не склеивайте сырые результаты без общей проверки.
## 20. Заранее задавайте правило остановки
#работа_с_ии #зацикливание #стоп
Для сложной задачи напишите: «После двух неудачных попыток остановись, перечисли проверенное, назови блокер и предложи другой подход». Это не даёт ИИ бесконечно повторять одно решение.
## 21. После неудачи меняйте метод, а не формулировку
#работа_с_ии #ошибка #диагностика
Если подход не сработал, попросите назвать причину и собрать новые доказательства. Повтор той же команды другими словами редко помогает. Нужна новая гипотеза или дополнительный источник данных.
## 22. Очищайте перегруженный контекст
#работа_с_ии #контекст #перезапуск
Если ИИ путает старые решения, попросите кратко зафиксировать цель, факты, принятые решения и открытый вопрос. Затем продолжите из этой сводки в новом чате.
## 23. Всегда проверяйте результат
#работа_с_ии #проверка #качество
Просите ИИ выполнить самопроверку, но важные утверждения проверяйте сами по первичным источникам. Для кода нужны тесты, для расчётов — пересчёт, для документов — сверка цитат и реквизитов.
## 24. Не передавайте лишние данные
#работа_с_ии #приватность #персональныеанные
Перед загрузкой удалите лишние ФИО, контакты, пароли, ключи и конфиденциальные сведения. Передавайте минимальный объём данных, необходимый для задачи, и учитывайте правила вашей организации.
## 25. Отделяйте анализ от действия
#работа_с_ии #безопасность #подтверждение
Просьба «проанализируй» не должна означать «отправь, удали или опубликуй». Для внешних и необратимых действий требуйте отдельный план, предварительный просмотр и явное подтверждение человека.
## 26. Подключайте только нужные инструменты
#работа_с_ии #инструменты #доступ
Давайте ИИ доступ только к тем файлам, сервисам и правам, которые нужны сейчас. Начните с чтения; разрешение на запись, отправку и удаление выдавайте отдельно.
## 27. Превращайте повторяемую работу в шаблон
#работа_с_ии #шаблон #автоматизация
Если удачный запрос используется регулярно, сохраните его как шаблон. Когда процесс стал стабильным и проверенным, оформите его как навык или автоматизацию. Не автоматизируйте ещё не понятный процесс.
## 28. Завершайте работу короткой приёмкой
#работа_с_ии #приемка #финальная_проверка
В конце спросите: «Что сделано, что проверено, что осталось и какие есть риски?» Сравните ответ с первоначальной целью и критериями готовности, прежде чем принимать результат.
## Справочные материалы
- [Prompting](https://learn.chatgpt.com/docs/prompting)
- [Personalize ChatGPT](https://learn.chatgpt.com/docs/personalize)
- [Subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents)
- [Custom instructions with AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md)
---
Акылдаш · Telegram-бот v0.2.2

View File

@@ -0,0 +1,8 @@
TELEGRAM_BOT_TOKEN=replace-me
TELEGRAM_CHAT_ID=-1004242041275
TELEGRAM_OWNER_ID=87262245
TELEGRAM_REPORT_THREAD_ID=37
TELEGRAM_ALLOWED_THREAD_IDS=0,2,4,6,8
BOT_DATABASE=data/secretary.sqlite3
APP_TIMEZONE=Asia/Bishkek
LOG_LEVEL=INFO

View File

@@ -0,0 +1,51 @@
# Telegram-бот Акылдаш
Версия: `0.2.2`
Бот сохраняет сообщения разрешённых тем Telegram в локальную SQLite-базу и
выгружает обсуждения в Markdown. Внешние Python-зависимости не требуются.
## Запуск
Требуется Python 3.11 или новее.
```bash
cd tools/telegram-bot
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='-1004242041275'
export TELEGRAM_OWNER_ID='87262245'
export TELEGRAM_REPORT_THREAD_ID='37'
export TELEGRAM_ALLOWED_THREAD_IDS='0,2,4,6,8'
python3 bot.py
```
Доступные команды: `/help`, `/status`, `/export`. Экспорт доступен только
пользователю с Telegram ID из `TELEGRAM_OWNER_ID`. Первый `/export` выгружает
всю сохранённую тему, последующие — сообщения после предыдущей успешно
созданной отсечки.
Markdown публикуется в теме `TELEGRAM_REPORT_THREAD_ID`, а в исходной теме
остаётся отсечка со ссылкой и общим хэштегом отчёта.
База по умолчанию хранится в `data/secretary.sqlite3`. Сообщения из других
групп и тем не сохраняются. Полный ответ Telegram сохраняется в базе, поэтому
метаданные вложений остаются доступными для последующего скачивания.
## Проверка
```bash
python3 -m unittest -v
```
## Synology
Контейнер `akyldash-bot` работает на образе `python:3.11-slim` через закрытый
прокси-контейнер, с политикой перезапуска `unless-stopped`. Постоянные данные
находятся в `/volume1/docker/akyldash/data`.
При обновлении развёртывания файл `tools/telegram-bot/bot.py` копируется в
каталог контейнера как `bot.py`.
---
Акылдаш · Telegram-бот v0.2.2

458
tools/telegram-bot/bot.py Executable file
View File

@@ -0,0 +1,458 @@
#!/usr/bin/env python3
"""Minimal Telegram secretary: archive allowed topics and export them to Markdown."""
from __future__ import annotations
import json
import logging
import os
import sqlite3
import time
import urllib.error
import urllib.request
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
APP_NAME = "Акылдаш"
APP_VERSION = "0.2.2"
FOOTER = f"{APP_NAME} v{APP_VERSION}"
@dataclass(frozen=True)
class Config:
token: str
chat_id: int
owner_id: int
report_thread_id: int
thread_ids: frozenset[int]
database: Path
timezone: ZoneInfo
def load_config() -> Config:
token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
chat_id = os.environ.get("TELEGRAM_CHAT_ID", "").strip()
owner_id = os.environ.get("TELEGRAM_OWNER_ID", "").strip()
report_thread_id = os.environ.get("TELEGRAM_REPORT_THREAD_ID", "").strip()
if not token or not chat_id or not owner_id or not report_thread_id:
raise SystemExit(
"Set TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_OWNER_ID "
"and TELEGRAM_REPORT_THREAD_ID"
)
try:
threads = frozenset(
int(value.strip())
for value in os.environ.get(
"TELEGRAM_ALLOWED_THREAD_IDS", "0,2,4,6,8"
).split(",")
if value.strip()
)
return Config(
token=token,
chat_id=int(chat_id),
owner_id=int(owner_id),
report_thread_id=int(report_thread_id),
thread_ids=threads,
database=Path(os.environ.get("BOT_DATABASE", "data/secretary.sqlite3")),
timezone=ZoneInfo(os.environ.get("APP_TIMEZONE", "Asia/Bishkek")),
)
except (ValueError, ZoneInfoNotFoundError) as error:
raise SystemExit(f"Invalid configuration: {error}") from error
def connect(database: Path) -> sqlite3.Connection:
database.parent.mkdir(parents=True, exist_ok=True)
connection = sqlite3.connect(database)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA journal_mode=WAL")
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS messages (
chat_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
thread_id INTEGER NOT NULL,
sent_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
update_id INTEGER NOT NULL,
payload TEXT NOT NULL,
PRIMARY KEY (chat_id, message_id)
);
CREATE INDEX IF NOT EXISTS messages_thread_date
ON messages (chat_id, thread_id, sent_at);
CREATE TABLE IF NOT EXISTS state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"""
)
return connection
def archive_update(
connection: sqlite3.Connection, update: dict, config: Config
) -> dict | None:
update_id = int(update["update_id"])
message = update.get("message") or update.get("edited_message")
with connection:
if message:
chat_id = int(message["chat"]["id"])
thread_id = int(message.get("message_thread_id", 0))
if chat_id == config.chat_id and thread_id in config.thread_ids:
connection.execute(
"""
INSERT INTO messages (
chat_id, message_id, thread_id, sent_at, updated_at,
update_id, payload
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (chat_id, message_id) DO UPDATE SET
thread_id = excluded.thread_id,
updated_at = excluded.updated_at,
update_id = excluded.update_id,
payload = excluded.payload
""",
(
chat_id,
int(message["message_id"]),
thread_id,
int(message["date"]),
int(message.get("edit_date", message["date"])),
update_id,
json.dumps(message, ensure_ascii=False),
),
)
else:
message = None
connection.execute(
"""
INSERT INTO state (key, value) VALUES ('next_update_id', ?)
ON CONFLICT (key) DO UPDATE SET value = excluded.value
""",
(str(update_id + 1),),
)
return message
def next_update_id(connection: sqlite3.Connection) -> int:
row = connection.execute(
"SELECT value FROM state WHERE key = 'next_update_id'"
).fetchone()
return int(row["value"]) if row else 0
def export_checkpoint(
connection: sqlite3.Connection, chat_id: int, thread_id: int
) -> int:
row = connection.execute(
"SELECT value FROM state WHERE key = ?",
(f"export_checkpoint:{chat_id}:{thread_id}",),
).fetchone()
return int(row["value"]) if row else 0
def save_export_checkpoint(
connection: sqlite3.Connection, chat_id: int, thread_id: int, message_id: int
) -> None:
with connection:
connection.execute(
"""
INSERT INTO state (key, value) VALUES (?, ?)
ON CONFLICT (key) DO UPDATE SET value = excluded.value
""",
(f"export_checkpoint:{chat_id}:{thread_id}", str(message_id)),
)
def telegram_request(
config: Config, method: str, payload: dict, timeout: int = 65
) -> object:
request = urllib.request.Request(
f"https://api.telegram.org/bot{config.token}/{method}",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
result = json.load(response)
if not result.get("ok"):
raise RuntimeError(result.get("description", "Telegram API error"))
return result["result"]
def send_text(config: Config, message: dict, text: str) -> None:
payload = {
"chat_id": config.chat_id,
"text": f"{text}\n\n{FOOTER}",
"reply_parameters": {"message_id": message["message_id"]},
}
if message.get("message_thread_id"):
payload["message_thread_id"] = message["message_thread_id"]
telegram_request(config, "sendMessage", payload)
def send_document(
config: Config, thread_id: int, filename: str, content: bytes, caption: str
) -> dict:
boundary = uuid.uuid4().hex
fields = {
"chat_id": str(config.chat_id),
"message_thread_id": str(thread_id),
"caption": caption,
}
body = bytearray()
for name, value in fields.items():
body.extend(
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\""
f"\r\n\r\n{value}\r\n".encode()
)
body.extend(
f"--{boundary}\r\nContent-Disposition: form-data; name=\"document\"; "
f"filename=\"{filename}\"\r\nContent-Type: text/markdown; charset=utf-8"
f"\r\n\r\n".encode()
)
body.extend(content)
body.extend(f"\r\n--{boundary}--\r\n".encode())
request = urllib.request.Request(
f"https://api.telegram.org/bot{config.token}/sendDocument",
data=bytes(body),
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
)
with urllib.request.urlopen(request, timeout=65) as response:
result = json.load(response)
if not result.get("ok"):
raise RuntimeError(result.get("description", "Telegram API error"))
return result["result"]
def author(message: dict) -> str:
sender = message.get("from", {})
name = " ".join(
part for part in (sender.get("first_name"), sender.get("last_name")) if part
)
username = sender.get("username")
if name and username:
return f"{name} (@{username})"
return name or (f"@{username}" if username else "Неизвестный участник")
def attachment_descriptions(message: dict) -> list[str]:
descriptions: list[str] = []
if message.get("photo"):
descriptions.append("фотография")
for field, label in (
("document", "документ"),
("video", "видео"),
("audio", "аудио"),
("voice", "голосовое сообщение"),
("animation", "анимация"),
("sticker", "стикер"),
):
attachment = message.get(field)
if attachment:
name = attachment.get("file_name") or attachment.get("emoji")
descriptions.append(f"{label}: {name}" if name else label)
return descriptions
def message_link(chat_id: int, message_id: int) -> str:
return f"https://t.me/c/{str(chat_id).removeprefix('-100')}/{message_id}"
def export_markdown(
connection: sqlite3.Connection,
config: Config,
thread_id: int,
days: int | None,
now: int | None = None,
after_message_id: int | None = None,
) -> str:
parameters: list[int] = [config.chat_id, thread_id]
condition = ""
if after_message_id is not None:
condition = " AND message_id > ?"
parameters.append(after_message_id)
elif days is not None:
condition = " AND sent_at >= ?"
parameters.append((now or int(time.time())) - days * 86400)
rows = connection.execute(
f"""
SELECT message_id, sent_at, payload FROM messages
WHERE chat_id = ? AND thread_id = ?{condition}
ORDER BY sent_at, message_id
""",
parameters,
).fetchall()
if after_message_id is not None:
period = (
"с начала архива"
if after_message_id == 0
else "после предыдущей отсечки"
)
else:
period = "за всё время" if days is None else f"за последние {days} дн."
lines = [
"# Обсуждение",
"",
f"Тема: `{thread_id or 'Общее'}` ",
f"Период: {period} ",
f"Экспортировано: {datetime.now(config.timezone):%Y-%m-%d %H:%M %Z}",
"",
"## Сообщения",
]
for row in rows:
message = json.loads(row["payload"])
text = message.get("text") or message.get("caption") or "[Служебное событие Telegram]"
if text.split(maxsplit=1)[0].split("@", 1)[0] in {"/help", "/status", "/export"}:
continue
sent_at = datetime.fromtimestamp(row["sent_at"], timezone.utc).astimezone(
config.timezone
)
lines.extend(
[
"",
f"### {author(message)}{sent_at:%Y-%m-%d %H:%M}",
"",
f"[Открыть сообщение]({message_link(config.chat_id, row['message_id'])})",
]
)
reply = message.get("reply_to_message", {}).get("message_id")
if reply and reply != message.get("message_thread_id"):
lines.append(f"Ответ на сообщение: #{reply}")
lines.extend(["", text])
attachments = attachment_descriptions(message)
if attachments:
lines.extend(["", "Вложения:", *[f"- {item}" for item in attachments]])
lines.extend(["", "---", FOOTER, ""])
return "\n".join(lines)
def parse_export_days(parts: list[str]) -> int | None:
if len(parts) == 1:
return 7
if parts[1].lower() in {"all", "все"}:
return None
days = int(parts[1])
if not 1 <= days <= 3650:
raise ValueError
return days
def handle_command(
connection: sqlite3.Connection, config: Config, message: dict
) -> None:
text = message.get("text", "")
if not text.startswith("/"):
return
parts = text.split()
command = parts[0].split("@", 1)[0].lower()
thread_id = int(message.get("message_thread_id", 0))
if command == "/help":
send_text(
config,
message,
"Я сохраняю обсуждения этой группы.\n"
"/status — количество сохранённых сообщений\n"
"/export — экспорт текущей темы после предыдущей отсечки",
)
elif command == "/status":
topic_count = connection.execute(
"SELECT COUNT(*) FROM messages WHERE chat_id = ? AND thread_id = ?",
(config.chat_id, thread_id),
).fetchone()[0]
total_count = connection.execute(
"SELECT COUNT(*) FROM messages WHERE chat_id = ?", (config.chat_id,)
).fetchone()[0]
send_text(
config,
message,
f"Сохранено сообщений: {topic_count} в этой теме, {total_count} всего.",
)
elif command == "/export":
if int(message.get("from", {}).get("id", 0)) != config.owner_id:
send_text(config, message, "Экспорт доступен только владельцу бота.")
return
try:
days = parse_export_days(parts) if len(parts) > 1 else None
except (ValueError, IndexError):
send_text(config, message, "Использование: /export [13650|все]")
return
checkpoint = (
None
if len(parts) > 1
else export_checkpoint(connection, config.chat_id, thread_id)
)
markdown = export_markdown(
connection,
config,
thread_id,
days,
after_message_id=checkpoint,
)
stamp = datetime.now(config.timezone).strftime("%Y%m%d-%H%M")
report_tag = f"#report_{message['message_id']}"
report = send_document(
config,
config.report_thread_id,
f"discussion-{thread_id}-{stamp}.md",
markdown.encode(),
f"{report_tag}\nИсходное обсуждение: "
f"{message_link(config.chat_id, message['message_id'])}\n\n{FOOTER}",
)
send_text(
config,
message,
"━━━━━━━━━━━━━━━━\n"
"✅ ОБСУЖДЕНИЕ ЗАВЕРШЕНО\n"
f"{report_tag}\n"
f"Отчёт: {message_link(config.chat_id, report['message_id'])}\n"
"━━━━━━━━━━━━━━━━",
)
save_export_checkpoint(
connection, config.chat_id, thread_id, int(message["message_id"])
)
def run() -> None:
config = load_config()
connection = connect(config.database)
logging.info("Starting %s with database %s", FOOTER, config.database)
while True:
try:
updates = telegram_request(
config,
"getUpdates",
{
"offset": next_update_id(connection),
"timeout": 50,
"allowed_updates": ["message", "edited_message"],
},
)
for update in updates:
message = archive_update(connection, update, config)
if message:
handle_command(connection, config, message)
except (urllib.error.URLError, TimeoutError, RuntimeError, OSError, ValueError):
logging.exception("Polling failed; retrying in 5 seconds")
time.sleep(5)
if __name__ == "__main__":
logging.basicConfig(
level=os.environ.get("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(message)s",
)
try:
run()
except KeyboardInterrupt:
logging.info("Stopped")

View File

@@ -0,0 +1,238 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from zoneinfo import ZoneInfo
from bot import (
APP_VERSION,
Config,
archive_update,
connect,
export_markdown,
handle_command,
next_update_id,
)
class SecretaryTest(unittest.TestCase):
def test_archives_allowed_topic_and_exports_reply(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-1004242041275,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({2}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("Asia/Bishkek"),
)
connection = connect(config.database)
message = {
"message_id": 12,
"message_thread_id": 2,
"date": 1_700_000_000,
"chat": {"id": config.chat_id},
"from": {"id": 7, "first_name": "Айжан"},
"text": "Зафиксируем это решение.",
"reply_to_message": {"message_id": 11},
}
archived = archive_update(
connection, {"update_id": 40, "message": message}, config
)
markdown = export_markdown(
connection, config, thread_id=2, days=None, now=1_700_000_001
)
self.assertEqual(archived, message)
self.assertEqual(next_update_id(connection), 41)
self.assertIn("Айжан", markdown)
self.assertIn("Зафиксируем это решение.", markdown)
self.assertIn("Ответ на сообщение: #11", markdown)
self.assertIn(f"Акылдаш v{APP_VERSION}", markdown)
def test_ignores_other_chat_but_advances_offset(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-1,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({0}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("UTC"),
)
connection = connect(config.database)
update = {
"update_id": 5,
"message": {
"message_id": 1,
"date": 1,
"chat": {"id": -2},
"text": "Не наша группа",
},
}
self.assertIsNone(archive_update(connection, update, config))
self.assertEqual(next_update_id(connection), 6)
self.assertEqual(connection.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0)
def test_omits_forum_topic_root_reply_from_export(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-1,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({2}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("UTC"),
)
connection = connect(config.database)
message = {
"message_id": 12,
"message_thread_id": 2,
"date": 1,
"chat": {"id": config.chat_id},
"from": {"id": config.owner_id},
"text": "Сообщение темы",
"reply_to_message": {"message_id": 2},
}
archive_update(connection, {"update_id": 1, "message": message}, config)
markdown = export_markdown(connection, config, 2, None)
self.assertNotIn("Ответ на сообщение: #2", markdown)
def test_rejects_export_from_non_owner(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-1,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({0}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("UTC"),
)
connection = connect(config.database)
message = {
"message_id": 1,
"date": 1,
"chat": {"id": config.chat_id},
"from": {"id": 8},
"text": "/export все",
}
with patch("bot.send_text") as send_text, patch(
"bot.send_document"
) as send_document:
handle_command(connection, config, message)
send_text.assert_called_once_with(
config, message, "Экспорт доступен только владельцу бота."
)
send_document.assert_not_called()
def test_sends_report_to_reports_topic_and_marks_discussion(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-100123,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({2}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("UTC"),
)
connection = connect(config.database)
message = {
"message_id": 12,
"message_thread_id": 2,
"date": 1,
"chat": {"id": config.chat_id},
"from": {"id": config.owner_id},
"text": "/export",
}
with patch("bot.send_text") as send_text, patch(
"bot.send_document", return_value={"message_id": 99}
) as send_document:
handle_command(connection, config, message)
document = send_document.call_args.args
self.assertEqual(document[1], config.report_thread_id)
self.assertIn("#report_12", document[4])
self.assertIn("https://t.me/c/123/12", document[4])
marker = send_text.call_args.args[2]
self.assertIn("ОБСУЖДЕНИЕ ЗАВЕРШЕНО", marker)
self.assertIn("#report_12", marker)
self.assertIn("https://t.me/c/123/99", marker)
def test_next_export_starts_after_previous_cutoff(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(
token="test",
chat_id=-100123,
owner_id=7,
report_thread_id=37,
thread_ids=frozenset({2}),
database=Path(directory) / "bot.sqlite3",
timezone=ZoneInfo("UTC"),
)
connection = connect(config.database)
def archive(message_id, text):
archive_update(
connection,
{
"update_id": message_id,
"message": {
"message_id": message_id,
"message_thread_id": 2,
"date": message_id,
"chat": {"id": config.chat_id},
"from": {"id": config.owner_id},
"text": text,
},
},
config,
)
archive(10, "Первое обсуждение")
with patch("bot.send_text"), patch(
"bot.send_document",
side_effect=[{"message_id": 90}, {"message_id": 91}],
) as send_document:
handle_command(
connection,
config,
{
"message_id": 12,
"message_thread_id": 2,
"from": {"id": config.owner_id},
"text": "/export",
},
)
archive(13, "Второе обсуждение")
handle_command(
connection,
config,
{
"message_id": 14,
"message_thread_id": 2,
"from": {"id": config.owner_id},
"text": "/export",
},
)
first_export = send_document.call_args_list[0].args[3].decode()
second_export = send_document.call_args_list[1].args[3].decode()
self.assertIn("Первое обсуждение", first_export)
self.assertNotIn("Первое обсуждение", second_export)
self.assertIn("Второе обсуждение", second_export)
if __name__ == "__main__":
unittest.main()