Merge branch 'регитсрация' into main

This commit is contained in:
AidarKC
2026-07-22 20:11:30 +04:00
880 changed files with 4549 additions and 12650 deletions
+6 -3
View File
@@ -13,6 +13,7 @@ build/
.kotlin .kotlin
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea/
.idea/modules.xml .idea/modules.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/compiler.xml .idea/compiler.xml
@@ -102,10 +103,12 @@ ESP32/**/*.d
ESP32/**/*.a ESP32/**/*.a
# Полные серверные бэкапы (тяжёлые архивы, не коммитим) # Полные серверные бэкапы (тяжёлые архивы, не коммитим)
server-backup/archive/** deploy/backup/archive/**
!server-backup/archive/.gitkeep !deploy/backup/archive/.gitkeep
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим # Локальная дев-обвязка AI-агентов (сессии, планы, настройки) — не коммитим
.agents/
.codex/
.claude/ .claude/
# Рабочие бэкапы/превью-ассеты UI — не для репозитория # Рабочие бэкапы/превью-ассеты UI — не для репозитория
*.bak.png *.bak.png
-8
View File
@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
Generated
-1
View File
@@ -1 +0,0 @@
shine-server-server
-10
View File
@@ -1,10 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" build-on-make="true" name="server:jar">
<output-path>$PROJECT_DIR$/out/artifacts/server_jar</output-path>
<root id="archive" name="server.jar">
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/META-INF/MANIFEST.MF" />
</element>
</root>
</artifact>
</component>
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/shine-server-blockchain" />
<option value="$PROJECT_DIR$/shine-server-config" />
<option value="$PROJECT_DIR$/shine-server-crypto" />
<option value="$PROJECT_DIR$/shine-server-db" />
<option value="$PROJECT_DIR$/shine-server-geo" />
<option value="$PROJECT_DIR$/shine-server-log" />
<option value="$PROJECT_DIR$/shine-server-net-protocol" />
<option value="$PROJECT_DIR$/shine-server-net-server" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (2)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+33 -51
View File
@@ -14,14 +14,15 @@
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`: - Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
- точка входа `shine-UI/server-ui.html`; - точка входа `shine-UI/server-ui.html`;
- остальные файлы серверного UI — в `shine-UI/server-ui/`. - остальные файлы серверного UI — в `shine-UI/server-ui/`.
- Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения. - Локальный Telegram-бот агента-кодера живёт рядом с репозиторием продукта, обычно в `../SHiNE-agent-bot-coder/`, и не входит в публичный код основного приложения.
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя. - Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
## Сервис агента-кодера ## Сервис агента-кодера
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`. - Локальный Telegram-бот-сервис агента-кодера находится вне этого git-репозитория, обычно в `../SHiNE-agent-bot-coder/`.
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту. - Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`. - Рабочая папка Codex для сервиса должна указывать на этот продуктовый репозиторий: `CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-product`.
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`. - Автоматически читаемые инструкции для Codex внутри сервиса держать в `../SHiNE-agent-bot-coder/AGENTS.md`.
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `../SHiNE-agent-bot-coder/AGENT.md`.
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`. - Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
## ESP32 UI homeserver ## ESP32 UI homeserver
@@ -36,68 +37,62 @@
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения. - Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их. - В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги. - В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
- Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля с `deployServer`/`deployUI` основного проекта. - Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля со скриптами основного server/UI deploy из `deploy/scripts/`.
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в: - Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - `docs/Инициализация_Solana_регистрации/README.md`
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте. - Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в: - Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
- `Dev_Docs/Solana_Architecture/README.md` - `docs/Solana_Architecture/README.md`
- Документ формата пользовательской PDA-записи `shine_users` находится в: - Документ формата пользовательской PDA-записи `shine_users` находится в:
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` - `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
## Документация блокчейна ## Документация блокчейна
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`. - Актуальная документация по форматам блокчейна находится в `docs/Blockchain/README.md`.
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям. - Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `Dev_Docs/Blockchain/`. - При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `docs/Blockchain/`.
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение. - Дополнительно обязательно вести `docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён. - Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять). - Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`. - Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока. - Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
## Документация личных сообщений (DM) ## Документация личных сообщений (DM)
- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`. - Актуальная документация по логике личных сообщений находится в `docs/Personal_Messages/Протокол_DM_v1.md`.
- Точный байтовый формат DM находится в `Dev_Docs/Personal_Messages/Формат_DM_v1.md`. - Точный байтовый формат DM находится в `docs/Personal_Messages/Формат_DM_v1.md`.
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять оба документа: - При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять оба документа:
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md` - `docs/Personal_Messages/Протокол_DM_v1.md`
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md` - `docs/Personal_Messages/Формат_DM_v1.md`
- Логика личных сообщений в коде должна всегда соответствовать этим документам. - Логика личных сообщений в коде должна всегда соответствовать этим документам.
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии. - Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
## Документация API сервера ## Документация API сервера
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `Dev_Docs/API/`. - Актуальная документация по публичному JSON/WebSocket API сервера находится в `docs/API/`.
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `Dev_Docs/API/`. - При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `docs/API/`.
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение. - Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду. - Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
- Если добавляется новая операция `op`, нужно обновить общий список операций в `Dev_Docs/API/09_Operations_Index.md` или создать его, если файла ещё нет. - Если добавляется новая операция `op`, нужно обновить общий список операций в `docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
## Документация Figma ## Документация Figma
- Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `Dev_Docs/Figma/`. - Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `docs/Figma/`.
- Точка входа: `Dev_Docs/Figma/README.md`. - Точка входа: `docs/Figma/README.md`.
- Подробный рабочий регламент: `Dev_Docs/Figma/TRANSFER_UI_SCREENS.md`. - Подробный рабочий регламент: `docs/Figma/TRANSFER_UI_SCREENS.md`.
- Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ. - Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ.
## Версионирование ## Версионирование
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория). - Все правила по коммитам, merge в `main`, `git push` и обновлению `VERSION.properties` находятся в `COMMIT_AND_VERSION_RULES.md`.
- Перед каждым новым коммитом обязательно увеличивать версии в `VERSION.properties`: - Этот файл считать единым источником истины по правилам версионирования и коммитов для данного репозитория.
- `client.version` — версия клиентского UI.
- `server.version` — версия серверной части.
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
## Deploy ## Deploy
- Все документы и заметки по деплою хранить в папке `Deploy/`. - Все документы, инструкции, backup-правила и скрипты деплоя хранить в папке `deploy/`.
- Production-хост SHiNE: `player@shineup.me` (`178.208.64.62`). - Подробные правила для агента по деплою находятся в `deploy/AGENTS.md`; перед любым deploy читать этот файл.
- Второй production-хост SHiNE: `player@193.8.215.70` (`server2.shineup.me`). - Production-серверы SHiNE: `shineup.me` и `server2.shineup.me`.
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`). - Тестовые/devnet серверы SHiNE: `t1.shineup.me`, `t2.shineup.me`, `t3.shineup.me`, `t4.shineup.me`.
- В deploy-документах и скриптах использовать домены, а не IP.
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке. - По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`. - Любые изменения и любой деплой на production (`shineup.me` и `server2.shineup.me`) выполнять только после отдельного явного подтверждения пользователя.
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя. - Перед production deploy обязательно проверить/обновить бэкап в `deploy/backup/archive/`.
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `server2.shineup.me`. - Если пользователь пишет просто `задеплой` без уточнения production/test, уточнить целевой контур; не выбирать production автоматически.
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`. - Deploy выполнять shell-скриптами из `deploy/scripts/`; Gradle deploy-задачи не использовать.
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
- Production server deploy: `./gradlew deployServerProduction`.
- Production UI deploy: `./gradlew deployUIProduction`.
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`). - Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя. - Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`. - Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
@@ -123,26 +118,13 @@
- `unknown_error` - `unknown_error`
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`. - В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
## Недопроверенные фичи (обязательно)
- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`.
- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`.
- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_<short-feature-name>.md`.
- Имена новых файлов и краткие описания фич по возможности писать на русском языке.
- Внутри файла обязательно указывать:
- краткое описание фичи;
- что именно проверять;
- ожидаемый результат;
- статус (например: `pending`, `in_progress`, `done`).
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность.
## Будущие фичи / TODO ## Будущие фичи / TODO
- Папка для задач, сознательно отложенных на будущее: `TODO/`. - Папка для задач, сознательно отложенных на будущее: `TODO/`.
- Точка входа по планам: `TODO/README.md`. - Точка входа по планам: `TODO/README.md`.
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/` и тематическим подпапкам. - Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/` и тематическим подпапкам.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `TODO/README.md`, затем при необходимости конкретные файлы из подпапок. - Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `TODO/README.md`, затем при необходимости конкретные файлы из подпапок.
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя. - Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
- Старую папку `Dev_Docs/Future_Features/` считать выведенной из использования и больше не использовать для новых записей. - Старую папку `docs/Future_Features/` считать выведенной из использования и больше не использовать для новых записей.
- Если часть кода временно отключена или закомментирована, либо удалена как временная заглушка, в TODO-файле подробно описывать: - Если часть кода временно отключена или закомментирована, либо удалена как временная заглушка, в TODO-файле подробно описывать:
- какие файлы и участки отключены; - какие файлы и участки отключены;
- что осталось в коде как заготовка; - что осталось в коде как заготовка;
+1 -1
View File
@@ -5,7 +5,7 @@
@shine-UI/AGENTS.md @shine-UI/AGENTS.md
## Справка по подпроектам ## Справка по подпроектам
- При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`. - При работе с локальным агентом-кодером — читать внешние файлы `../SHiNE-agent-bot-coder/AGENTS.md` и `../SHiNE-agent-bot-coder/AGENT.md`.
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`. - При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`. - При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`. - При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.
+47
View File
@@ -0,0 +1,47 @@
# Правила коммитов, merge и версионирования
Этот файл является единым источником правил для:
- коммитов;
- merge в `main`;
- изменения версий в `VERSION.properties`;
- `git push` из этого репозитория.
## Язык
- Пояснения к коммитам, PR и merge-запросам писать на русском языке.
## Где хранится версия
- Единый файл версий проекта: `VERSION.properties` в корне репозитория.
- Основные поля:
- `client.version` — версия клиентского UI.
- `server.version` — версия серверной части.
## Базовое правило для обычных коммитов
- Перед каждым новым коммитом обязательно обновлять версии в `VERSION.properties`.
- Если менялся только UI, увеличивать только `client.version`.
- Если менялся только сервер, увеличивать только `server.version`.
- Если менялись и UI, и сервер, увеличивать обе версии.
- Для обычных коммитов вне `main` использовать стандартный patch-инкремент: `+1` к последнему числовому сегменту.
- Пример: `1.2.346``1.2.347`.
## Правило для `main`
- Ветка `main` предназначена только для стабильных версий.
- По умолчанию в `main` нужно не коммитить напрямую, а мержить готовые изменения из рабочей ветки.
- Если пользователь просит сделать прямой коммит в `main`, нужно отдельно и явно предупредить, что это обход обычного стабильного процесса, и обязательно переспросить подтверждение.
## Версионирование при merge или прямом коммите в `main`
- Для попадания изменений в `main` действует отдельная схема инкремента.
- Нужно увеличивать вторую цифру версии и обнулять третью.
- Пример: `1.2.346``1.3.0`.
- Если в наборе изменений менялся только UI, обновлять только `client.version`.
- Если в наборе изменений менялся только сервер, обновлять только `server.version`.
- Если соответствующая часть не менялась, её версию не трогать.
## Правила для git commit и git push
- Обычные коммиты делать стандартным `git commit`; токен для локального коммита не нужен и не используется.
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
-93
View File
@@ -1,93 +0,0 @@
# Production-серверы SHiNE
## Короткий ответ
По текущим данным репозитория у SHiNE описаны **два production-контура**:
- `player@shineup.me`
- домен `shineup.me`
- IP `178.208.64.62`
и
- `player@193.8.215.70`
- домен `server2.shineup.me`
- IP `193.8.215.70`
## 1. Основной production-хост
- SSH: `player@shineup.me`
- домен: `shineup.me`
- IP: `178.208.64.62`
- пользователь: `player`
- базовый путь: `/home/player`
Основные каталоги:
- проект SHiNE: `/home/player/SHiNE`
- серверный jar: `/home/player/SHiNE/shine-server/shine-server.jar`
- UI: `/home/player/SHiNE/shine-ui`
- данные: `/home/player/SHiNE/shine-server/data/`
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
Сервисы:
- `shine-server.service`
- `caddy.service`
Caddy:
- активный конфиг: `/etc/caddy/Caddyfile`
- UI root: `/home/player/SHiNE/shine-ui`
- `/ws` проксируется на `127.0.0.1:7070`
Deploy:
- `./gradlew deployServerProduction`
- `./gradlew deployUIProduction`
Правило:
- любые изменения на `shineup.me` делать только после отдельного подтверждения пользователя.
## 2. Второй production-сервер
- SSH: `player@193.8.215.70`
- домен: `server2.shineup.me`
- IP: `193.8.215.70`
- пользователь: `player`
- базовый путь: `/home/player`
Роль:
- второй production-контур SHiNE;
- использовать как production-сервер, несмотря на исторические имена deploy-задач
`deployServerTest2` / `deployUITest2`.
Основные каталоги:
- проект SHiNE: `/home/player/SHiNE`
- серверный jar: `/home/player/SHiNE/shine-server/shine-server.jar`
- UI: `/home/player/SHiNE/shine-ui`
- данные: `/home/player/SHiNE/shine-server/data/`
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
## 3. Связанные публичные production-публикации на том же хосте
На этом же production-хосте есть отдельная публикация для `shine_payments`:
- каталог: `/home/player/sites/test-solana-tickets.shineup.me`
- домены:
- `https://test-solana-tickets.shineup.me`
- `https://test-solana-tickets.shiningpeople.ru`
Это не второй production-хост SHiNE, а отдельный сайт на том же сервере.
## 4. Какие серверы не считать production
Не production:
- `t1.shineup.me`
- `t2.shineup.me`
- `t3.shineup.me`
- `t4.shineup.me`
-35
View File
@@ -1,35 +0,0 @@
# Deploy
Подробности о том, где что задеплоено в SHiNE, нужно искать в папке `Deploy/`.
Эта папка служит краткой картой окружений:
- [TEST_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/TEST_SERVERS.md) — тестовые стенды;
- [PRODUCTION_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/PRODUCTION_SERVERS.md) — production-контур и связанные публичные публикации.
Ниже краткая сводка.
## Основные публичные контуры
- Production SHiNE:
- `player@shineup.me`
- домен `shineup.me`
- IP `178.208.64.62`
- Второй production SHiNE:
- `player@193.8.215.70`
- домен `server2.shineup.me`
- IP `193.8.215.70`
## Отдельный quad-devnet стенд
На отдельном VPS `178.208.90.249` подняты 4 независимых test/devnet-инстанса:
- `t1.shineup.me`
- `t2.shineup.me`
- `t3.shineup.me`
- `t4.shineup.me`
## Важно
- Production-контура SHiNE сейчас два: `shineup.me` и `server2.shineup.me`.
- `t1..t4.shineup.me` — это отдельные тестовые/devnet-контуры, не production.
- Любые изменения на `shineup.me` делать только после отдельного подтверждения пользователя.
-124
View File
@@ -1,124 +0,0 @@
# Тестовые серверы SHiNE
Этот файл описывает тестовые стенды, которые сейчас фигурируют в проекте.
## 1. Исторический `test2`, теперь второй production-сервер
- SSH: `player@193.8.215.70`
- Домен: `server2.shineup.me`
- IP: `193.8.215.70`
- Назначение: второй production-контур SHiNE
Структура:
- каталог SHiNE: `/home/player/SHiNE`
- сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
- UI: `/home/player/SHiNE/shine-ui`
- данные: `/home/player/SHiNE/shine-server/data/`
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
Сервисы:
- `shine-server.service`
- `caddy.service`
Deploy:
- `./gradlew deployServer`
- `./gradlew deployServerTest2`
- `./gradlew deployUI`
- `./gradlew deployUITest2`
Примечания:
- этот хост больше не считать test-контуром;
- исторические имена deploy-задач `deployServerTest2` / `deployUITest2` сохранены, но сам хост считать production;
- задача `deployUITest2` по умолчанию выкладывает UI на `server2.shineup.me`, а не на `t2.shineup.me`;
- при описании окружений перечислять его как второй production-сервер.
## 2. Отдельный quad-devnet стенд `t1..t4`
- VPS: `178.208.90.249`
- пользователь: `player`
- назначение: 4 независимых SHiNE-инстанса на Solana `devnet`
Домены и логины:
- `server_t1` -> `https://t1.shineup.me`
- `server_t2` -> `https://t2.shineup.me`
- `server_t3` -> `https://t3.shineup.me`
- `server_t4` -> `https://t4.shineup.me`
Каталоги:
- `/home/player/t1/server`
- `/home/player/t1/UI`
- `/home/player/t2/server`
- `/home/player/t2/UI`
- `/home/player/t3/server`
- `/home/player/t3/UI`
- `/home/player/t4/server`
- `/home/player/t4/UI`
Подробная памятка на самом VPS:
- `/home/player/Agents.md`
Порты и systemd:
- `t1` -> `7101` -> `shine-t1.service`
- `t2` -> `7102` -> `shine-t2.service`
- `t3` -> `7103` -> `shine-t3.service`
- `t4` -> `7104` -> `shine-t4.service`
Что важно по конфигу каждого инстанса:
- отдельный `/home/player/tX/server/application.properties`
- `server.port=710X`
- `server.SHiNE.login=server_tX`
- `db.path=data/shine.sqlite`
- `solana.cluster=devnet`
- `solana.rpcUrl=https://api.devnet.solana.com`
- `server.ui.indexPath=/home/player/tX/UI/index.html`
- `server.info.url=https://tX.shineup.me`
UI каждого инстанса:
- живёт в отдельной копии `shine-UI`;
- использует свой `js/deploy-config.js`;
- по умолчанию смотрит именно на свой `tX.shineup.me`.
Caddy на стенде:
- конфиг: `/etc/caddy/Caddyfile`
- статика: `/home/player/tX/UI`
- `/ws` проксируется на `127.0.0.1:710X`
Operational-нюанс:
- при одновременных рестартах возможны `HTTP 429` от `api.devnet.solana.com`;
- поэтому сервисы `shine-t1..shine-t4` лучше перезапускать по одному, с паузой.
## 3. Что проверять первым делом
Для любого test-контура полезны такие быстрые проверки:
```bash
curl -I https://server2.shineup.me
curl -I https://t1.shineup.me
curl -I https://t2.shineup.me
curl -I https://t3.shineup.me
curl -I https://t4.shineup.me
```
Для quad-devnet VPS:
```bash
sudo systemctl --no-pager --full status caddy shine-t1 shine-t2 shine-t3 shine-t4
```
Для второго production-контура:
```bash
sudo systemctl --no-pager --full status shine-server caddy
```
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
@@ -1,71 +0,0 @@
# Задание для Айдара: навести порядок в инструкциях агентов SHiNE
## Кратко
Нужно согласовать и оформить единый порядок инструкций для Codex/Telegram-агентов в проекте SHiNE, чтобы агенты стабильно понимали структуру проекта, границы ответственности и правила работы с сервером, UI, Solana-модулем, Telegram-ботом и игроками.
## Зачем это нужно
Сейчас проект состоит из нескольких связанных, но разных частей:
- основной сервер `SHiNE-server/`;
- UI `shine-UI/`;
- Solana/Anchor-модуль `shine-solana/shine/`;
- Telegram-агент-кодер `SHiNE-agent-bot-coder/`;
- TURN-сервер;
- документация `Dev_Docs/`;
- отдельные рабочие папки игроков `Players/`.
Без явных инструкций агент может путать эти зоны: например, смешать деплой Solana с деплоем сервера, изменить код от имени игрока, не обновить документацию API/DM/блокчейна или неправильно трактовать файл инструкций.
## Что предлагается сделать
1. Утвердить корневой `AGENTS.md` как главный набор правил проекта.
2. Проверить и при необходимости уточнить локальный `AGENTS.md` внутри `shine-solana/shine/`.
3. Оставить отдельные служебные инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENT.md`.
4. Оставить автоматически читаемые инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENTS.md`.
5. Явно закрепить режим игроков:
- игроки могут задавать вопросы, просить анализ, идеи и ТЗ;
- игроки не меняют код проекта напрямую;
- материалы игроков сохраняются только в `Players/<username>/`.
6. Зафиксировать правило: если пользователь говорит «агент MD» или похожую формулировку, считать, что речь про автоматически читаемый `AGENTS.md`.
7. Добавить простой процесс согласования изменений инструкций:
- Дима или другой участник готовит предложение;
- Айдар получает уведомление/заявку;
- Айдар отвечает: одобрить, отклонить или попросить доработать;
- только после одобрения агент вносит изменения в проектные инструкции.
## Предлагаемая логика уведомления Айдару
Минимальный вариант без сложной разработки:
1. Агент готовит текст заявки.
2. Текст отправляется Айдару в Telegram или в общий рабочий чат.
3. В заявке явно указаны варианты ответа:
- `одобрить`;
- `отклонить`;
- `доработать: ...`.
4. После ответа Айдара агент либо выполняет согласованные правки, либо фиксирует, что задача отклонена/нужна доработка.
Более удобный вариант на будущее:
- добавить в Telegram-бота команду или сценарий согласования задач, например:
- `/approve <id>`;
- `/reject <id> причина`;
- `/revise <id> комментарий`.
Но для начала достаточно простого текстового согласования через Telegram.
## Что нужно от Айдара
Подтвердить, что такой порядок подходит:
1. Корневой `AGENTS.md` остается главным правилом проекта.
2. Для Solana, Telegram-агента и игроков сохраняются отдельные локальные правила.
3. Игроки не меняют код напрямую, а готовят материалы и предложения.
4. Изменения инструкций выполняются только после явного одобрения Айдара.
5. Уведомления Айдару на первом этапе можно делать простым текстом в Telegram, без отдельной сложной системы заявок.
## Ожидаемый результат
После одобрения:
- агенты будут стабильнее понимать границы проекта;
- снизится риск случайных изменений не в той части системы;
- появится понятный порядок согласования задач от игроков;
- Айдар будет явно контролировать изменения в инструкциях и правилах работы агентов.
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-19
View File
@@ -1,19 +0,0 @@
TELEGRAM_BOT_TOKEN=replace_me
OPENAI_API_KEY=replace_me
ALLOWED_TELEGRAM_USERNAME=AidarKC
ALLOWED_TELEGRAM_PLAYERS=malvviiina:Милана,zodiaktechnika32:Сергей,oidasyda:Иван,blackbyrd1:Ворон,dimasol1:Дима
ALLOWED_TELEGRAM_CHANNEL_USERNAME=shine_writing
BOT_USERNAME=aidar_su_bot
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
OPENAI_TTS_MODEL=gpt-4o-mini-tts
OPENAI_TTS_VOICE=alloy
OPENAI_TTS_RESPONSE_FORMAT=opus
OPENAI_TTS_TIMEOUT_SECONDS=180
OPENAI_TTS_CHUNK_CHARS=3500
CODEX_BIN=/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl
CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256
CODEX_TIMEOUT_SECONDS=900
MAX_RETRIES=3
DATA_DIR=./data
-5
View File
@@ -1,5 +0,0 @@
.env
data/
logs/
run/
__pycache__/
-81
View File
@@ -1,81 +0,0 @@
# AGENT.md для SHiNE-agent-bot-coder
Ты запущен как обработчик входящего Telegram-сообщения от пользователя.
## Контекст
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с этим проектом.
- Сервис принимает входящие сообщения от пользователя Telegram, сохраняет историю, ставит задачи в очередь и последовательно запускает Codex CLI в рабочем проекте.
- Текстовые сообщения обрабатываются напрямую, voice/audio сначала распознаются через OpenAI transcription, затем передаются как текстовая задача.
- История диалога хранится в JSONL-файле, путь передаётся в промпте.
- Сообщение может быть текстом или результатом распознавания голосового.
- Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение.
- Единственная рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; старая Java-реализация удалена как нерабочая и не должна восстанавливаться без отдельного решения Айдара.
- В репозитории также есть отдельный Solana/Anchor-модуль `shine-solana/shine/`; он логически связан с SHiNE, но не должен автоматически подключаться к основному серверному deploy без отдельной команды.
- Перед изменениями внутри `shine-solana/shine/` читать локальные инструкции `shine-solana/shine/AGENTS.md`; в git не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
## Авторитет команд и история
- Основной пользователь и источник команд — Айдар: `@AidarKC` / `@aidarkc`.
- Дополнительно разрешены игроки из whitelist (`ALLOWED_TELEGRAM_PLAYERS`), каждый со своей отдельной историей и рабочей папкой `Players/<username>/`.
- Игроки работают в режиме вопросов/анализа/подготовки материалов: в промпте явно задано правило не менять код проекта и писать материалы только в своей папке.
- Для неизвестных пользователей в личном чате сервис отвечает вежливым отказом.
- В Telegram-канале/группе `@shine_writing` сервис выполняет сообщения только от Айдара, а ответы отправляет в тот же чат.
- Если Telegram сообщает о миграции обычной группы в supergroup, сервис должен запомнить новый `chat_id` и отправлять ответы уже туда.
- На события подключения/отключения пользователей (join/leave) сервис не отвечает и ничего не отправляет.
## Очередь и состояние
- Входящие задачи записываются в файловую очередь и обрабатываются строго по одной, чтобы не смешивать изменения в проекте.
- Сервис ведёт состояние активной задачи и текущего файла истории, а после рестарта продолжает незавершённую обработку с учётом сохранённого состояния.
- Истории диалогов хранятся в JSONL по каждому разрешённому username отдельно: `data/history/<username>/`.
- Архив истории после `/new`: `data/history/<username>/archive/`.
- После `/new` для этого же пользователя должен сбрасываться и контекст продолжения Codex-сессии; следующий запрос запускается как новая сессия, не через resume.
- Для просмотра истории игрока открывать файлы в его папке истории по username.
- Дедупликация входящих Telegram update нужна, чтобы одно сообщение не попало в обработку повторно.
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус с общим временем работы задачи; при дальнейшем молчании повторяет статус каждые 2 минуты.
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`; для новых пользователей оно включено по умолчанию.
- Адаптация текста перед озвучкой настраивается персонально командами `/voice_rewrite_on`, `/voice_rewrite_off`. Если она включена, сервис перед TTS вызывает дешёвую текстовую модель OpenAI и делает голосовую версию без длинных хэшей, путей, команд и технического шума, сохраняя смысл и порядок исходного ответа.
- Режим личных ответов настраивается персонально командами `/single_message_on`, `/single_message_off`: либо одно редактируемое сообщение по этапам, либо отдельные сообщения как раньше.
- Команда `/settings` должна сразу показывать текущее состояние всех персональных настроек пользователя и список команд для их изменения.
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS даже для текстовых запросов. Voice отправляется в исходный чат, а также в известный личный чат пользователя и в общий чат `@shine_writing`, если они отличаются и доступны. Промежуточные статусы не озвучивать.
- Команда `/status` должна показывать состояние очереди и персональные настройки: voice-ответы, адаптацию текста перед озвучкой и режим одного сообщения в личке.
## Правила голосовой версии ответа
- Текстовый финальный ответ должен оставаться полноценным: в нём можно указывать команды, пути, хэши коммитов, номера версий, результаты проверок и другие технические детали.
- Голосовую версию финального ответа нужно делать короче и проще для восприятия на слух. Основной механизм — персонально включаемая адаптация текста через дополнительный OpenAI-вызов перед TTS.
- В голосовой версии не зачитывать длинные хэши коммитов, токены, file_id, длинные команды, полные пути и другие строки, которые человек всё равно не сможет надёжно запомнить на слух.
- Для commit/push в голосовой версии достаточно сказать краткий итог: что коммит сделан, что именно изменено, проверки прошли без ошибок, push выполнен, рабочее дерево чистое.
- Если пользователю нужны точные команды, хэши или подробности, они должны оставаться в текстовом ответе.
## Планы и отложенные фичи
- Планы проекта по отложенным фичам хранятся в `TODO/`.
- Внутри есть три горизонта:
- `near/` - ближайшие планы, обычно сегодня/завтра;
- `medium/` - среднесрочные планы, обычно недели или 1-2 месяца;
- `far/` - дальнее будущее без понятного срока.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, нужно смотреть эти три папки и отвечать кратким списком по горизонтам.
- Файлы из `TODO/` не начинать реализовывать без явной команды пользователя.
- После реализации фичи, требующей ручной проверки, нужно добавить отдельный файл в `Dev_Docs/Pending_Features/`.
## Центр задач и предложений
- Сервис хранит простые задачи и предложения в `data/task_center/items.json`.
- Айдар может смотреть список через `/tasks` или естественные фразы вроде «покажи мои задачи», «покажи задачи Миланы».
- Айдар может ставить задачи игрокам фразой вида «поставь задачу Милане: ...».
- Игроки могут отправлять предложения Айдару фразой вида `предложение: ...`, `идея: ...` или `заявка: ...`.
- Статусы меняются фразами с ID: `одобрить TC-0001`, `отклонить TC-0001`, `доработать TC-0001`, `закрыть TC-0001`.
- После финального ответа в личном чате сервис добавляет короткое напоминание, если у пользователя есть активные задачи или предложения.
## Локальный запуск и systemd
- Основной запуск сервиса выполняется Python-скриптом `py_bot_service.py` из папки `SHiNE-agent-bot-coder/`.
- Локальные секреты и параметры должны храниться в `.env`, этот файл не коммитится.
- Для проверки Codex без Telegram можно использовать self-test режим сервиса.
- Для постоянного локального запуска используется user-level systemd service `shine-agent-bot-coder`; скрипты установки лежат в `SHiNE-agent-bot-coder/scripts/systemd/`.
- Если меняется логика сервиса, после изменений нужно проверить запуск локально и при необходимости перезапустить user systemd service.
- Команда Telegram `/restart` (`/restart_service`) доступна только Айдару и выполняет отложенный рестарт после текущей задачи, до взятия следующей. Аварийный жёсткий рестарт доступен только Айдару командами `/restart_hard`, `/restart_now`, `/restart_force`.
## Правила ответа
- Пиши содержательно и коротко.
- Не упоминай внутренние служебные детали, файловую систему и технические логи.
- Если запрос требует действий с кодом/проектом, выполняй их в рабочей директории.
- Если для ответа данных недостаточно, задай ровно один уточняющий вопрос.
- Если была ошибка предыдущего запуска, в промпте будет пометка retry — учти это и продолжи с учётом текущего состояния проекта.
-25
View File
@@ -1,25 +0,0 @@
# AGENTS
## Назначение
- Это автоматически читаемые инструкции Codex для папки `SHiNE-agent-bot-coder/`.
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с проектом SHiNE.
- Если пользователь говорит «агент MD», «агент с MD» или похожим образом про файл инструкций Codex, считать, что имеется в виду `AGENTS.md`.
## Связанные инструкции
- Подробные служебные правила Telegram-обработчика лежат в `AGENT.md`.
- `AGENT.md` используется самим сервисом как файл инструкций, который передаётся в промпт обработчика входящих Telegram-сообщений.
- При изменении логики сервиса сначала читать `AGENT.md`, затем код `py_bot_service.py`.
## Планы и задачи
- Отложенные задачи проекта лежат в `../TODO/`.
- Точка входа по планам: `../TODO/README.md`.
- Горизонты планов:
- `near/` - ближайшие планы;
- `medium/` - среднесрочные планы;
- `far/` - дальнее будущее.
- Если пользователь спрашивает, какие есть планы или что можно продолжить, кратко перечислять задачи по этим горизонтам.
- Не начинать реализацию задач из `TODO` без явной команды пользователя.
## Проверка после изменений
- Если меняется логика Telegram-бота, проверить локальный запуск или self-test, когда это уместно.
- Если меняется только документация или инструкции, достаточно проверить, что ссылки на документы актуальны.
-2
View File
@@ -1,2 +0,0 @@
@AGENTS.md
@AGENT.md
@@ -1,26 +0,0 @@
# Промпты для режима игроков (на согласование)
## 1) Базовый служебный промпт (добавка к задаче игрока)
```text
Режим игрока (обязательно):
- Пользователь: <Имя> (@<username>).
- Рабочая папка игрока: <project>/Players/<username>
- Код проекта не изменять.
- Можно отвечать на вопросы по проекту, предлагать идеи и готовить ТЗ.
- Если нужны правки кода, описывать предложение текстом и сохранять материалы только в папке игрока.
```
## 2) Приветственное сообщение игроку (один раз)
```text
Привет, <Имя>.
Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.
Команда /new начинает новую сессию и архивирует текущую историю.
```
## 3) Отказ неизвестному пользователю
```text
Извините, доступ к этому агенту пока не выдан. Обратитесь к Айдару.
```
-100
View File
@@ -1,100 +0,0 @@
# SHiNE-agent-bot-coder
Локальный Telegram-бот-сервис для пользователя `ai`:
- принимает сообщения от `@AidarKC`;
- поддерживает whitelist игроков (`ALLOWED_TELEGRAM_PLAYERS`) с отдельными историями;
- ведёт историю диалога в `JSONL`;
- ставит задачи в файловую очередь;
- обрабатывает задачи строго последовательно;
- поддерживает текстовые и голосовые сообщения (voice/audio через OpenAI transcription);
- вызывает Codex CLI и отправляет ответ в Telegram;
- в личном чате умеет работать в двух персонально переключаемых режимах: через одно редактируемое статусное сообщение или через отдельные сообщения по этапам;
- умеет персонально для каждого пользователя озвучивать финальный ответ через OpenAI TTS;
- при рестарте восстанавливает незавершённые задачи;
- отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи;
- принимает сообщения из канала/группы `@shine_writing`, выполняет команды только от `@AidarKC`;
- учитывает миграцию обычной Telegram-группы в supergroup и перенаправляет ответы на новый `chat_id`.
Рабочая реализация сервиса — только `py_bot_service.py`. Старая Java-реализация удалена, потому что не заработала и больше не используется.
## Структура
- `.env` — локальные секреты и параметры запуска (не коммитится);
- `data/py_queue.jsonl` — очередь Python-сервиса;
- `data/py_state.json` — текущее состояние Python-сервиса;
- `data/py_processed_updates.log` — дедуп входящих update;
- `data/history/<username>/*.jsonl` — активные истории по пользователям;
- `data/history/<username>/archive/*.jsonl` — архивы после `/new`.
## Локальный запуск
1. Скопировать пример:
- `cp .env.example .env`
2. Заполнить секреты в `.env`.
- `TELEGRAM_BOT_TOKEN` — токен рабочего Telegram-бота.
- `ALLOWED_TELEGRAM_USERNAME` — пользователь, чьи сообщения выполняются как команды.
- `ALLOWED_TELEGRAM_PLAYERS` — whitelist игроков в формате `username:Имя,username2:Имя2`.
- `ALLOWED_TELEGRAM_CHANNEL_USERNAME` — канал, из которого принимаются `channel_post`; обычные group/supergroup-сообщения обрабатываются как `message`.
- `TELEGRAM_API_BASE_URL` — базовый URL Bot API; по умолчанию `https://api.telegram.org`. Для очень больших voice/audio можно поднять локальный `telegram-bot-api` и направить бота туда.
- `TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS` — тайм-аут скачивания voice/audio из Telegram, по умолчанию 300 секунд.
- `OPENAI_TRANSCRIBE_TIMEOUT_SECONDS` — тайм-аут распознавания voice/audio в OpenAI, по умолчанию 900 секунд.
- `OPENAI_TRANSCRIBE_MAX_UPLOAD_BYTES` — безопасный лимит размера одного куска для OpenAI transcription, по умолчанию `24 MiB`.
- `OPENAI_TRANSCRIBE_MAX_CHUNK_SECONDS` — максимальная длина одного куска при длинном аудио, по умолчанию `900` секунд.
- `OPENAI_TRANSCRIBE_OVERLAP_SECONDS` — перекрытие соседних кусков для более ровной склейки текста, по умолчанию `2` секунды.
- `OPENAI_TRANSCRIBE_REENCODE_BITRATE_KBPS` — битрейт локального пережатия длинного аудио через `ffmpeg`, по умолчанию `24`.
- `OPENAI_TRANSCRIBE_FFMPEG_TIMEOUT_SECONDS` — тайм-аут локальной обработки длинного аудио через `ffmpeg`/`ffprobe`, по умолчанию `1800`.
- `FFMPEG_BIN` и `FFPROBE_BIN` — пути к локальным бинарям `ffmpeg`/`ffprobe`, если они не лежат в `PATH`.
- `OPENAI_TTS_MODEL` — модель синтеза речи, по умолчанию `gpt-4o-mini-tts`.
- `OPENAI_TTS_VOICE` — голос синтеза речи, по умолчанию `alloy`.
- `OPENAI_TTS_RESPONSE_FORMAT` — аудиоформат для Telegram voice, по умолчанию `opus`.
- `OPENAI_TTS_TIMEOUT_SECONDS` — тайм-аут генерации одного фрагмента речи, по умолчанию 180 секунд.
- `OPENAI_TTS_CHUNK_CHARS` — максимальный размер одного фрагмента озвучки, по умолчанию 3500 символов.
3. Запуск:
- `python3 SHiNE-agent-bot-coder/py_bot_service.py`
## Быстрый self-test Codex (без Telegram)
```bash
python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь одной строкой: Codex работает"
```
## Длинные voice/audio
- Если аудио короткое, бот отправляет его в OpenAI как раньше.
- Если аудио большое или длинное, бот локально пережимает его через `ffmpeg`, при необходимости режет на куски и распознаёт последовательно.
- Если Telegram заранее сообщает большой размер файла, бот больше не отказывается сразу: сначала явно пишет, что пробует скачать файл, затем отдельно сообщает, удалось ли скачивание, и только после успешной загрузки переходит к подготовке аудио и OpenAI.
- Для очень больших файлов упираемся не только в OpenAI, но и в лимит обычного облачного Telegram Bot API на скачивание файла ботом. Для таких случаев нужно использовать локальный `telegram-bot-api` сервер и указать его через `TELEGRAM_API_BASE_URL`.
## Статусы в личке
- Для `private`-чата бот поддерживает персональную настройку режима ответа.
- По умолчанию он старается не засорять переписку промежуточными сообщениями: создаёт одно статусное сообщение и редактирует его по этапам.
- Если включить `/single_message_off`, бот возвращается к старому режиму и отправляет отдельные сообщения по этапам и финальный ответ отдельно.
- Если финальный текст в режиме одного сообщения не помещается целиком, бот оставляет первую часть в отредактированном статусном сообщении и отправляет максимум ещё одно дополнительное текстовое сообщение с хвостом ответа.
- Голосовой ответ, если он включён, всегда приходит отдельным новым сообщением.
## Запуск как systemd-сервис
Файлы для установки:
- `scripts/systemd/shine-agent-bot-coder.service`
- `scripts/systemd/install-local-systemd.sh`
Установка:
- `bash SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh`
Проверка:
- `systemctl --user status shine-agent-bot-coder --no-pager`
- `journalctl --user -u shine-agent-bot-coder -f`
Перезапуск после изменений:
- `systemctl --user restart shine-agent-bot-coder`
## Telegram-команды
- `/status` — активная задача и размер очереди.
- `/settings` — текущие пользовательские настройки и команды для их изменения.
- `/queue` — список задач в очереди.
- `/stop` — остановить текущую задачу.
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
- `/new` — архивировать текущую историю, сбросить продолжение Codex-сессии для этого пользователя и начать новый диалог.
- `/voice_on` — включить озвучивание финальных ответов для текущего пользователя.
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
- `/voice_rewrite_on` — включить адаптацию текста перед озвучкой.
- `/voice_rewrite_off` — выключить адаптацию текста перед озвучкой.
- `/single_message_on` — вести ответ в личке через одно редактируемое сообщение.
- `/single_message_off` — слать отдельные сообщения по этапам и отдельный финальный ответ.
- `/restart` или `/restart_service` — отложенный рестарт после текущей задачи, до взятия следующей (только для Айдара).
- `/restart_hard` — жёсткий рестарт прямо сейчас (только для Айдара).
File diff suppressed because it is too large Load Diff
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="/home/ai/work/SHiNE/SHiNE-server-sha256"
SERVICE_DIR="${ROOT_DIR}/SHiNE-agent-bot-coder"
UNIT_SRC="${SERVICE_DIR}/scripts/systemd/shine-agent-bot-coder.service"
UNIT_DST="${HOME}/.config/systemd/user/shine-agent-bot-coder.service"
echo "[1/6] Проверка python3..."
command -v python3 >/dev/null 2>&1 || { echo "python3 не найден"; exit 1; }
echo "[2/6] Подготовка папки логов..."
mkdir -p "${SERVICE_DIR}/logs"
echo "[3/6] Копирование user systemd unit..."
mkdir -p "$(dirname "${UNIT_DST}")"
cp "${UNIT_SRC}" "${UNIT_DST}"
echo "[4/6] daemon-reload..."
systemctl --user daemon-reload
echo "[5/6] enable + start..."
systemctl --user enable --now shine-agent-bot-coder
echo "[6/6] Статус:"
systemctl --user status shine-agent-bot-coder --no-pager
echo "Готово. Логи: journalctl --user -u shine-agent-bot-coder -f"
@@ -1,19 +0,0 @@
[Unit]
Description=SHiNE Agent Bot Coder (Telegram + Codex queue worker)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder
EnvironmentFile=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/.env
ExecStart=/usr/bin/python3 /home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/py_bot_service.py
Restart=always
RestartSec=5
TimeoutStopSec=20
SuccessExitStatus=143 0
StandardOutput=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
StandardError=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
[Install]
WantedBy=default.target
+7 -17
View File
@@ -45,30 +45,20 @@ shine-UI/server-ui.html
- `shine_users`: `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6` - `shine_users`: `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6`
- `shine_payments`: `SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v` - `shine_payments`: `SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v`
Подробнее: `Dev_Docs/Инициализация_Solana_регистрации/README.md` Подробнее: `docs/Инициализация_Solana_регистрации/README.md`
## Синхронизация с партнёрскими серверами ## Синхронизация с партнёрскими серверами
Сервер должен синхронизировать блоки блокчейна и DM с серверами-партнёрами из `sync_servers`. Сервер должен синхронизировать блоки блокчейна и DM с серверами-партнёрами из `sync_servers`.
Детали: `Dev_Docs/Blockchain/sync-between-servers.md` Детали: `docs/Blockchain/sync-between-servers.md`
## Деплой ## Деплой
``` - Основные инструкции по деплою находятся в `../deploy/AGENTS.md`.
./gradlew deployServer - Deploy выполнять shell-скриптами из `../deploy/scripts/`.
./gradlew deployUI - Gradle deploy-задачи не использовать: Gradle остаётся для сборки и локального запуска.
``` - Любые изменения на production (`shineup.me`, `server2.shineup.me`) делать только после отдельного явного подтверждения пользователя.
- Перед production deploy обязательно обновить/проверить backup в `deploy/backup/archive/`.
Default deploy по умолчанию идёт на `server2.shineup.me` (`player@193.8.215.70`).
Production deploy:
```
./gradlew deployServerProduction
./gradlew deployUIProduction
```
Любые изменения на `shineup.me` делать только после отдельного явного подтверждения пользователя.
Логи на проде: Логи на проде:
- `/home/player/SHiNE/shine-server/logs/app.log` - `/home/player/SHiNE/shine-server/logs/app.log`
@@ -641,6 +641,7 @@ public final class DatabaseInitializer {
origin_session_id TEXT, origin_session_id TEXT,
receipt_ref_base_key TEXT, receipt_ref_base_key TEXT,
receipt_ref_type INTEGER, receipt_ref_type INTEGER,
read_at_ms INTEGER,
FOREIGN KEY (from_login) REFERENCES solana_users(login), FOREIGN KEY (from_login) REFERENCES solana_users(login),
FOREIGN KEY (to_login) REFERENCES solana_users(login) FOREIGN KEY (to_login) REFERENCES solana_users(login)
); );
@@ -14,7 +14,7 @@ import java.sql.Statement;
public final class SqliteDbController { public final class SqliteDbController {
private static volatile SqliteDbController instance; private static volatile SqliteDbController instance;
private static final int LATEST_SCHEMA_VERSION = 11; private static final int LATEST_SCHEMA_VERSION = 12;
private final String jdbcUrl; private final String jdbcUrl;
@@ -94,6 +94,7 @@ public final class SqliteDbController {
case 9 -> migrateToV9(); case 9 -> migrateToV9();
case 10 -> migrateToV10(); case 10 -> migrateToV10();
case 11 -> migrateToV11(); case 11 -> migrateToV11();
case 12 -> migrateToV12();
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion); default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
} }
} }
@@ -329,6 +330,26 @@ public final class SqliteDbController {
} }
} }
private void migrateToV12() {
try (Connection c = DriverManager.getConnection(jdbcUrl);
Statement st = c.createStatement()) {
c.setAutoCommit(false);
try {
ensureSignedMessagesReadAtColumn(c, st);
backfillSignedMessagesReadAt(st);
setSchemaVersion(c, 12);
c.commit();
} catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {}
throw new RuntimeException("DB migration to v12 failed", e);
} finally {
try { c.setAutoCommit(true); } catch (Exception ignored) {}
}
} catch (SQLException e) {
throw new RuntimeException("DB migration to v12 failed", e);
}
}
private static void ensureChat200StateTables(Statement st) throws SQLException { private static void ensureChat200StateTables(Statement st) throws SQLException {
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_state ( CREATE TABLE IF NOT EXISTS chat200_state (
@@ -463,6 +484,33 @@ public final class SqliteDbController {
} }
} }
private static void ensureSignedMessagesReadAtColumn(Connection c, Statement st) throws SQLException {
if (!tableExists(c, "signed_messages_v2")) return;
if (!columnExists(c, "signed_messages_v2", "read_at_ms")) {
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN read_at_ms INTEGER");
}
}
private static void backfillSignedMessagesReadAt(Statement st) throws SQLException {
st.executeUpdate("""
UPDATE signed_messages_v2 AS content
SET read_at_ms = (
SELECT MIN(receipt.time_ms)
FROM signed_messages_v2 AS receipt
WHERE receipt.message_type IN (3, 4)
AND receipt.receipt_ref_base_key = content.base_key
)
WHERE content.message_type IN (1, 2)
AND (content.read_at_ms IS NULL OR content.read_at_ms <= 0)
AND EXISTS (
SELECT 1
FROM signed_messages_v2 AS receipt
WHERE receipt.message_type IN (3, 4)
AND receipt.receipt_ref_base_key = content.base_key
);
""");
}
/** /**
* Временная одноразовая миграция на переходе к SHiNE_DM v1: * Временная одноразовая миграция на переходе к SHiNE_DM v1:
* старые строки signed_messages_v2 больше не гарантированно совместимы * старые строки signed_messages_v2 больше не гарантированно совместимы
@@ -7,10 +7,14 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public final class SignedMessagesV2DAO { public final class SignedMessagesV2DAO {
private static final int SQLITE_BUSY_MAX_RETRIES = 6;
private static final long SQLITE_BUSY_RETRY_BASE_DELAY_MS = 40L;
public enum ApplyStatus { public enum ApplyStatus {
APPLIED, APPLIED,
DUPLICATE_OR_OLDER, DUPLICATE_OR_OLDER,
@@ -37,6 +41,7 @@ public final class SignedMessagesV2DAO {
} }
public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception { public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) { if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE; return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
@@ -46,17 +51,23 @@ public final class SignedMessagesV2DAO {
message_key, base_key, target_login, from_login, to_login, message_key, base_key, target_login, from_login, to_login,
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms, time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
raw_block, created_at_ms, source_api, origin_session_id, raw_block, created_at_ms, source_api, origin_session_id,
receipt_ref_base_key, receipt_ref_type receipt_ref_base_key, receipt_ref_type, read_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
bindSignedMessage(ps, e); bindSignedMessage(ps, e);
return ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER; ApplyStatus status = ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
if (status.applied()) {
markMessageReadByReceipt(c, e);
}
return status;
} }
} }
});
} }
public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception { public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean prevAutoCommit = c.getAutoCommit(); boolean prevAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
@@ -64,6 +75,8 @@ public final class SignedMessagesV2DAO {
int insertedFirst = insertStrict(c, first); int insertedFirst = insertStrict(c, first);
int insertedSecond = insertStrict(c, second); int insertedSecond = insertStrict(c, second);
if (insertedFirst == 1 && insertedSecond == 1) { if (insertedFirst == 1 && insertedSecond == 1) {
markMessageReadByReceipt(c, first);
markMessageReadByReceipt(c, second);
c.commit(); c.commit();
return true; return true;
} }
@@ -79,9 +92,11 @@ public final class SignedMessagesV2DAO {
c.setAutoCommit(prevAutoCommit); c.setAutoCommit(prevAutoCommit);
} }
} }
});
} }
public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception { public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean prevAutoCommit = c.getAutoCommit(); boolean prevAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
@@ -104,6 +119,8 @@ public final class SignedMessagesV2DAO {
upsertMessage(c, incoming); upsertMessage(c, incoming);
upsertMessage(c, outgoing); upsertMessage(c, outgoing);
markMessageReadByReceipt(c, incoming);
markMessageReadByReceipt(c, outgoing);
resetDeliveryRows(c, incoming.getMessageKey()); resetDeliveryRows(c, incoming.getMessageKey());
resetDeliveryRows(c, outgoing.getMessageKey()); resetDeliveryRows(c, outgoing.getMessageKey());
@@ -116,9 +133,11 @@ public final class SignedMessagesV2DAO {
c.setAutoCommit(prevAutoCommit); c.setAutoCommit(prevAutoCommit);
} }
} }
});
} }
public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception { public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean prevAutoCommit = c.getAutoCommit(); boolean prevAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
@@ -140,6 +159,7 @@ public final class SignedMessagesV2DAO {
} }
upsertMessage(c, incoming); upsertMessage(c, incoming);
markMessageReadByReceipt(c, incoming);
resetDeliveryRows(c, incoming.getMessageKey()); resetDeliveryRows(c, incoming.getMessageKey());
c.commit(); c.commit();
return ApplyStatus.APPLIED; return ApplyStatus.APPLIED;
@@ -150,9 +170,11 @@ public final class SignedMessagesV2DAO {
c.setAutoCommit(prevAutoCommit); c.setAutoCommit(prevAutoCommit);
} }
} }
});
} }
public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception { public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean prevAutoCommit = c.getAutoCommit(); boolean prevAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
@@ -179,9 +201,11 @@ public final class SignedMessagesV2DAO {
c.setAutoCommit(prevAutoCommit); c.setAutoCommit(prevAutoCommit);
} }
} }
});
} }
public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception { public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean prevAutoCommit = c.getAutoCommit(); boolean prevAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
@@ -205,6 +229,7 @@ public final class SignedMessagesV2DAO {
c.setAutoCommit(prevAutoCommit); c.setAutoCommit(prevAutoCommit);
} }
} }
});
} }
public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception { public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception {
@@ -214,7 +239,7 @@ public final class SignedMessagesV2DAO {
message_key, base_key, target_login, from_login, to_login, message_key, base_key, target_login, from_login, to_login,
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms, time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
raw_block, created_at_ms, source_api, origin_session_id, raw_block, created_at_ms, source_api, origin_session_id,
receipt_ref_base_key, receipt_ref_type receipt_ref_base_key, receipt_ref_type, read_at_ms
FROM signed_messages_v2 FROM signed_messages_v2
WHERE message_key = ? WHERE message_key = ?
"""; """;
@@ -235,6 +260,12 @@ public final class SignedMessagesV2DAO {
} }
public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception { public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception {
ensureDeliveryRows(messageKey, List.of(sessionId), nowMs);
}
public void ensureDeliveryRows(String messageKey, List<String> sessionIds, long nowMs) throws Exception {
if (sessionIds == null || sessionIds.isEmpty()) return;
withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
String sql = """ String sql = """
INSERT OR IGNORE INTO signed_message_session_delivery ( INSERT OR IGNORE INTO signed_message_session_delivery (
@@ -242,43 +273,49 @@ public final class SignedMessagesV2DAO {
) VALUES (?, ?, 0, NULL, ?) ) VALUES (?, ?, 0, NULL, ?)
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
for (String sessionId : sessionIds) {
if (sessionId == null || sessionId.isBlank()) continue;
ps.setString(1, messageKey); ps.setString(1, messageKey);
ps.setString(2, sessionId); ps.setString(2, sessionId);
ps.setLong(3, nowMs); ps.setLong(3, nowMs);
ps.executeUpdate(); ps.addBatch();
} }
ps.executeBatch();
} }
return null;
}
});
} }
public void markDelivered(String messageKey, String sessionId, long deliveredAtMs) throws Exception { public void markDelivered(String messageKey, String sessionId, long deliveredAtMs) throws Exception {
withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
String insertSql = """ String sql = """
INSERT OR IGNORE INTO signed_message_session_delivery ( INSERT INTO signed_message_session_delivery (
message_key, session_id, delivered, delivered_at_ms, created_at_ms message_key, session_id, delivered, delivered_at_ms, created_at_ms
) VALUES (?, ?, 0, NULL, ?) ) VALUES (?, ?, 1, ?, ?)
ON CONFLICT(message_key, session_id) DO UPDATE SET
delivered = 1,
delivered_at_ms = CASE
WHEN signed_message_session_delivery.delivered_at_ms IS NULL THEN excluded.delivered_at_ms
WHEN signed_message_session_delivery.delivered_at_ms > excluded.delivered_at_ms THEN excluded.delivered_at_ms
ELSE signed_message_session_delivery.delivered_at_ms
END
"""; """;
try (PreparedStatement ps = c.prepareStatement(insertSql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, messageKey); ps.setString(1, messageKey);
ps.setString(2, sessionId); ps.setString(2, sessionId);
ps.setLong(3, deliveredAtMs); ps.setLong(3, deliveredAtMs);
ps.setLong(4, deliveredAtMs);
ps.executeUpdate(); ps.executeUpdate();
} }
return null;
String updateSql = """
UPDATE signed_message_session_delivery
SET delivered = 1, delivered_at_ms = ?
WHERE message_key = ? AND session_id = ?
""";
try (PreparedStatement ps = c.prepareStatement(updateSql)) {
ps.setLong(1, deliveredAtMs);
ps.setString(2, messageKey);
ps.setString(3, sessionId);
ps.executeUpdate();
}
} }
});
} }
public List<SignedMessageV2Entry> listPendingForSession(String login, String sessionId) throws Exception { public List<SignedMessageV2Entry> listPendingForSession(String login, String sessionId) throws Exception {
return withBusyRetry(() -> {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
String fillSql = """ String fillSql = """
INSERT OR IGNORE INTO signed_message_session_delivery ( INSERT OR IGNORE INTO signed_message_session_delivery (
@@ -309,7 +346,7 @@ public final class SignedMessagesV2DAO {
m.message_key, m.base_key, m.target_login, m.from_login, m.to_login, m.message_key, m.base_key, m.target_login, m.from_login, m.to_login,
m.time_ms, m.nonce, m.message_type, m.revision_time_ms, m.reencrypted_at_ms, m.time_ms, m.nonce, m.message_type, m.revision_time_ms, m.reencrypted_at_ms,
m.raw_block, m.created_at_ms, m.source_api, m.origin_session_id, m.raw_block, m.created_at_ms, m.source_api, m.origin_session_id,
m.receipt_ref_base_key, m.receipt_ref_type m.receipt_ref_base_key, m.receipt_ref_type, m.read_at_ms
FROM signed_messages_v2 m FROM signed_messages_v2 m
JOIN signed_message_session_delivery d JOIN signed_message_session_delivery d
ON d.message_key = m.message_key ON d.message_key = m.message_key
@@ -325,6 +362,57 @@ public final class SignedMessagesV2DAO {
} }
return out; return out;
} }
});
}
public List<SignedMessageV2Entry> listConversationPage(
String login,
String peerLogin,
long beforeTimeMs,
String beforeMessageKey,
int limit
) throws Exception {
try (Connection c = db.getConnection()) {
String sql = """
SELECT
message_key, base_key, target_login, from_login, to_login,
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
raw_block, created_at_ms, source_api, origin_session_id,
receipt_ref_base_key, receipt_ref_type, read_at_ms
FROM signed_messages_v2
WHERE target_login = ? COLLATE NOCASE
AND message_type IN (1, 2)
AND (
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
)
AND (
? <= 0
OR time_ms < ?
OR (time_ms = ? AND (? = '' OR message_key < ?))
)
ORDER BY time_ms DESC, message_key DESC
LIMIT ?
""";
List<SignedMessageV2Entry> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, login);
ps.setString(2, login);
ps.setString(3, peerLogin);
ps.setString(4, peerLogin);
ps.setString(5, login);
ps.setLong(6, beforeTimeMs);
ps.setLong(7, beforeTimeMs);
ps.setLong(8, beforeTimeMs);
ps.setString(9, beforeMessageKey == null ? "" : beforeMessageKey);
ps.setString(10, beforeMessageKey == null ? "" : beforeMessageKey);
ps.setInt(11, limit);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) out.add(mapRow(rs));
}
}
return out;
}
} }
private void upsertMessage(Connection c, SignedMessageV2Entry e) throws SQLException { private void upsertMessage(Connection c, SignedMessageV2Entry e) throws SQLException {
@@ -333,8 +421,8 @@ public final class SignedMessagesV2DAO {
message_key, base_key, target_login, from_login, to_login, message_key, base_key, target_login, from_login, to_login,
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms, time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
raw_block, created_at_ms, source_api, origin_session_id, raw_block, created_at_ms, source_api, origin_session_id,
receipt_ref_base_key, receipt_ref_type receipt_ref_base_key, receipt_ref_type, read_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(message_key) DO UPDATE SET ON CONFLICT(message_key) DO UPDATE SET
base_key = excluded.base_key, base_key = excluded.base_key,
target_login = excluded.target_login, target_login = excluded.target_login,
@@ -350,7 +438,8 @@ public final class SignedMessagesV2DAO {
source_api = excluded.source_api, source_api = excluded.source_api,
origin_session_id = excluded.origin_session_id, origin_session_id = excluded.origin_session_id,
receipt_ref_base_key = excluded.receipt_ref_base_key, receipt_ref_base_key = excluded.receipt_ref_base_key,
receipt_ref_type = excluded.receipt_ref_type receipt_ref_type = excluded.receipt_ref_type,
read_at_ms = COALESCE(signed_messages_v2.read_at_ms, excluded.read_at_ms)
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
bindSignedMessage(ps, e); bindSignedMessage(ps, e);
@@ -358,6 +447,32 @@ public final class SignedMessagesV2DAO {
} }
} }
private void markMessageReadByReceipt(Connection c, SignedMessageV2Entry entry) throws SQLException {
if (entry == null) return;
int messageType = entry.getMessageType();
if (messageType != 3 && messageType != 4) return;
String receiptRefBaseKey = String.valueOf(entry.getReceiptRefBaseKey() == null ? "" : entry.getReceiptRefBaseKey()).trim();
if (receiptRefBaseKey.isEmpty()) return;
long readAtMs = entry.getTimeMs();
if (readAtMs <= 0) return;
try (PreparedStatement ps = c.prepareStatement("""
UPDATE signed_messages_v2
SET read_at_ms = CASE
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
WHEN read_at_ms > ? THEN ?
ELSE read_at_ms
END
WHERE base_key = ?
AND message_type IN (1, 2)
""")) {
ps.setLong(1, readAtMs);
ps.setLong(2, readAtMs);
ps.setLong(3, readAtMs);
ps.setString(4, receiptRefBaseKey);
ps.executeUpdate();
}
}
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException { private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
String sql = """ String sql = """
SELECT revision_time_ms, reencrypted_at_ms SELECT revision_time_ms, reencrypted_at_ms
@@ -536,8 +651,8 @@ public final class SignedMessagesV2DAO {
message_key, base_key, target_login, from_login, to_login, message_key, base_key, target_login, from_login, to_login,
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms, time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
raw_block, created_at_ms, source_api, origin_session_id, raw_block, created_at_ms, source_api, origin_session_id,
receipt_ref_base_key, receipt_ref_type receipt_ref_base_key, receipt_ref_type, read_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
bindSignedMessage(ps, e); bindSignedMessage(ps, e);
@@ -563,6 +678,8 @@ public final class SignedMessagesV2DAO {
ps.setString(15, e.getReceiptRefBaseKey()); ps.setString(15, e.getReceiptRefBaseKey());
if (e.getReceiptRefType() == null) ps.setObject(16, null); if (e.getReceiptRefType() == null) ps.setObject(16, null);
else ps.setInt(16, e.getReceiptRefType()); else ps.setInt(16, e.getReceiptRefType());
if (e.getReadAtMs() == null) ps.setObject(17, null);
else ps.setLong(17, e.getReadAtMs());
} }
private void bindObjects(PreparedStatement ps, Object... bindValues) throws SQLException { private void bindObjects(PreparedStatement ps, Object... bindValues) throws SQLException {
@@ -586,6 +703,45 @@ public final class SignedMessagesV2DAO {
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key"); return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
} }
private boolean isBusyLock(SQLException ex) {
Throwable current = ex;
while (current != null) {
String msg = String.valueOf(current.getMessage()).toLowerCase();
if (msg.contains("sqlite_busy") || msg.contains("database is locked") || msg.contains("database table is locked")) {
return true;
}
current = current.getCause();
}
return false;
}
private void sleepBeforeBusyRetry(int attempt) throws SQLException {
long delayMs = SQLITE_BUSY_RETRY_BASE_DELAY_MS * (1L << Math.min(attempt, 4));
try {
Thread.sleep(delayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
SQLException sqlEx = new SQLException("Interrupted while retrying SQLite busy lock", ie);
throw sqlEx;
}
}
private <T> T withBusyRetry(SqlWork<T> work) throws Exception {
SQLException lastBusy = null;
for (int attempt = 0; attempt < SQLITE_BUSY_MAX_RETRIES; attempt++) {
try {
return work.run();
} catch (SQLException ex) {
if (!isBusyLock(ex) || attempt >= SQLITE_BUSY_MAX_RETRIES - 1) {
throw ex;
}
lastBusy = ex;
sleepBeforeBusyRetry(attempt);
}
}
throw lastBusy == null ? new SQLException("SQLite busy retry failed") : lastBusy;
}
private int compareMarkers(RevisionMarker left, RevisionMarker right) { private int compareMarkers(RevisionMarker left, RevisionMarker right) {
int revisionCompare = Long.compare(left.revisionTimeMs, right.revisionTimeMs); int revisionCompare = Long.compare(left.revisionTimeMs, right.revisionTimeMs);
if (revisionCompare != 0) return revisionCompare; if (revisionCompare != 0) return revisionCompare;
@@ -611,6 +767,8 @@ public final class SignedMessagesV2DAO {
e.setReceiptRefBaseKey(rs.getString("receipt_ref_base_key")); e.setReceiptRefBaseKey(rs.getString("receipt_ref_base_key"));
int maybeRefType = rs.getInt("receipt_ref_type"); int maybeRefType = rs.getInt("receipt_ref_type");
e.setReceiptRefType(rs.wasNull() ? null : maybeRefType); e.setReceiptRefType(rs.wasNull() ? null : maybeRefType);
long maybeReadAt = rs.getLong("read_at_ms");
e.setReadAtMs(rs.wasNull() ? null : maybeReadAt);
return e; return e;
} }
@@ -619,4 +777,9 @@ public final class SignedMessagesV2DAO {
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs()); return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
} }
} }
@FunctionalInterface
private interface SqlWork<T> {
T run() throws Exception;
}
} }
@@ -17,6 +17,7 @@ public class SignedMessageV2Entry {
private String originSessionId; private String originSessionId;
private String receiptRefBaseKey; private String receiptRefBaseKey;
private Integer receiptRefType; private Integer receiptRefType;
private Long readAtMs;
public String getMessageKey() { return messageKey; } public String getMessageKey() { return messageKey; }
public void setMessageKey(String messageKey) { this.messageKey = messageKey; } public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
@@ -50,4 +51,6 @@ public class SignedMessageV2Entry {
public void setReceiptRefBaseKey(String receiptRefBaseKey) { this.receiptRefBaseKey = receiptRefBaseKey; } public void setReceiptRefBaseKey(String receiptRefBaseKey) { this.receiptRefBaseKey = receiptRefBaseKey; }
public Integer getReceiptRefType() { return receiptRefType; } public Integer getReceiptRefType() { return receiptRefType; }
public void setReceiptRefType(Integer receiptRefType) { this.receiptRefType = receiptRefType; } public void setReceiptRefType(Integer receiptRefType) { this.receiptRefType = receiptRefType; }
public Long getReadAtMs() { return readAtMs; }
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
} }
@@ -91,6 +91,7 @@ import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler; import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler; import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler; import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler; import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler; import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler; import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
@@ -102,6 +103,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_R
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request; import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
@@ -200,6 +202,7 @@ public final class JsonHandlerRegistry {
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()), Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()), Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()), Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()), Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()), Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
Map.entry("CallSignalToSession", new Net_CallSignalToSession_Handler()), Map.entry("CallSignalToSession", new Net_CallSignalToSession_Handler()),
@@ -280,6 +283,7 @@ public final class JsonHandlerRegistry {
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class), Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class), Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class), Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class), Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class), Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
Map.entry("CallSignalToSession", Net_CallSignalToSession_Request.class), Map.entry("CallSignalToSession", Net_CallSignalToSession_Request.class),
@@ -10,7 +10,6 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
import server.logic.ws_protocol.JSON.messages.SignedMessagesRealtime;
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils; import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
@@ -51,8 +50,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
private static final long CLOSE_AFTER_ERROR_DELAY_MS = 75L; private static final long CLOSE_AFTER_ERROR_DELAY_MS = 75L;
private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L;
public static final long ALLOWED_SKEW_MS = 30_000L; public static final long ALLOWED_SKEW_MS = 30_000L;
@Override @Override
@@ -424,7 +421,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().register(ctx); ActiveConnectionsRegistry.getInstance().register(ctx);
SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS);
// --- формируем ответ --- // --- формируем ответ ---
Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response(); Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
@@ -10,7 +10,6 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
import server.logic.ws_protocol.JSON.messages.SignedMessagesRealtime;
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils; import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
@@ -44,8 +43,6 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
private static final long ALLOWED_SKEW_MS = 30_000L; private static final long ALLOWED_SKEW_MS = 30_000L;
private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L;
@Override @Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq; Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
@@ -302,7 +299,6 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().register(ctx); ActiveConnectionsRegistry.getInstance().register(ctx);
SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS);
// ответ // ответ
Net_SessionLogin_Response resp = new Net_SessionLogin_Response(); Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
@@ -268,7 +268,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
} }
// Репосты временно отключены до будущей реализации. // Репосты временно отключены до будущей реализации.
// Точка возврата: Dev_Docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md // Точка возврата: docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md
if ((block.type & 0xFFFF) == 1 if ((block.type & 0xFFFF) == 1
&& (block.subType & 0xFFFF) == (MsgSubType.TEXT_REPOST & 0xFFFF)) { && (block.subType & 0xFFFF) == (MsgSubType.TEXT_REPOST & 0xFFFF)) {
log.warn("AddBlock: repost_disabled (login={}, blockchainName={}, blockNumber={})", log.warn("AddBlock: repost_disabled (login={}, blockchainName={}, blockNumber={})",
@@ -0,0 +1,98 @@
package server.logic.ws_protocol.JSON.messages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SignedMessagesV2DAO;
import shine.db.entities.SignedMessageV2Entry;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_GetDirectMessages_Handler.class);
private static final int DEFAULT_LIMIT = 50;
private static final int MAX_LIMIT = 200;
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_GetDirectMessages_Request req = (Net_GetDirectMessages_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
if (req.getPeerLogin() == null || req.getPeerLogin().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "peerLogin обязателен");
}
int limit = req.getLimit() == null ? DEFAULT_LIMIT : req.getLimit();
if (limit <= 0 || limit > MAX_LIMIT) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LIMIT", "limit должен быть в диапазоне 1.." + MAX_LIMIT);
}
String login = ctx.getLogin().trim();
String peerLogin = req.getPeerLogin().trim();
long beforeTimeMs = req.getBeforeTimeMs() == null ? 0L : req.getBeforeTimeMs();
String beforeMessageKey = req.getBeforeMessageKey() == null ? "" : req.getBeforeMessageKey().trim();
try {
List<SignedMessageV2Entry> page = SignedMessagesV2DAO.getInstance().listConversationPage(
login,
peerLogin,
beforeTimeMs,
beforeMessageKey,
limit + 1
);
boolean hasMore = page.size() > limit;
if (hasMore) {
page = new ArrayList<>(page.subList(0, limit));
}
Net_GetDirectMessages_Response resp = new Net_GetDirectMessages_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setLogin(login);
resp.setPeerLogin(peerLogin);
resp.setLimit(limit);
resp.setHasMore(hasMore);
List<Net_GetDirectMessages_Response.MessageItem> items = new ArrayList<>();
for (SignedMessageV2Entry entry : page) {
Net_GetDirectMessages_Response.MessageItem item = new Net_GetDirectMessages_Response.MessageItem();
item.setMessageKey(entry.getMessageKey());
item.setBaseKey(entry.getBaseKey());
item.setFromLogin(entry.getFromLogin());
item.setToLogin(entry.getToLogin());
item.setMessageType(entry.getMessageType());
item.setTimeMs(entry.getTimeMs());
item.setNonce(entry.getNonce());
item.setRevisionTimeMs(entry.getRevisionTimeMs());
item.setReencryptedAtMs(entry.getReencryptedAtMs());
item.setCreatedAtMs(entry.getCreatedAtMs());
item.setReadAtMs(entry.getReadAtMs());
item.setBlobB64(Base64.getEncoder().encodeToString(entry.getRawBlock()));
items.add(item);
}
resp.setMessages(items);
if (hasMore && !items.isEmpty()) {
Net_GetDirectMessages_Response.MessageItem last = items.get(items.size() - 1);
resp.setNextBeforeTimeMs(last.getTimeMs());
resp.setNextBeforeMessageKey(last.getMessageKey());
}
return resp;
} catch (Exception e) {
log.error("GetDirectMessages failed for login={} peerLogin={}", login, peerLogin, e);
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
}
}
}
@@ -50,12 +50,20 @@ public final class SignedMessagesRealtime {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
for (String targetLogin : targetLoginsForMessage(message)) { for (String targetLogin : targetLoginsForMessage(message)) {
List<ActiveSessionEntry> sessions = ActiveSessionsDAO.getInstance().getByLogin(targetLogin); List<ActiveSessionEntry> sessions = ActiveSessionsDAO.getInstance().getByLogin(targetLogin);
List<String> sessionIdsToTrack = new ArrayList<>();
for (ActiveSessionEntry s : sessions) {
String sessionId = s.getSessionId();
if (excludeSessionId != null && excludeSessionId.equals(sessionId)) {
continue;
}
sessionIdsToTrack.add(sessionId);
}
SignedMessagesV2DAO.getInstance().ensureDeliveryRows(message.getMessageKey(), sessionIdsToTrack, now);
for (ActiveSessionEntry s : sessions) { for (ActiveSessionEntry s : sessions) {
String sessionId = s.getSessionId(); String sessionId = s.getSessionId();
if (excludeSessionId != null && excludeSessionId.equals(sessionId)) { if (excludeSessionId != null && excludeSessionId.equals(sessionId)) {
continue; continue;
} }
SignedMessagesV2DAO.getInstance().ensureDeliveryRow(message.getMessageKey(), sessionId, now);
boolean deliveredOnline = sendEventToSessionIfOnline(sessionId, targetLogin, message, false); boolean deliveredOnline = sendEventToSessionIfOnline(sessionId, targetLogin, message, false);
if (deliveredOnline) { if (deliveredOnline) {
counters.wsDelivered++; counters.wsDelivered++;
@@ -0,0 +1,19 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_GetDirectMessages_Request extends Net_Request {
private String peerLogin;
private Integer limit;
private Long beforeTimeMs;
private String beforeMessageKey;
public String getPeerLogin() { return peerLogin; }
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
public Integer getLimit() { return limit; }
public void setLimit(Integer limit) { this.limit = limit; }
public Long getBeforeTimeMs() { return beforeTimeMs; }
public void setBeforeTimeMs(Long beforeTimeMs) { this.beforeTimeMs = beforeTimeMs; }
public String getBeforeMessageKey() { return beforeMessageKey; }
public void setBeforeMessageKey(String beforeMessageKey) { this.beforeMessageKey = beforeMessageKey; }
}
@@ -0,0 +1,71 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
public class Net_GetDirectMessages_Response extends Net_Response {
private String login;
private String peerLogin;
private int limit;
private boolean hasMore;
private Long nextBeforeTimeMs;
private String nextBeforeMessageKey;
private List<MessageItem> messages = new ArrayList<>();
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public String getPeerLogin() { return peerLogin; }
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
public int getLimit() { return limit; }
public void setLimit(int limit) { this.limit = limit; }
public boolean isHasMore() { return hasMore; }
public void setHasMore(boolean hasMore) { this.hasMore = hasMore; }
public Long getNextBeforeTimeMs() { return nextBeforeTimeMs; }
public void setNextBeforeTimeMs(Long nextBeforeTimeMs) { this.nextBeforeTimeMs = nextBeforeTimeMs; }
public String getNextBeforeMessageKey() { return nextBeforeMessageKey; }
public void setNextBeforeMessageKey(String nextBeforeMessageKey) { this.nextBeforeMessageKey = nextBeforeMessageKey; }
public List<MessageItem> getMessages() { return messages; }
public void setMessages(List<MessageItem> messages) { this.messages = messages; }
public static class MessageItem {
private String messageKey;
private String baseKey;
private String fromLogin;
private String toLogin;
private int messageType;
private long timeMs;
private long nonce;
private long revisionTimeMs;
private long reencryptedAtMs;
private long createdAtMs;
private Long readAtMs;
private String blobB64;
public String getMessageKey() { return messageKey; }
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
public String getBaseKey() { return baseKey; }
public void setBaseKey(String baseKey) { this.baseKey = baseKey; }
public String getFromLogin() { return fromLogin; }
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
public String getToLogin() { return toLogin; }
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
public int getMessageType() { return messageType; }
public void setMessageType(int messageType) { this.messageType = messageType; }
public long getTimeMs() { return timeMs; }
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
public long getNonce() { return nonce; }
public void setNonce(long nonce) { this.nonce = nonce; }
public long getRevisionTimeMs() { return revisionTimeMs; }
public void setRevisionTimeMs(long revisionTimeMs) { this.revisionTimeMs = revisionTimeMs; }
public long getReencryptedAtMs() { return reencryptedAtMs; }
public void setReencryptedAtMs(long reencryptedAtMs) { this.reencryptedAtMs = reencryptedAtMs; }
public long getCreatedAtMs() { return createdAtMs; }
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
public Long getReadAtMs() { return readAtMs; }
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
public String getBlobB64() { return blobB64; }
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
}
}
@@ -44,7 +44,7 @@ webpush.vapid.subject=mailto:admin@shine.local
# Тогда сервер будет выдавать временный username/password (TTL). # Тогда сервер будет выдавать временный username/password (TTL).
# ------------------------------------------------------------ # ------------------------------------------------------------
call.ice.stun.urls=stun:stun.l.google.com:19302 call.ice.stun.urls=stun:stun.l.google.com:19302
call.ice.turn.urls=turn:185.229.109.118:3478?transport=udp,turn:185.229.109.118:3478?transport=tcp call.ice.turn.urls=turn:turn1.shineup.me:3478?transport=udp,turn:turn1.shineup.me:3478?transport=tcp
call.ice.turn.ttlSec=600 call.ice.turn.ttlSec=600
call.ice.turn.userPrefix=shine call.ice.turn.userPrefix=shine
call.ice.turn.sharedSecret= call.ice.turn.sharedSecret=
@@ -58,12 +58,24 @@ call.ice.turn.password=
# Каждый блок описывает один TURN-узел. Новые узлы добавляются по индексу. # Каждый блок описывает один TURN-узел. Новые узлы добавляются по индексу.
# Приоритет авторизации на узел: sharedSecret -> статические username/password. # Приоритет авторизации на узел: sharedSecret -> статические username/password.
# ------------------------------------------------------------ # ------------------------------------------------------------
call.ice.turn.servers.1.id=shineup-main-185 call.ice.turn.servers.1.id=turn1
call.ice.turn.servers.1.urls=turn:185.229.109.118:3478?transport=udp,turn:185.229.109.118:3478?transport=tcp call.ice.turn.servers.1.urls=turn:turn1.shineup.me:3478?transport=udp,turn:turn1.shineup.me:3478?transport=tcp
call.ice.turn.servers.1.sharedSecret=def6d444734d380d2f67a9d345b1debf985eaba0973c343e392c060d97c30106 call.ice.turn.servers.1.sharedSecret=
call.ice.turn.servers.1.username= call.ice.turn.servers.1.username=
call.ice.turn.servers.1.password= call.ice.turn.servers.1.password=
call.ice.turn.servers.2.id=turn2
call.ice.turn.servers.2.urls=turn:turn2.shineup.me:3478?transport=udp,turn:turn2.shineup.me:3478?transport=tcp
call.ice.turn.servers.2.sharedSecret=
call.ice.turn.servers.2.username=
call.ice.turn.servers.2.password=
call.ice.turn.servers.3.id=turn3
call.ice.turn.servers.3.urls=turn:turn3.shineup.me:3478?transport=udp,turn:turn3.shineup.me:3478?transport=tcp
call.ice.turn.servers.3.sharedSecret=
call.ice.turn.servers.3.username=
call.ice.turn.servers.3.password=
# ------------------------------------------------------------ # ------------------------------------------------------------
# Временные debug HTTP API для тестирования соединений # Временные debug HTTP API для тестирования соединений
# true - endpoint'ы /debug/ws/* включены (только при наличии .debug-token) # true - endpoint'ы /debug/ws/* включены (только при наличии .debug-token)
@@ -1,220 +0,0 @@
# Задача 01: Доработка вкладки «Каналы» (UI + API)
## Кратко и по делу
Нужно довести вторую вкладку «Каналы» до полностью рабочего состояния на реальных данных сервера.
Что должно работать:
- список каналов;
- вход в канал и чтение сообщений;
- вход в тред сообщения (история/ветка);
- ответ на сообщение;
- лайк/снятие лайка;
- подписка на пользователя;
- подписка на канал;
- видимое имя канала в формате `имя_пользователя/имя_канала`.
Запись любых новых сущностей делается через `AddBlock` с подписью на клиенте.
Чтение делается через 3 API:
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
Техническая особенность (оставляем как есть):
- на экране каналов индикатор непрочитанного = общее число сообщений канала.
---
## Подробное ТЗ
### 1. Цель
Сделать рабочий каналовый сценарий «от списка до треда», где чтение строится на RPC API, а запись действий пользователя — только через `AddBlock`.
### 2. Что уже есть в проекте
#### 2.1 UI (частично)
- Есть страницы:
- `channels-list`
- `channel-view`
- `add-channel-view`
- Есть запросы чтения в клиенте:
- `authService.listSubscriptionsFeed(...)`
- `authService.getChannelMessages(...)`
- `authService.getMessageThread(...)`
- Есть fallback на mock-данные при ошибках сервера.
#### 2.2 API/сервер (уже реализованы)
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
- `AddBlock`
#### 2.3 Тесты
- Есть интеграционный тест API каналов: `IT_06_ChannelsApi`.
- Есть тесты генерации блоков каналов/связей: `IT_03_AddBlock_NoAuth`.
- Формат `AddBlock` и его сборка/подпись описаны в `AddBlockSender`.
### 3. Проблемы текущей реализации (что надо закрыть)
- Кнопки «подписаться на человека/канал» в списке каналов сейчас UI-only (модалка без реальной записи через `AddBlock`).
- `add-channel-view` пока не создает канал на сервере через `AddBlock` (`CreateChannelBody`), только делает `navigate`.
- `channel-view` добавляет пост локально (в память), а не отправляет блок `TEXT_POST` через `AddBlock`.
- Нет полноценного экрана треда сообщения с реальными `GetMessageThread` и действиями `ответить/лайк/убрать лайк` через блоки.
- Нет гарантированного отображения канала в требуемом формате `ownerLogin/channelName`.
### 4. Функциональные требования
#### 4.1 Список каналов
На вкладке «Каналы» отображать 3 группы:
- Мои каналы
- Каналы пользователей, на кого я подписан
- Каналы, на которые я подписан
Источник данных: `ListSubscriptionsFeed`.
Каждый канал показывать в формате:
- `ownerLogin/channelName`
#### 4.2 Открытие канала
При входе в канал:
- загрузить сообщения через `GetChannelMessages`;
- показать список сообщений в хронологическом порядке (по текущему параметру `sort`);
- оставить техническую особенность непрочитанных как есть.
#### 4.3 Открытие треда сообщения
При клике на сообщение:
- загрузить тред через `GetMessageThread`;
- показать `ancestors`, `focus`, `descendants`;
- из треда должны быть доступны действия:
- «Ответить»
- «Лайк»
- «Убрать лайк»
Запись действий — только `AddBlock`.
#### 4.4 Создание канала
В `add-channel-view` кнопка «Создать» должна:
- отправлять `AddBlock` с телом `CreateChannelBody`;
- после успеха возвращать к списку каналов и обновлять его.
#### 4.5 Подписки
- Подписка на пользователя: `AddBlock` с `ConnectionBody` подтип `CONNECTION_FOLLOW`, target = HEADER пользователя.
- Подписка на канал: `AddBlock` с `ConnectionBody` подтип `CONNECTION_FOLLOW`, target = root блока канала (`CreateChannelBody` или HEADER для канала `0`).
### 5. API (форматы)
## 5.1 ListSubscriptionsFeed (чтение)
Request:
```json
{
"op": "ListSubscriptionsFeed",
"requestId": "...",
"payload": {
"login": "A1",
"limit": 200
}
}
```
Response (смысловые поля):
- `ownedChannels[]`
- `followedUsersChannels[]`
- `followedChannels[]`
---
## 5.2 GetChannelMessages (чтение)
Request:
```json
{
"op": "GetChannelMessages",
"requestId": "...",
"payload": {
"channel": {
"ownerBlockchainName": "A1-001",
"channelRootBlockNumber": 0,
"channelRootBlockHash": ""
},
"limit": 200,
"sort": "asc"
}
}
```
Response (смысловые поля):
- `channel`
- `messages[]`
---
## 5.3 GetMessageThread (чтение)
Request:
```json
{
"op": "GetMessageThread",
"requestId": "...",
"payload": {
"message": {
"blockchainName": "A1-001",
"blockNumber": 15,
"blockHash": "..."
},
"depthUp": 20,
"depthDown": 2,
"limitChildrenPerNode": 50
}
}
```
Response (смысловые поля):
- `ancestors[]`
- `focus`
- `descendants[]`
---
## 5.4 AddBlock (запись)
Любое изменение (создать канал, пост, reply, реакция, подписка) записывается через:
```json
{
"op": "AddBlock",
"requestId": "...",
"payload": {
"blockchainName": "A1-001",
"blockNumber": 6,
"prevBlockHash": "<64-hex>",
"blockBytesB64": "<base64 full block>"
}
}
```
Важно:
- `blockBytesB64` формируется на клиенте.
- Подпись блока формируется на клиенте приватным blockchain key пользователя.
- Перед добавлением блока клиент берет актуальный курсор цепочки с сервера.
### 6. Типы блоков для каналов и связей (через AddBlock)
- Создание канала: `CreateChannelBody`
- Пост/ответ: `TextBody` (`TEXT_POST`, `TEXT_REPLY`)
- Реакции: `ReactionBody` (лайк/снятие лайка)
- Подписки: `ConnectionBody` (`CONNECTION_FOLLOW`)
### 7. Критерии приемки
- Список каналов отображается с реальными данными API.
- Формат названия канала в UI: `ownerLogin/channelName`.
- Создание канала реально пишет блок и канал появляется после обновления.
- Отправка поста/ответа/реакций реально пишет блок и видна после перечитки API.
- Подписка на пользователя/канал реально пишет блок и отражается в выдаче.
- Переход в тред сообщения показывает реальные `ancestors/focus/descendants`.
- Непрочитанные в списке каналов = общее число сообщений (временное правило).
### 8. Локальный запуск (уже сделано)
Команда:
```bash
./gradlew startLocal
```
Что делает:
- чистит логи;
- билдит сервер;
- запускает локальный WS сервер;
- запускает локальный HTTP сервер клиента;
- открывает браузер по URL с параметром `localWsPort`.
@@ -1,25 +0,0 @@
# Краткое описание задачи
Нужно сделать полностью рабочую вкладку «Каналы» в SHiNE.
Пользователь должен:
- видеть список каналов;
- открывать канал и читать сообщения;
- открывать тред сообщения;
- отвечать, ставить и убирать лайк;
- подписываться на пользователей и каналы.
Чтение данных идет через 3 API:
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
Все действия записи делаются только через `AddBlock` с подписью на клиенте.
Формат имени канала в интерфейсе:
- `имя_пользователя/имя_канала`
Локальный запуск проекта:
```bash
./gradlew startLocal
```
@@ -1,153 +0,0 @@
# Задача 02: Web Push + подписанный API отправки личных сообщений
## Контекст (по текущему состоянию проекта)
- Уже есть JSON WebSocket API для личных сообщений: `SendDirectMessage`, `AckIncomingMessage`, `UpsertPushToken`.
- Сейчас серверный fallback-пуш реализован через FCM (`FcmPushSender`) и ключ `fcm.server.key`.
- Клиент уже регистрирует service worker и токен Firebase, затем аплоадит push token на сервер.
## Цель
Добавить полностью рабочий сценарий доставки личных сообщений с приоритетом:
1) онлайн-доставка в активную WebSocket-сессию;
2) если не подтверждено — Web Push;
3) поддержать отдельный API отправки без авторизации, где доступ проверяется цифровой подписью Ed25519 по `clientKey` отправителя.
---
## Предварительная спецификация подписанного пакета (v1)
> ВАЖНО: финально фиксируется после уточнений по endian/кодировкам/лимитам.
Пакет (binary):
1. `prefix` — ASCII-константа, например `SHINE_MESSAGE`.
2. `toLoginLen` — 1 байт.
3. `toLogin` — ASCII, длина = `toLoginLen`.
4. `fromLoginLen` — 1 байт.
5. `fromLogin` — ASCII, длина = `fromLoginLen`.
6. `timeMs` — 8 байт (unix ms).
7. `nonce32` — 4 байта случайное число.
8. `messageType` — 4 байта.
9. `targetMode` — 1 байт:
- `0` = всем сессиям пользователя,
- `1` = конкретной сессии.
10. Если `targetMode=1`:
- `sessionIdLen` — 1 байт,
- `sessionId` — ASCII.
11. `messageLen` — 2 байта.
12. `messageBytes` — бинарные данные длиной `messageLen`.
13. `signature64` — 64 байта, Ed25519 подпись всего блока **без** `signature64`.
Ограничения (первичный draft):
- общий размер пакета ≤ 4000 байт;
- логины/префикс/идентификатор сессии — ASCII;
- повторы отсекаются по `(fromLogin, timeMs, nonce32)` в окне TTL.
---
## Сервер: что доработать
### 1) Новый endpoint без авторизации
Операция (через WS JSON обертку) условно `SendSignedDirectMessage`:
- принимает пакет (base64 binary blob);
- парсит и валидирует формат;
- достает `fromLogin`, поднимает `clientKey` пользователя;
- проверяет подпись Ed25519;
- проверяет анти-replay (time window + nonce);
- отправляет сообщение по правилам маршрутизации;
- пишет результат (messageId, каналы доставки, причины недоставки).
### 2) Маршрутизация доставки
Для `targetMode=1`:
- если целевая сессия онлайн и ACK пришел вовремя — успех;
- иначе отправка в Web Push этой сессии (если есть subscription).
Для `targetMode=0`:
- обход всех сессий пользователя;
- сначала online delivery + ACK;
- для непринятых/офлайн — Web Push по соответствующим subscription;
- если subscription отсутствует — тихий skip.
### 3) Миграция от FCM к Web Push
- добавить конфиг VAPID (`webpush.public.key`, `webpush.private.key`, `webpush.subject`);
- хранить на сервере не только token, а web-push subscription (endpoint + keys);
- сделать отправщик Web Push и заменить/расширить текущий `FcmPushSender`.
### 4) Безопасность
- строгая ASCII-валидация логинов/sessionId;
- лимиты длины всех полей;
- rate limit на endpoint;
- audit-лог неуспешных проверок подписи/формата;
- защита от replay.
---
## Клиент (shine-UI): что доработать
1. Перейти на стандартный Web Push flow:
- регистрация service worker;
- `PushManager.subscribe(...)` с VAPID public key;
- отправка subscription на сервер (`UpsertPushSubscription` или расширение `UpsertPushToken`).
2. Service worker:
- `push` handler получает payload целиком;
- показывает системное уведомление;
- при клике открывает/фокусирует нужный чат.
3. Online-сообщения:
- сохранить текущий event-канал `IncomingDirectMessage`;
- обязателен ACK (`AckIncomingMessage` уже есть).
4. Keep-alive:
- UI отправляет `Ping` раз в 60 секунд при активной сессии.
---
## Документация
Сделать отдельный документ настройки Web Push:
- как сгенерировать VAPID ключи;
- какие параметры прописать на сервере и в UI;
- как проверить локально e2e (онлайн + офлайн пуш);
- ограничения payload и рекомендации по ретраям.
---
## Этапы реализации (предложение)
1. Зафиксировать бинарный формат + валидации.
2. Реализовать серверный parser/validator/signature verify/replay guard.
3. Реализовать Web Push sender + storage subscription.
4. Подключить новый endpoint и маршрутизацию доставки.
5. Обновить UI (subscription + service worker + ping timer).
6. Добавить интеграционные тесты (online ACK / offline push / bad signature / replay / oversize).
7. Добавить документацию.
---
## Что нужно уточнить до разработки
1. Endian для `timeMs/nonce/messageType/messageLen` (big-endian или little-endian).
2. Что именно подписывается: строго весь префикс..messageBytes (без подписи) — подтвердить.
3. Диапазон допустимых `messageType`.
4. TTL окна для анти-replay (например 5 минут / 15 минут).
5. Лимиты длин для login/session/message.
6. Можно ли временно оставить FCM как fallback, пока не готов Web Push в проде.
7. Формат сообщения в `messageBytes`: opaque bytes или UTF-8 строка.
## Статус реализации (12.04.2026)
### Что уже внедрено в коде
- `SendDirectMessage` переведён на signed-binary payload (`blobB64`) без обязательной авторизации WS-сессии.
- Внедрён бинарный парсер пакета формата `SHiNE_msg + version(1) + ... + signature64`.
- Проверка подписи Ed25519 делается по `clientKey` отправителя через `shine-server-crypto` (`Ed25519Util`).
- Добавлен anti-replay guard `(from_login, time_ms, nonce)` с TTL 15 минут.
- Добавлено историческое хранилище `signed_direct_messages_history` с сырым пакетом `raw_packet`.
- Логика доставки: сначала WS+ACK, затем fallback на Web Push (по подписке конкретной session).
- Поле типа сообщения переведено на `uint16`, пока поддерживается только `1`.
- Для `targetMode=1` при несуществующей сессии возвращается `success` с `sessionNotFound=true` и `delivered=0`.
- UI переведён с Firebase/FCM на браузерный `PushManager.subscribe` + Service Worker `push`.
- Добавлен keep-alive ping из UI раз в 60 секунд при авторизованной сессии.
### Что настроить в окружении
- В `application.properties` задать:
- `webpush.vapid.public`
- `webpush.vapid.private`
- `webpush.vapid.subject`
- В `shine-UI/index.html` задать публичный VAPID ключ в `window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__`.
+86
View File
@@ -0,0 +1,86 @@
# Third-party notices
This file lists third-party components and assets used by SHiNE. It is a practical attribution file for repository publication; individual dependency artifacts may include additional license text in their own packages.
## Twemoji
SHiNE UI uses Twemoji graphics for emoji rendering in the chat emoji picker and emoji-only messages.
- Source: https://github.com/twitter/twemoji
- Graphics license: Creative Commons Attribution 4.0 International (CC BY 4.0)
- Code license: MIT
- License text: https://creativecommons.org/licenses/by/4.0/
Attribution:
Emoji graphics by Twemoji, licensed under CC BY 4.0.
Changes in SHiNE:
- Emoji are stored and sent as normal Unicode text.
- Twemoji SVG graphics are used only as a visual rendering layer in the web UI.
- The UI loads Twemoji SVG assets from the pinned `twitter/twemoji@14.0.2` package via jsDelivr.
## QR Code Generator
SHiNE UI includes `shine-UI/js/vendor-qrcode-generator.js`.
- Project: QR Code Generator for JavaScript
- Copyright: Kazuhiko Arase
- License: MIT
- Source: http://www.d-project.com/
## SHiNE project-owned visual assets
SHiNE logos, icons and generated visual assets in `shine-UI/assets/` and `shine-UI/img/` are project-owned assets, unless a specific file says otherwise.
## Solana JavaScript libraries
SHiNE UI and Solana tooling use Solana JavaScript libraries, including `@solana/web3.js`.
- Package: `@solana/web3.js`
- License: MIT
- Source: https://github.com/solana-foundation/solana-web3.js
Some Solana JavaScript dependency trees include additional packages under permissive licenses such as MIT, Apache-2.0, BSD, ISC, 0BSD and CC0-1.0.
Known stricter dependency:
- Package: `rpc-websockets`
- License: LGPL-3.0-only
- Used transitively through Solana JavaScript dependencies in `shine-solana/shine` and `SHiNE-browser-plugin-wallet`.
## Noble cryptography libraries
SHiNE browser wallet/vendor bundles include Noble cryptography code.
- Packages: `@noble/curves`, `@noble/hashes`
- License: MIT
- Source: https://github.com/paulmillr/noble-curves and https://github.com/paulmillr/noble-hashes
## Java server dependencies
The SHiNE Java server uses third-party dependencies from Maven Central, including:
- Eclipse Jetty (`org.eclipse.jetty:*`) - EPL-2.0 / Apache-2.0 family licensing
- Bouncy Castle (`org.bouncycastle:bcprov-jdk18on`) - Bouncy Castle permissive license
- Jackson (`com.fasterxml.jackson.core:jackson-databind`) - Apache-2.0
- Logback (`ch.qos.logback:logback-classic`) - EPL-1.0 / LGPL-2.1
- SLF4J (`org.slf4j:slf4j-api`) - MIT
- SQLite JDBC (`org.xerial:sqlite-jdbc`) - Apache-2.0
- Web Push Java library (`nl.martijndwars:web-push`) - Apache-2.0
- JUnit (`org.junit:*`) - EPL-2.0
## Gradle Wrapper
The repository includes Gradle wrapper scripts.
- License: Apache License 2.0
- Source: https://gradle.org/
## Espressif code snippets
The ESP32 prototype area includes Espressif audio codec helper files with SPDX/license headers.
- Files include `ESPRESSIF MIT License` and `SPDX-License-Identifier: Apache-2.0` notices.
- Original copyright notices are preserved in the source files.
@@ -35,5 +35,5 @@
## Какие документы потом обновить ## Какие документы потом обновить
- `Deploy/`; - `deploy/`;
- `Dev_Docs/Blockchain/sync-between-servers.md`, если изменится поведение остановки/восстановления. - `docs/Blockchain/sync-between-servers.md`, если изменится поведение остановки/восстановления.
@@ -24,6 +24,6 @@ QR-подключение других устройств сейчас есть
## Какие документы потом обновить ## Какие документы потом обновить
- `Dev_Docs/Solana_Architecture/README.md`; - `docs/Solana_Architecture/README.md`;
- `TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md`; - `TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md`;
- `TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md`. - `TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md`.
+18 -3
View File
@@ -12,16 +12,30 @@
- откуда продолжать; - откуда продолжать;
- какие документы потом надо обновить. - какие документы потом надо обновить.
- Это не активная разработка. Тут только план и контекст. - Это не активная разработка. Тут только план и контекст.
- Старую папку `Dev_Docs/Future_Features/` считать архивной и больше не использовать как источник новых задач. - Старую папку `docs/Future_Features/` считать архивной и больше не использовать как источник новых задач.
## Текущие задачи ## Текущие задачи
- `2026-06-26_1800_корректное_завершение_за_30с.md` - дать сервису до 30 секунд на корректное завершение опасных операций перед рестартом. - `2026-06-26_1800_корректное_завершение_за_30с.md` - дать сервису до 30 секунд на корректное завершение опасных операций перед рестартом.
- `2026-06-26_1805_межсерверный_ws_и_dm_sync.md` - постоянный server-to-server WebSocket, push новых блоков и DM, ACK и backfill.
- `2026-06-26_1810_подключение_устройств_по_qr.md` - довести подключение других устройств по QR и перевести это в нормальные типизированные сессии. - `2026-06-26_1810_подключение_устройств_по_qr.md` - довести подключение других устройств по QR и перевести это в нормальные типизированные сессии.
- `2026-06-26_1815_esp32_файловое_хранилище.md` - использовать ESP32 как личное файловое хранилище для переписок и вложений. - `2026-06-26_1815_esp32_файловое_хранилище.md` - использовать ESP32 как личное файловое хранилище для переписок и вложений.
## Перенесённые планы из `Dev_Docs/Future_Features/` ## Децентрализация
Текущий production-режим SHiNE считается односерверным. Задачи по нескольким серверам, Arweave и realtime PDA/Solana sync вынесены в `Децентрализация/` и не блокируют выкладку текущей версии на GitHub.
- `Децентрализация/односерверный_production_режим.md` - границы текущей production-версии с одним сервером.
- `Децентрализация/запись_блокчейнов_в_arweave.md` - будущая запись/архивация блокчейнов в Arweave.
- `Децентрализация/realtime_pda_solana_sync.md` - будущая онлайн-синхронизация PDA и Solana.
- `Децентрализация/межсерверная_передача_сообщений.md` - будущая доставка сообщений между серверами.
- `Децентрализация/межсерверные_звонки.md` - будущая маршрутизация звонков между серверами.
- `Децентрализация/2026-06-26_1805_межсерверный_ws_и_dm_sync.md` - перенесённый старый план постоянного server-to-server WS и DM sync.
## Новые фишки которые надо доделать
- `Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/` - отложенная новая контентная модель блокчейна, не входящая в текущий односерверный production-релиз.
## Перенесённые планы из `docs/Future_Features/`
### near ### near
@@ -35,6 +49,7 @@
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений. - `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи. - `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать. - `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
- `medium/2026-07-22_переход_с_sqlite_на_postgresql.md` - подготовить перевод серверной БД с `SQLite` на `PostgreSQL` для более серьёзной конкурентной нагрузки и дальнейшего масштабирования.
### dao_запуск ### dao_запуск
@@ -1,64 +0,0 @@
# ESP32 как аппаратный кошелёк (device-сессия)
## Суть фичи
ESP32 становится аппаратным HSM (hardware security module): хранит ключи, постоянно подключён к SHiNE-серверу как device-сессия, подтверждает операции нажатием на экране. Другие устройства (браузер, телефон) взаимодействуют с ESP32 через сервер — без прямого соединения.
## Два ключевых сценария
### Сценарий 1 — Создание делегированной сессии
1. Браузер/телефон → сервер: «хочу делегированную сессию от имени пользователя X»
2. Сервер → ESP32 (device-сессия): «запрос на одобрение»
3. Пользователь нажимает «Да» на сенсорном экране ESP32
4. ESP32 → сервер: одобрено → сервер создаёт делегированную сессию для браузера
### Сценарий 2 — Подпись транзакции / блока
1. Браузер (через делегированную сессию) → сервер → ESP32: «подпиши вот это»
2. ESP32 показывает запрос на экране, пользователь подтверждает
3. ESP32 подписывает нужным ключом → ответ через сервер → браузер
## Что нужно сделать
### ESP32 (основная работа)
- [ ] Инициализация WiFi (SSID/пароль в NVS)
- [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером
- [ ] Авторизация на сервере: `AuthChallenge``CreateAuthSession` через `clientKey` (уже есть в NVS), сохранить `sessionId` в NVS
- [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу
- [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет
- [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх)
- [ ] Переподключение при разрыве (reconnect loop)
### Сервер (минимальные изменения)
- [ ] Добавить поле `sessionType` (`USER` / `DEVICE`) в таблицу `active_sessions`
- [ ] Новая операция `DeviceApprovalRequest` — браузер запрашивает одобрение у device-сессии
- [ ] Новая операция `DeviceApprovalResponse` — ESP32 отвечает (одобрено/отклонено)
- [ ] Новые операции `SignRequest` / `SignResponse` — запрос подписи и ответ
- [ ] Роутинг: при получении запроса найти device-сессию через `ActiveConnectionsRegistry.getByLogin(login)` + фильтр по `sessionType=DEVICE`, переслать туда
### Клиент (отдельный этап)
- [ ] Браузерное расширение или UI: создание делегированной сессии, отправка `SignRequest`
## Что уже готово (переиспользуем)
- **Роутинг сообщений**`SendDirectMessage` с `TARGET_ONE_SESSION` и `CallSignalToSession` уже умеют точечно доставлять в конкретный `sessionId`. Механизм готов, нужно добавить только новые op-коды поверх него.
- **Ed25519 на ESP32** — библиотека `<Ed25519.h>` уже используется в скетче. Подписи работают.
- **NVS** — уже хранит логин, мастер-секрет, 3 пары ключей. Расширяется легко.
- **`ActiveConnectionsRegistry`** — поиск по `login` и `sessionId` уже есть на сервере.
- **Аутентификация** — схема `AuthChallenge``CreateAuthSession` через Ed25519 уже полностью реализована.
## Оценка сложности
| Компонент | Сложность |
|---|---|
| ESP32: WiFi + WebSocket-клиент + авторизация | Средняя |
| ESP32: обработчик входящих + UI подтверждений | Средняя |
| Сервер: флаг sessionType + 4 новых op-а + роутинг | Низкая–средняя |
| Браузерное расширение | Высокая (отдельный этап) |
**Итого фазы ESP32 + сервер: ~11.5 недели.**
## С чего начинать
1. Серверная часть проще и быстрее — начать с добавления `sessionType` и `DeviceApprovalRequest/Response`.
2. Затем ESP32: WiFi → WebSocket → авторизация → обработчик входящих → UI.
3. Браузерное расширение — отдельная итерация после того как ESP32 + сервер работают.
@@ -104,9 +104,9 @@
## Какие документы нужно будет обновить при реализации ## Какие документы нужно будет обновить при реализации
- `Dev_Docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд. - `docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд.
- `Dev_Docs/API/` если изменится публичный серверный API или появятся новые операции. - `docs/API/` если изменится публичный серверный API или появятся новые операции.
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий. - `docs/Personal_Messages/Протокол_DM_v1.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий.
- Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве. - Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве.
## С какого места продолжать позже ## С какого места продолжать позже
@@ -56,11 +56,9 @@
- Код формирования репоста в `auth-service.js` не удалён: его можно будет использовать как основу при возвращении к задаче. - Код формирования репоста в `auth-service.js` не удалён: его можно будет использовать как основу при возвращении к задаче.
- Код отображения target-полей и перехода к оригиналу не удалён: он нужен для будущей проверки и возможной совместимости с уже созданными тестовыми блоками. - Код отображения target-полей и перехода к оригиналу не удалён: он нужен для будущей проверки и возможной совместимости с уже созданными тестовыми блоками.
## Почему это не лежит в Pending_Features ## Почему это лежит в TODO
`Dev_Docs/Pending_Features/` предназначена для фич, которые уже реализованы и ждут ручной проверки. Репосты сейчас не должны проверяться как готовая фича, потому что пользовательский сценарий временно закрыт, а серверная запись новых репостов заблокирована. Поэтому задача остаётся в TODO как будущая.
Репосты сейчас не подходят под этот статус: они не должны проверяться как готовая фича, потому что пользовательский сценарий временно закрыт, а серверная запись новых репостов заблокирована. Поэтому старый pending-файл удалён, а задача перенесена сюда как будущая.
## Что сделать при возврате к реализации ## Что сделать при возврате к реализации
@@ -82,11 +80,11 @@
- отображение `targetBlockchainName`, `targetBlockNumber`, `targetBlockHash`. - отображение `targetBlockchainName`, `targetBlockNumber`, `targetBlockHash`.
7. Добавить или обновить тесты на успешный репост и отказ некорректных target-полей. 7. Добавить или обновить тесты на успешный репост и отказ некорректных target-полей.
8. Обновить документацию: 8. Обновить документацию:
- `Dev_Docs/Blockchain/11_TEXT_Blocks.md`; - `docs/Blockchain/11_TEXT_Blocks.md`;
- `Dev_Docs/Blockchain/CHANGELOG.md`; - `docs/Blockchain/CHANGELOG.md`;
- `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`; - `docs/API/04_Add_Block_to_Blockchain_API.md`;
- документы API чтения каналов/тредов, если изменятся поля ответа. - документы API чтения каналов/тредов, если изменятся поля ответа.
9. После реализации перенести задачу из `TODO/` в `Dev_Docs/Pending_Features/` как фичу, требующую ручной проверки. 9. После реализации отдельно согласовать ручную проверку пользовательского сценария.
## Минимальный чек-лист ручной проверки в будущем ## Минимальный чек-лист ручной проверки в будущем
@@ -47,10 +47,10 @@
## Документы, которые обновить при реализации ## Документы, которые обновить при реализации
- `Dev_Docs/Blockchain/`, если появятся или изменятся блоки баланса. - `docs/Blockchain/`, если появятся или изменятся блоки баланса.
- `Dev_Docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат. - `docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат.
- `Dev_Docs/API/`, если меняется серверный API. - `docs/API/`, если меняется серверный API.
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации. - после реализации отдельно согласовать ручную проверку.
- Документацию Solana-регистрации, если баланс будет связан с Solana-модулем. - Документацию Solana-регистрации, если баланс будет связан с Solana-модулем.
## Минимальная проверка в будущем ## Минимальная проверка в будущем
@@ -34,10 +34,10 @@
## Документы, которые нужно обновить при возврате ## Документы, которые нужно обновить при возврате
- `Dev_Docs/Keys/README.md` - `docs/Keys/README.md`
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md` - `docs/Personal_Messages/Протокол_DM_v1.md`
- `Dev_Docs/API/` - `docs/API/`
- `Dev_Docs/Blockchain/`, если появятся новые блоки или команды для файлов. - `docs/Blockchain/`, если появятся новые блоки или команды для файлов.
## С какого места продолжать ## С какого места продолжать
@@ -84,11 +84,11 @@
## Что нужно обновить при реализации ## Что нужно обновить при реализации
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` - `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
- `Dev_Docs/Solana_Architecture/README.md` - `docs/Solana_Architecture/README.md`
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - `docs/Инициализация_Solana_регистрации/README.md`
- `Dev_Docs/Keys/README.md` - `docs/Keys/README.md`
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`, если изменится адресация DM по типам сессий - `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится адресация DM по типам сессий
- `Dev_Docs/API/`, если появятся новые серверные операции или изменятся ответы - `docs/API/`, если появятся новые серверные операции или изменятся ответы
## Что пока не делать ## Что пока не делать
@@ -37,9 +37,8 @@
## Что обновить при возврате ## Что обновить при возврате
- `Dev_Docs/Pending_Features/README.md` - после реализации отдельно согласовать ручную проверку
- `shine-UI/js/pages/connect-device-view.js` - `shine-UI/js/pages/connect-device-view.js`
- `shine-UI/js/pages/device-qr-view.js` - `shine-UI/js/pages/device-qr-view.js`
- `shine-UI/js/services/qr-key-transfer-service.js` - `shine-UI/js/services/qr-key-transfer-service.js`
- документацию по ключам, если формат переноса меняется - документацию по ключам, если формат переноса меняется
@@ -0,0 +1,36 @@
# Переход с SQLite на PostgreSQL
## Зачем
Текущая серверная база на `SQLite` удобна для простого односерверного режима, но она хуже подходит для большого числа параллельных записей, роста нагрузки и дальнейшего масштабирования сервера.
`PostgreSQL` нужен как следующий уровень серверной БД для более надёжной конкурентной записи, более предсказуемой работы под нагрузкой и дальнейшего роста проекта.
## Что сделать
- Подготовить план переноса серверной БД с `SQLite` на `PostgreSQL`.
- Найти все места, где код завязан на особенности `SQLite`.
- Проверить все DAO и SQL-запросы на совместимость с `PostgreSQL`.
- Продумать схему миграции существующей production/test базы без потери данных.
- Отдельно проверить транзакции, `UPSERT`, индексы, case-insensitive сравнения и миграции схемы.
- После этого подготовить отдельный этап внедрения и переключения сервера.
## Что уже есть в коде
- Доступ к БД в основном проходит через DAO-слой, а не полностью размазан по проекту.
- Основная серверная логика уже разделена по модулям.
- Но SQL и миграции сейчас написаны под `SQLite` и потребуют отдельного прохода.
## Откуда продолжать
- Начать с инвентаризации всех DAO и схемы БД.
- После этого сделать отдельный документ с оценкой объёма работ по переносу.
- Затем решить, будет ли это:
- полный перевод сервера на `PostgreSQL`;
- или поддержка двух драйверов на переходный период.
## Что потом обновить
- Серверную документацию по БД и миграциям.
- Инструкции по локальному запуску сервера.
- Скрипты деплоя и настройки окружения.
@@ -58,8 +58,8 @@
## Документы, которые обновить при реализации ## Документы, которые обновить при реализации
- Документацию UI/кошельков, если такая есть. - Документацию UI/кошельков, если такая есть.
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации. - после реализации отдельно согласовать ручную проверку.
- `Dev_Docs/API/`, только если появится новый серверный API или логирование. - `docs/API/`, только если появится новый серверный API или логирование.
## Минимальная проверка ## Минимальная проверка
@@ -69,7 +69,7 @@ git show 0240db5:shine-solana/shine/programs/shine_login_guard/src/lib.rs
- `shine-solana/shine/doc/programs/shine_login_guard.md` - `shine-solana/shine/doc/programs/shine_login_guard.md`
5. Архитектурная документация: 5. Архитектурная документация:
- `Dev_Docs/Solana_Architecture/README.md` - `docs/Solana_Architecture/README.md`
6. UI-логика precheck: 6. UI-логика precheck:
- `shine-UI/js/pages/register-view.js` - `shine-UI/js/pages/register-view.js`
@@ -108,7 +108,7 @@ git show 0240db5:shine-solana/shine/programs/shine_login_guard/src/lib.rs
4. Проверить, что `build.rs` всё ещё генерирует `generated_dictionary.rs` в прежнем формате. 4. Проверить, что `build.rs` всё ещё генерирует `generated_dictionary.rs` в прежнем формате.
5. Сверить актуальность словарей в `src/dictionaries`. 5. Сверить актуальность словарей в `src/dictionaries`.
6. Обновить `shine_login_guard.md` обратно под словарную логику. 6. Обновить `shine_login_guard.md` обратно под словарную логику.
7. Обновить `Dev_Docs/Solana_Architecture/README.md`. 7. Обновить `docs/Solana_Architecture/README.md`.
### Проверка после возврата ### Проверка после возврата
@@ -28,6 +28,6 @@
## Какие документы потом обновить ## Какие документы потом обновить
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`, если изменится стратегия миграции старой истории; - `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится стратегия миграции старой истории;
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`, если появится отдельное правило совместимости/конвертации; - `docs/Personal_Messages/Формат_DM_v1.md`, если появится отдельное правило совместимости/конвертации;
- при необходимости `Dev_Docs/API/12_Direct_Messages_Push_Calls_API.md`, если затронется поведение backlog/доставки. - при необходимости `docs/API/12_Direct_Messages_Push_Calls_API.md`, если затронется поведение backlog/доставки.
@@ -2,7 +2,9 @@
## Зачем ## Зачем
Сейчас синхронизация между серверами работает в основном как periodic sync и one-shot push. Для нормальной репликации ещё нужен постоянный межсерверный канал: Текущий production-режим SHiNE рассчитан на один основной сервер. Межсерверная синхронизация относится к будущей децентрализации и не должна блокировать выкладку односерверной production-версии.
Сейчас синхронизация между серверами работает в основном как periodic sync и one-shot push. Для нормальной репликации в будущем ещё нужен постоянный межсерверный канал:
- живое подключение к партнёру; - живое подключение к партнёру;
- push новых блоков; - push новых блоков;
@@ -35,7 +37,7 @@
## Какие документы потом обновить ## Какие документы потом обновить
- `Dev_Docs/Blockchain/sync-between-servers.md`; - `docs/Blockchain/sync-between-servers.md`;
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`; - `docs/Personal_Messages/Протокол_DM_v1.md`;
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`; - `docs/Personal_Messages/Формат_DM_v1.md`;
- `Dev_Docs/API/`. - `docs/API/`.
@@ -0,0 +1,16 @@
# Децентрализация
Папка для задач, которые нужны для будущего режима с несколькими серверами, Solana/PDA-синхронизацией и внешним хранением данных.
## Текущий статус
Сейчас production-режим SHiNE считается односерверным: один сервер обслуживает пользователей, сообщения, звонки и запись данных. Задачи из этой папки не являются блокерами для выкладки текущего репозитория на GitHub и запуска одного production-сервера.
## Задачи
- `односерверный_production_режим.md` - зафиксировать границы текущей production-версии.
- `запись_блокчейнов_в_arweave.md` - вынести долговременную запись блокчейнов в Arweave.
- `realtime_pda_solana_sync.md` - сделать онлайн-синхронизацию PDA/Solana в реальном времени.
- `межсерверная_передача_сообщений.md` - реализовать доставку сообщений между серверами.
- `межсерверные_звонки.md` - реализовать маршрутизацию звонков между серверами.
- `2026-06-26_1805_межсерверный_ws_и_dm_sync.md` - старый план постоянного server-to-server WS и DM sync, перенесённый в контекст децентрализации.
@@ -0,0 +1,30 @@
# Realtime-синхронизация PDA и Solana
## Зачем
В будущем PDA-записи и Solana-состояние должны автоматически и быстро синхронизироваться с серверным состоянием, чтобы данные пользователей, homeserver-сессии и связанные записи не расходились.
## Что сделать
1. Определить, какие серверные события должны обновлять PDA.
2. Добавить очередь/воркер для надёжной отправки изменений в Solana.
3. Добавить периодическую сверку серверного состояния с PDA.
4. Добавить обработку ошибок, повторов и конфликтов версий.
5. Добавить мониторинг задержек и неуспешных Solana-транзакций.
## Что учесть
- Solana/Anchor-модуль находится в `shine-solana/shine/` и ведётся отдельно от основного server/UI deploy.
- Перед изменениями внутри Solana-модуля нужно читать `shine-solana/shine/AGENTS.md`.
- Основная инструкция по Solana-регистрации находится в `docs/Инициализация_Solana_регистрации/README.md`.
- Формат пользовательской PDA-записи описан в `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`.
## Документы, которые потом нужно обновить
- `docs/Инициализация_Solana_регистрации/README.md`;
- `docs/Solana_Architecture/README.md`;
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`, если меняется формат PDA.
## Статус
Отложено до этапа децентрализации.
@@ -0,0 +1,29 @@
# Запись блокчейнов в Arweave
## Зачем
Для будущей децентрализации нужно долговременное внешнее хранение блокчейнов, чтобы данные не зависели только от одного серверного диска.
## Что сделать
1. Определить, какие блокчейны и какие диапазоны блоков записываются в Arweave.
2. Зафиксировать формат пачки блоков, метаданных, ссылок и контрольных хэшей.
3. Добавить безопасный механизм публикации без хранения приватного JWK в git.
4. Добавить проверку уже загруженных диапазонов, чтобы не плодить дубли.
5. Описать восстановление блокчейна из Arweave при потере локальных данных.
## Важные ограничения
- Любое изменение формата блокчейна требует отдельного предупреждения и явного подтверждения пользователя.
- Добавление данных в блокчейн должно выполняться только через `AddBlock`.
- Секреты Arweave нельзя хранить в репозитории.
## Документы, которые потом нужно обновить
- `docs/Blockchain/README.md`;
- `docs/Blockchain/CHANGELOG.md`;
- документы deploy/секретов в `deploy/`, если появятся новые параметры.
## Статус
Отложено до этапа децентрализации.
@@ -0,0 +1,30 @@
# Межсерверная передача сообщений
## Зачем
Когда у SHiNE появится несколько серверов, пользователи на разных серверах должны получать личные сообщения без ручной синхронизации и без привязки к одному центральному узлу.
## Что сделать
1. Определить протокол server-to-server доставки DM.
2. Добавить маршрутизацию получателя по серверу, user id, публичному ключу или PDA.
3. Добавить ACK, повторы, дедупликацию и backfill пропущенных сообщений.
4. Разделить realtime-доставку и восстановление истории.
5. Описать поведение при недоступности удалённого сервера.
## Что учесть
- Логика DM должна соответствовать документам в `docs/Personal_Messages/`.
- При изменении формата signed DM-блока или правил доставки нужно обновлять протокол и байтовый формат DM.
- Если появятся новые server API/WebSocket операции, нужно обновить `docs/API/`.
## Документы, которые потом нужно обновить
- `docs/Personal_Messages/Протокол_DM_v1.md`;
- `docs/Personal_Messages/Формат_DM_v1.md`;
- `docs/API/`;
- `docs/API/09_Operations_Index.md`, если добавляются новые `op`.
## Статус
Отложено до этапа децентрализации.
@@ -0,0 +1,29 @@
# Межсерверные звонки
## Зачем
В будущем пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера.
## Что сделать
1. Определить протокол межсерверной сигнализации звонков.
2. Добавить маршрутизацию offer/answer/ICE-кандидатов между серверами.
3. Добавить обработку статусов занятости, отказа, таймаута и ошибок маршрута.
4. Добавить диагностику доставки сигналов между серверами.
5. Проверить совместимость с текущими логами `CallDeliveryReport`.
## Что учесть
- Специальная диагностика установки звонков идёт через `CallDeliveryReport`.
- На production важно сохранять поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
- Межсерверные звонки не должны ломать текущий односерверный сценарий.
## Документы, которые потом нужно обновить
- `docs/API/`, если добавляются или меняются операции сигнализации;
- документы по звонкам/диагностике, если они будут выделены отдельно;
- deploy-документы, если появятся новые параметры TURN/server-to-server маршрутизации.
## Статус
Отложено до этапа децентрализации.
@@ -0,0 +1,23 @@
# Односерверный production-режим
## Зачем
Перед выкладкой репозитория на GitHub и запуском production нужно явно зафиксировать, что текущая стабильная версия работает как один основной сервер.
## Что считаем текущей нормой
- Один production-сервер обслуживает пользователей, сообщения, звонки и серверные данные.
- Децентрализованные сценарии не считаются обязательными для первого production-релиза.
- Межсерверная доставка сообщений, межсерверные звонки, realtime PDA/Solana sync и запись блокчейнов в Arweave вынесены в отдельные будущие задачи.
- Код и документация текущего production не должны создавать ожидание, что несколько серверов уже работают как единая realtime-сеть.
## Что сделать перед возвратом к децентрализации
1. Проверить актуальные документы по API, blockchain, DM и deploy.
2. Выделить минимальный протокол server-to-server взаимодействия.
3. Решить, какие данные остаются локальными, какие реплицируются между серверами, а какие записываются во внешнее долговременное хранилище.
4. После изменения API, blockchain-форматов или DM-протокола обновить соответствующие документы по правилам проекта.
## Статус
Отложено. Текущий production работает как один сервер.
@@ -0,0 +1,275 @@
# Новая логика контента в блокчейне SHiNE
## Зачем это нужно
Сейчас блокчейн SHiNE хорошо умеет хранить обычные сообщения, ответы, лайки и связи между людьми.
Новая модель добавляет поверх этого более понятный смысл контента:
- обычный текст;
- упражнение;
- услуга / процедура;
- курс;
- стартовая страница канала (`entrypoint`).
Это нужно для того, чтобы канал стал не просто лентой постов, а полноценным пространством знаний, практик, услуг и сообществ.
## Что меняется для людей
### 1. В канале появятся понятные виды материалов
Сообщение можно будет создать не только как обычный текст, но и как:
- упражнение;
- услугу / процедуру;
- курс;
- стартовую страницу канала.
Смысл в том, что приложение и сервер будут понимать, что это за материал, а не просто показывать любой текст одинаково.
### 2. У канала будет стартовая страница
У канала появится отдельное стартовое сообщение `entrypoint`.
Это не курс и не оглавление, а именно главная точка входа в канал:
- короткое объяснение, о чём канал;
- описание структуры;
- ссылки на нужные материалы;
- удобное начало для новых людей.
У канала в каждый момент времени будет только одна актуальная стартовая страница.
Если её исправляют, то сохраняется история версий.
Если её удаляют, для интерфейса считается, что стартовой страницы у канала сейчас нет.
### 3. Курс, упражнение и услуга / процедура будут отличаться по смыслу
Это важно для логики и статистики.
- `Упражнение` — то, что человек может делать много раз.
- `Услуга / процедура` — то, что тоже можно проходить много раз, но обычно с участием другого человека.
- `Курс` — то, что можно начать, закончить или бросить.
За счёт этого сервер сможет честно считать активность, а интерфейс сможет показывать человеку именно те действия, которые подходят к данному типу материала.
### 4. Появятся статусные действия
На контент можно будет не только ответить или поставить лайк, но и отметить свой путь:
- сделал один раз;
- заинтересовался и рассматривает;
- начал;
- закончил / освоил / знаю;
- бросил.
При этом:
- для упражнений и услуг / процедур будет отдельно считаться, сколько раз человек сделал / прошёл;
- для упражнений и курсов будет храниться текущий статус.
Текущий статус определяется просто:
- последнее статусное действие и считается актуальным.
Например:
- если последнее действие “заинтересовался и рассматривает”, значит человек присматривается, но ещё не начал;
- если последнее действие `started`, значит материал сейчас в процессе;
- если последнее действие `abandoned`, значит человек бросил;
- если последнее действие `completed`, значит для системы он завершил / освоил материал.
### 5. К действиям можно добавлять живой текст
Практически любое статусное действие можно будет сопровождать коротким комментарием.
Например:
- “Начал изучать, потому что давно хотел разобраться”;
- “Бросил, пока нет времени”;
- “Прошёл процедуру, стало заметно легче”.
Это важно, потому что сам блокчейн будет хранить не только формальный статус, но и живую человеческую причину или заметку.
### 6. Появится подтверждение статуса другими людьми
Отдельный человек сможет подтвердить чей-то статус.
Примеры:
- подтвердить, что человек действительно занимался;
- подтвердить, что он реально прошёл услугу;
- подтвердить, что он освоил материал.
Подтверждение — это не замена статуса, а отдельное мнение / свидетельство со стороны.
### 7. Появится отдельный тип «мнение»
На любое сообщение можно будет ответить не только обычным ответом, но и специальным типом ответа: `мнение`.
Это по сути тоже текстовый ответ, но с отдельным смыслом:
- это отзыв;
- это оценка;
- это мнение о материале;
- это явная метка для будущего анализа нейронками.
То есть:
- обычный ответ нужен для разговора;
- `мнение` нужно для отзыва, оценки и анализа реакции людей.
## Что остаётся как раньше
### Комментарии
Обычные ответы на сообщения остаются.
То есть обсуждение материалов не ломается и не меняется концептуально.
### Лайки контента
Лайк на сообщение, курс, упражнение или услугу остаётся обычной реакцией на конкретный блок.
### Лайк пользователю
Лайк пользователю не будет считаться реакцией на сообщение.
Он относится к графу связей между людьми.
Это удобно, потому что:
- лайк человека — это отношение к человеку;
- лайк материала — это отношение к контенту.
## Сообщество вокруг канала
Канал сможет работать не только как лента, но и как сообщество.
Для этого появятся простые действия:
- заявка на вступление;
- самостоятельный выход;
- принятие;
- исключение.
Сервер сможет понимать:
- кто только подал заявку;
- кто уже принят;
- кто вышел;
- кто был исключён.
## Личный канал и лента достижений
У каждого человека по смыслу появляется два важных пространства:
- канал его обычных постов;
- отдельная лента его тренировок и достижений.
В обычном канале человек сможет:
- писать посты;
- делиться мыслями;
- публиковать материалы;
- обсуждать темы как раньше.
А в ленте достижений будут видны его реальные действия:
- какие упражнения он делал;
- какие услуги / процедуры проходил;
- какие курсы его заинтересовали;
- какие курсы он начал;
- какие курсы он закончил;
- что он бросил.
То есть блокчейн SHiNE сможет хранить не только слова человека, но и его путь, активность и историю практики.
## Что смогут делать авторы контента
Создатели контента в своих каналах смогут публиковать не только обычные посты, но и:
- упражнения;
- курсы;
- стартовую страницу канала;
- услуги / процедуры, которые они оказывают.
Это превращает канал в сочетание:
- блога;
- базы знаний;
- пространства обучения;
- каталога услуг и практик.
## Что увидит человек в интерфейсе
На специальных сообщениях в UI можно будет показывать отдельные кнопки действий.
Например:
- `Выполнил упражнение`
- `Прошёл процедуру`
- `Заинтересовало`
- `Начал курс`
- `Закончил курс`
То есть материал можно будет не просто прочитать, а сразу отметить реальное действие.
Также при ответе на любое сообщение можно будет выбрать:
- обычный ответ;
- `мнение / отзыв`.
## Как будет работать лента достижений
Если кто-то зайдёт в твою ленту достижений, он сможет:
- прочитать, что ты делал;
- оставить мнение / отзыв;
- подтвердить, что это действительно было.
Это даёт основу для мягкой “сертификации” внутри SHiNE.
Например:
- человек прошёл курс и получил подтверждения;
- человек прошёл процедуру и получил отзыв;
- человек регулярно делает упражнения, и это видно в его истории.
Так постепенно у пользователя появляется не только лента постов, но и лента достижений, подтверждений и репутации.
## Ссылки внутри SHiNE
Для переходов между материалами вводятся простые внутренние адреса:
- обычная ссылка: `SHiNE/alice-001/157`
- особополная ссылка: `SHiNE/alice-001/157/ХЭШ`
Первая форма — основная и каноническая.
Вторая нужна там, где хочется добавить ещё и точную проверку по хэшу.
## Что это даёт в итоге
После внедрения новая блокчейн-логика позволит:
- строить каналы как структурированные пространства, а не просто как поток постов;
- выделять упражнения, услуги и курсы как отдельные сущности;
- показывать стартовую страницу канала;
- хранить путь человека по материалу;
- хранить отдельную ленту его действий и достижений;
- считать активность и статусы;
- подтверждать результаты другими людьми;
- развивать сообщество вокруг канала.
И самое важное: всё это можно добавить как расширение уже существующего блокчейна SHiNE, не разрушая старую модель сообщений.
## Отдельный вопрос для будущего
Отзывы о людях как о людях — полезная идея, но её стоит дополнительно обдумать.
Например, на вкладке связей в будущем можно:
- писать человеку отзыв;
- смотреть все отзывы о человеке;
- выводить сначала отзывы близких друзей, родственников, друзей и контактов, а уже потом остальные.
Но этот слой нужно делать осторожно, чтобы он не стал слишком жёстким или неприятным для людей.
Поэтому отзывы о людях как отдельная социальная механика требуют дополнительного обсуждения и проектирования.
@@ -0,0 +1,670 @@
# ТЗ: новая контентная модель блокчейна SHiNE
## Статус документа
Этот документ описывает предлагаемые новые типы блоков и правила их обработки.
Цель:
- добавить новую семантику контента;
- не ломать существующие блоки `type=0..4`;
- внедрить всё как расширение блокчейна за счёт новых форматов.
Документ является проектным ТЗ на реализацию в сервере, БД, API чтения и UI.
## 1. Базовые принципы
### 1.1. Совместимость
Старые типы не меняются:
- `type=0` — TECH
- `type=1` — TEXT
- `type=2` — REACTION
- `type=3` — CONNECTION
- `type=4` — USER_PARAM
Новые сущности и действия добавляются только как новые `type` и новые `body`.
Это означает:
- старые блоки продолжают читаться как раньше;
- старые `TEXT_POST`, `TEXT_REPLY`, `REACTION_LIKE` и остальные форматы не ломаются;
- существующий блокчейн остаётся валидным;
- новый функционал появляется только там, где клиент и сервер умеют его понимать.
### 1.2. Общая стратегия
Новая модель делится на четыре слоя:
1. контентные сущности;
2. текстовые отзывы и мнения;
3. статусные действия пользователей;
4. community-события вокруг канала.
### 1.3. Редактирование и удаление
Для новых контентных сущностей сохраняется действующий принцип SHiNE:
- редактирование всегда ссылается на оригинальный блок;
- тип сущности edit не меняет;
- удаление выполняется через `edit` с пустым текстом;
- отдельный `DELETE`-подтип не вводится.
Это правило особенно важно для:
- `plain_text`
- `exercise`
- `service`
- `course`
- `entrypoint`
В пользовательских текстах и UI желательно использовать русские названия:
- обычный текст;
- упражнение;
- услуга / процедура;
- курс;
- стартовое сообщение канала.
## 2. Канонические внутренние ссылки
В новой модели поддерживаются только две формы внутренней ссылки:
- каноническая: `SHiNE/<blockchainName>/<blockNumber>`
- особополная: `SHiNE/<blockchainName>/<blockNumber>/<blockHash>`
Примеры:
- `SHiNE/alice-001/157`
- `SHiNE/alice-001/157/abcd1234...`
Правила:
- канонической считается именно короткая форма без хэша;
- форма с хэшем используется как усиленный вариант для точной проверки;
- внутри UI и серверной логики ссылка должна приводиться как минимум к паре:
- `blockchainName`
- `blockNumber`
- если хэш присутствует, он участвует в дополнительной валидации ссылки.
## 3. Новые контентные сущности
## 3.1. Новый `type=5``CONTENT`
Назначение:
- хранение новых смысловых материалов канала;
- сохранение линии канала;
- поддержка edit-версий и логического удаления.
### 3.1.1. Подтипы `CONTENT`
- `subType=10``CONTENT_PLAIN`
- `subType=11``CONTENT_EDIT_PLAIN`
- `subType=20``CONTENT_EXERCISE`
- `subType=21``CONTENT_EDIT_EXERCISE`
- `subType=30``CONTENT_SERVICE`
- `subType=31``CONTENT_EDIT_SERVICE`
- `subType=40``CONTENT_COURSE`
- `subType=41``CONTENT_EDIT_COURSE`
- `subType=50``CONTENT_ENTRYPOINT`
- `subType=51``CONTENT_EDIT_ENTRYPOINT`
### 3.1.2. Семантика подтипов
- `CONTENT_PLAIN` — обычный текст нового поколения.
- `CONTENT_EXERCISE` — упражнение, которое можно выполнять многократно.
- `CONTENT_SERVICE` — услуга / процедура, которую можно проходить многократно.
- `CONTENT_COURSE` — курс / оглавление.
- `CONTENT_ENTRYPOINT` — стартовое сообщение канала.
### 3.1.3. Почему `entrypoint` отдельный тип
`entrypoint` не считается курсом.
Это отдельная сущность, потому что:
- она описывает вход в канал;
- по ней нельзя делать `started / completed / abandoned`;
- у канала в каждый момент времени должна быть только одна актуальная стартовая страница.
### 3.1.4. Ограничение на `entrypoint`
Для одного канала допускается только один исходный блок `CONTENT_ENTRYPOINT`.
Правила:
- если entrypoint уже существует, создать второй нельзя;
- изменять можно только через `CONTENT_EDIT_ENTRYPOINT`;
- если entrypoint логически удалён, UI должен считать, что стартовой страницы больше нет;
- исторический блок при этом остаётся в цепочке.
### 3.1.5. Формат body для `CONTENT_*`
Для `version=1` рекомендуется использовать формат, максимально совместимый по логике с текущими `TEXT_POST` / `TEXT_EDIT_POST`.
#### Создающие блоки
Для:
- `CONTENT_PLAIN`
- `CONTENT_EXERCISE`
- `CONTENT_SERVICE`
- `CONTENT_COURSE`
- `CONTENT_ENTRYPOINT`
body:
```text
ContentLineBody_v1
- lineCode: int32
- prevLineNumber: int32
- prevLineHash32: [32]
- thisLineNumber: int32
- textLenBytes: uint16
- text UTF-8
```
#### Edit-блоки
Для:
- `CONTENT_EDIT_PLAIN`
- `CONTENT_EDIT_EXERCISE`
- `CONTENT_EDIT_SERVICE`
- `CONTENT_EDIT_COURSE`
- `CONTENT_EDIT_ENTRYPOINT`
body:
```text
ContentEditBody_v1
- lineCode: int32
- prevLineNumber: int32
- prevLineHash32: [32]
- thisLineNumber: int32
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- textLenBytes: uint16
- text UTF-8
```
Правила:
- edit всегда ссылается на оригинальный блок соответствующего типа;
- `toBlockchainName` в edit не хранится;
- `textLen=0` означает логическое удаление содержимого;
- тип исходной сущности edit не меняет.
### 3.1.6. Что считается комментарием
Комментарии не требуют нового формата.
Для обсуждения новых контентных сущностей продолжают использоваться уже существующие:
- `TEXT_REPLY`
- `TEXT_EDIT_REPLY`
Это позволяет не ломать старую reply-механику и reuse текущую модель тредов.
## 4. Текстовые отзывы
## 4.1. Новый `type=6``TEXT_RATING`
Назначение:
- текстовая оценка / отзыв на объект;
- без числовой шкалы;
- с возможностью редактирования и логического удаления.
Смысл `TEXT_RATING`:
- это текст;
- это специальный отзыв / мнение / оценка;
- это явный сигнал, что перед нами не просто комментарий, а осмысленный отзыв;
- в будущем это поле можно отдельно анализировать нейронками.
### 4.1.1. Подтипы
- `subType=10``TEXT_RATING_POST`
- `subType=11``TEXT_RATING_EDIT`
### 4.1.2. Где разрешён `TEXT_RATING_POST`
Разрешён на target:
- `HEADER` пользователя;
- контентный блок `type=5`;
- при необходимости в будущем — на другие target-блоки по отдельному решению.
Сейчас в данном ТЗ:
- отзыв / оценка на пользователя — да;
- отзыв / оценка на контент — да;
- отзыв / лайк на канал целиком — не вводится, только оставляется как будущая возможность.
### 4.1.3. Где и как используется `TEXT_RATING_POST`
`TEXT_RATING_POST` можно создавать:
- как отзыв на контентный блок;
- как отзыв на пользователя через target на `HEADER`;
- как специальный ответ вместо обычного комментария.
Практическое правило для UI:
- при ответе на любое сообщение пользователь может выбрать:
- обычный ответ;
- `мнение / отзыв`.
### 4.1.4. Формат body
#### Создание
```text
TextRatingBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- textLenBytes: uint16
- text UTF-8
```
#### Редактирование
```text
TextRatingEditBody_v1
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- textLenBytes: uint16
- text UTF-8
```
Правила:
- edit ссылается на оригинальный `TEXT_RATING_POST`;
- пустой текст в edit означает логическое удаление отзыва.
## 5. Статусные действия и накопительные события
## 5.1. Новый `type=7``STATUS_ACTION`
Назначение:
- хранение действий пользователя по отношению к контенту;
- вычисление текущего статуса;
- накопительный учёт повторных прохождений;
- подтверждение статусов другими людьми.
### 5.1.1. Подтипы
- `subType=10``STATUS_DONE_ONCE`
- `subType=20``STATUS_INTERESTED`
- `subType=30``STATUS_STARTED`
- `subType=40``STATUS_COMPLETED`
- `subType=50``STATUS_ABANDONED`
- `subType=60``STATUS_CONFIRMED`
### 5.1.2. Матрица допустимости по контенту
`STATUS_DONE_ONCE` разрешён только для:
- `CONTENT_EXERCISE`
- `CONTENT_SERVICE`
`STATUS_INTERESTED`, `STATUS_STARTED`, `STATUS_COMPLETED`, `STATUS_ABANDONED` разрешены только для:
- `CONTENT_EXERCISE`
- `CONTENT_COURSE`
`CONTENT_ENTRYPOINT` не поддерживает:
- `interested`
- `started`
- `completed`
- `abandoned`
### 5.1.3. Как считать текущее состояние
Для пары:
- `actorLogin`
- `targetBlock`
актуальным статусом считается последнее по времени статусное событие из набора:
- `STATUS_INTERESTED`
- `STATUS_STARTED`
- `STATUS_COMPLETED`
- `STATUS_ABANDONED`
Следствия:
- у одного пользователя по одному объекту в каждый момент времени только один актуальный статус;
- если последним пришёл `interested`, статус считается “заинтересовался / рассматривает, но ещё не начал”;
- если последним пришёл `started`, статус считается “в процессе”;
- если последним пришёл `completed`, статус считается “завершён / освоен / знаю”;
- если последним пришёл `abandoned`, статус считается “брошен”.
### 5.1.4. Как считать количество прохождений
`STATUS_DONE_ONCE` не меняет текущий статус.
Он считается отдельно как накопительное событие.
Сервер должен уметь считать:
- сколько раз пользователь сделал упражнение;
- сколько раз пользователь прошёл услугу / процедуру.
### 5.1.5. Дополнительный текст действия
Каждое действие `STATUS_*` может содержать дополнительный текст-комментарий.
Примеры:
- как именно делал упражнение;
- чем заинтересовал курс;
- с какими мыслями начал курс;
- почему бросил;
- что именно подтверждает подтверждающий человек.
### 5.1.6. Подтверждение статуса
`STATUS_CONFIRMED` разрешён только на target-статусы:
- `STATUS_DONE_ONCE`
- `STATUS_INTERESTED`
- `STATUS_STARTED`
- `STATUS_COMPLETED`
- `STATUS_ABANDONED`
Это значит:
- подтверждение не ставится прямо на курс или упражнение;
- подтверждение ставится на конкретный статусный блок другого человека.
Подтверждение:
- не меняет основной статус автора;
- не меняет счётчик `done_once`;
- хранится как отдельное мнение / свидетельство.
### 5.1.7. Формат body
Для `STATUS_DONE_ONCE`, `STATUS_INTERESTED`, `STATUS_STARTED`, `STATUS_COMPLETED`, `STATUS_ABANDONED`:
```text
StatusActionBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- noteLenBytes: uint16
- note UTF-8
```
Для `STATUS_CONFIRMED`:
```text
StatusConfirmBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- noteLenBytes: uint16
- note UTF-8
```
На уровне бинарного формата тело можно оставить одинаковым.
Различие задаётся `subType` и правилами валидации target.
## 6. Community-события
## 6.1. Новый `type=8``COMMUNITY_EVENT`
Назначение:
- заявки в сообщество;
- выход из сообщества;
- принятие;
- исключение.
### 6.1.1. Подтипы
- `subType=10``COMMUNITY_JOIN_REQUEST`
- `subType=20``COMMUNITY_LEAVE`
- `subType=30``COMMUNITY_ACCEPT`
- `subType=40``COMMUNITY_REMOVE`
### 6.1.2. Базовая логика
`COMMUNITY_JOIN_REQUEST`
- создаёт пользователь;
- target — `CONTENT_ENTRYPOINT` канала;
- может содержать текст заявки.
`COMMUNITY_LEAVE`
- создаёт сам участник;
- target — `CONTENT_ENTRYPOINT` канала;
- подтверждение не требуется;
- может содержать текст.
`COMMUNITY_ACCEPT`
- создаёт владелец канала;
- target — конкретный блок `COMMUNITY_JOIN_REQUEST`;
- может содержать текст.
`COMMUNITY_REMOVE`
- создаёт владелец канала;
- target — `CONTENT_ENTRYPOINT` канала;
- body дополнительно хранит `subjectLogin`, кого исключили;
- может содержать текст.
### 6.1.3. Текущее членство
Пользователь считается текущим участником сообщества, если:
- у него есть хотя бы одно принятие в это сообщество;
- после этого принятия нет более позднего:
- `COMMUNITY_LEAVE`
- `COMMUNITY_REMOVE`
Заявка сама по себе членство не создаёт.
### 6.1.4. Формат body
Для `JOIN_REQUEST` и `LEAVE`:
```text
CommunityActionBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- noteLenBytes: uint16
- note UTF-8
```
Для `ACCEPT`:
```text
CommunityAcceptBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- noteLenBytes: uint16
- note UTF-8
```
Для `REMOVE`:
```text
CommunityRemoveBody_v1
- toBlockchainNameLen: uint8
- toBlockchainName UTF-8
- toBlockGlobalNumber: int32
- toBlockHash32: [32]
- subjectLoginLen: uint8
- subjectLogin ASCII
- noteLenBytes: uint16
- note UTF-8
```
## 7. Что остаётся на старых типах
### 7.0. Обычный канал и лента достижений
На уровне продукта рекомендуется различать:
- обычный канал постов пользователя;
- отдельную ленту его действий и достижений.
В обычном канале пользователь:
- пишет посты;
- публикует материалы;
- общается и обсуждает.
В ленте достижений видны события:
- какие упражнения он делал;
- какие услуги / процедуры проходил;
- какие курсы его заинтересовали;
- какие курсы он начал;
- какие курсы он завершил;
- что он бросил.
В данном ТЗ эта модель фиксируется как продуктовая логика.
Конкретный способ хранения можно реализовать:
- либо отдельным специальным каналом;
- либо отдельным режимом чтения по статусным блокам.
### 7.1. Лайк пользователю
Лайк пользователю не вводится как `REACTION`.
Он остаётся в слое социальных связей:
- через `CONNECTION`
- как будущий отдельный подтип связи
В этом ТЗ сам новый подтип связи не описывается детально.
Нужно только зафиксировать правило:
- лайк человека относится к графу связей, а не к реакции на блок.
### 7.2. Лайк контента
Лайк на:
- `CONTENT_PLAIN`
- `CONTENT_EXERCISE`
- `CONTENT_SERVICE`
- `CONTENT_COURSE`
- `CONTENT_ENTRYPOINT`
может использовать уже существующий:
- `REACTION_LIKE`
- `REACTION_UNLIKE`
Отдельный новый формат для лайка контента не нужен.
### 7.3. Канал целиком
В текущем ТЗ не вводятся:
- отзыв на канал целиком;
- лайк канала целиком.
Это оставляется как будущая возможность.
### 7.4. Отзывы о людях
Отзывы о человеке как о человеке в текущем ТЗ допустимы через `TEXT_RATING` на `HEADER`.
Но продуктовую модель их показа нужно отдельно продумать.
Направление для будущего:
- просмотр отзывов о человеке на вкладке связей;
- приоритетный вывод отзывов от близких друзей, родственников, друзей и контактов;
- затем вывод остальных отзывов.
Эта тема полезна, но требует дополнительной осторожной проработки с точки зрения UX и социальных рисков.
## 8. Требования к серверу
Сервер после внедрения должен уметь:
1. Валидировать новые `type=5..8`.
2. Хранить новые блоки без ломки старого чтения.
3. Определять текущий статус пользователя по объекту:
- `interested`
- `started`
- `completed`
- `abandoned`
4. Считать накопительные события `done_once` для:
- `exercise`
- `service`
5. Считать подтверждения статусов.
6. Определять единственный актуальный `entrypoint` канала.
7. Определять текущее членство в сообществе канала.
8. Поддерживать внутренние ссылки вида:
- `SHiNE/<blockchainName>/<blockNumber>`
- `SHiNE/<blockchainName>/<blockNumber>/<blockHash>`
## 9. Требования к UI
UI после внедрения должен уметь:
1. Показывать разные карточки для:
- текста
- упражнения
- услуги
- курса
- entrypoint
2. Показывать стартовую страницу канала, если `entrypoint` существует.
3. Не показывать entrypoint, если он логически удалён.
4. Давать человеку только допустимые действия по типу материала.
5. Показывать:
- текущий статус;
- количество `done_once`;
- подтверждения статуса.
6. Показывать отдельные действия-кнопки на специальных блоках, например:
- `Выполнил упражнение`
- `Прошёл процедуру`
- `Заинтересовало`
- `Начал курс`
- `Закончил курс`
7. При ответе на сообщение давать выбор:
- обычный ответ;
- `мнение / отзыв`.
8. Открывать внутренние ссылки SHiNE.
## 10. Вывод по совместимости
Предлагаемая модель реализуема без слома старого блокчейна.
Причина:
- старые `type=0..4` не меняются;
- новые сущности вводятся только как новые `type=5..8`;
- существующие `reply`, `like`, `edit`, `HEADER`, `CREATE_CHANNEL` и `CONNECTION` продолжают работать как раньше;
- старые клиенты смогут игнорировать новые типы как неизвестные;
- новые клиенты смогут постепенно включать поддержку нового функционала.
Итог:
- это расширение формата блокчейна;
- это не миграция со сломом старых блоков;
- это можно внедрять поэтапно.
-41
View File
@@ -1,41 +0,0 @@
# TODO: Будущие доработки
## 1) Полный переход на `ReceiveOutcomingMessage`
- Сейчас в UI используется `ReceiveOutcomingMessage` с fallback на `SendMessagePair`.
- Fallback нужен только временно для совместимости со старыми серверами.
- После обновления всех серверов:
- убрать вызов `SendMessagePair` из UI,
- убрать регистрацию `SendMessagePair` на сервере (оставить только `ReceiveOutcomingMessage`).
## 2) Реальная мультисерверная доставка
- Сейчас фактически предполагается 1 сервер на пользователя.
- Нужно реализовать штатную мультисерверную схему:
- пересылка исходящих сообщений между серверами пользователя A,
- пересылка входящих сообщений между серверами пользователя B,
- дедупликация на уровне БД для затухания дублей.
## 3) Надёжная доставка при перезапуске сервера
- Сейчас возможен сценарий: запись уже сохранена в БД, но сервер не успел переслать дальше из-за перезапуска.
- Нужно добавить механизм «store + guaranteed forward»:
- очередь/аутбокс для межсерверной пересылки,
- фоновый ретрай до подтверждения отправки,
- корректная остановка (graceful shutdown) с дожатием критичных задач.
## 4) Политика идемпотентности
- Сохранить принцип: пара (`incoming`, `outgoing`) пишется одной транзакцией, либо обе, либо ни одной.
- Не допускать частичного состояния, при котором в БД есть только один блок пары.
## 5) Наблюдаемость и аналитика
- Добавить метрики по доставке:
- количество дублей,
- количество успешных вставок пар,
- доля доставок в WS/push,
- количество ретраев межсерверной пересылки.
## 6) Ограничение текущих звонков (важно)
- Сейчас звонки работают только в рамках одного сигнального сервера (или единого контура, где обе стороны уже подключены).
- Сценарий «пользователь A на своих серверах, пользователь B на других серверах» пока не поддержан.
- TODO на будущее:
- временная межсерверная авторизация/сессия для старта звонка,
- отправка сигнальных сообщений между разными серверами пользователей,
- аккуратное завершение временной сессии после установления/завершения звонка.
@@ -1,41 +0,0 @@
# TODO: Звонки и межсерверность
## Текущее ограничение
- Текущая реализация звонков фактически работает в одном сигнальном контуре (один сервер/единый кластер, где обе стороны уже присутствуют).
- Если пользователь A подключён к серверу A, а пользователь B к серверу B (и между ними нет общего сигнального слоя), `CallInviteBroadcast`/`CallSignalToSession` не смогут полноценно провести звонок между ними.
## Почему так сейчас
- Сигналинг звонка привязан к активным сессиям и событиям на конкретном сервере.
- Выбор целевой сессии (`sessionId`) и обмен `OFFER/ANSWER/ICE` происходит в рамках текущего сигнального контура.
- Push решает только «разбудить/уведомить», но не заменяет межсерверный сигнальный канал.
## Что можно сделать дальше
- Добавить временное межсерверное подключение именно для старта и ведения звонка:
- инициатор получает short-lived access на сервер callee (или через доверенный межсерверный gateway),
- в рамках короткой сессии отправляет invite/signal для конкретного `callId`,
- после завершения звонка временная сессия закрывается автоматически.
## Что нужно доработать для этого
1. Межсерверная доверенная модель:
- подпись/верификация межсерверных вызовов,
- allowlist доверенных серверов и ротация ключей.
2. Короткоживущая «call-only» авторизация:
- отдельный тип токена/сессии с TTL (например 1–3 минуты),
- минимальные права только на `CallInviteBroadcast/CallSignalToSession`.
3. Маршрутизация сессий пользователя между серверами:
- где находится активная сессия callee,
- как доставлять `stop_call` и terminal-сигналы на все устройства callee.
4. Идемпотентность и дедупликация:
- защита от повторов межсерверных сигналов по `callId + eventId`,
- корректная обработка out-of-order событий.
5. Наблюдаемость:
- метрики межсерверной доставки сигналов,
- диагностика по стадиям звонка и причинам срыва.
## Временный рабочий подход (до межсерверности)
- Держать звонки в одном сигнальном контуре.
- Использовать WebPush как fallback-уведомление (`incoming_call`/`stop_call`) для офлайн-сессий.
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.2.327 client.version=1.2.346
server.version=1.2.298 server.version=1.2.318
-54
View File
@@ -185,60 +185,6 @@ tasks.named('build') {
finalizedBy tasks.named('integrationTest') finalizedBy tasks.named('integrationTest')
} }
tasks.register('deployServerProduction', JavaExec) {
group = "!!deployment"
description = "Production deploy: build → upload to shineup.me → restart service (только после явного подтверждения)"
classpath = sourceSets.test.runtimeClasspath
mainClass = "test.it.IT_DeployRestartNoCleanNoTestsMain"
workingDir = file('SHiNE-server')
dependsOn shadowJar
systemProperty "it.remoteHost", System.getProperty("it.remoteHost", "shineup.me")
systemProperty "it.remoteUser", System.getProperty("it.remoteUser", "player")
systemProperty "it.remoteDir", System.getProperty("it.remoteDir", "/home/player/SHiNE/shine-server")
systemProperty "it.service", System.getProperty("it.service", "shine-server")
systemProperty "it.localJar", System.getProperty("it.localJar", "build/libs/shine-server.jar")
dependsOn testClasses
}
tasks.register('deployUIProduction', Exec) {
group = "!!deployment"
description = "Production UI deploy: shineup.me (только после явного подтверждения)"
workingDir = rootDir
commandLine 'bash', file('deploy_shine-ui_production_shineupme.sh').absolutePath
}
tasks.register('deployServer', Exec) {
group = "!!deployment"
description = "Default deploy server: server2.shineup.me"
dependsOn shadowJar
workingDir = rootDir
environment 'LOCAL_JAR', file('SHiNE-server/build/libs/shine-server.jar').absolutePath
commandLine 'bash', file('deploy_shine-server_test2.sh').absolutePath
}
tasks.register('deployUI', Exec) {
group = "!!deployment"
description = "Default deploy UI: server2.shineup.me"
workingDir = rootDir
commandLine 'bash', file('deploy_shine-ui_production_server2shineupme.sh').absolutePath
}
tasks.register('deployServerTest2') {
group = "!!deployment"
description = "Явный алиас второго production deploy server: server2.shineup.me"
dependsOn tasks.named('deployServer')
}
tasks.register('deployUITest2') {
group = "!!deployment"
description = "Явный алиас второго production deploy UI: server2.shineup.me"
dependsOn tasks.named('deployUI')
}
tasks.register('startLocal', Exec) { tasks.register('startLocal', Exec) {
group = "!!run" group = "!!run"
description = "Builds server, starts local WS server and local HTTP UI for end-to-end local testing" description = "Builds server, starts local WS server and local HTTP UI for end-to-end local testing"
-31
View File
@@ -1,31 +0,0 @@
TELEGRAM_BOT_TOKEN=replace_me
OPENAI_API_KEY=
ALLOWED_TELEGRAM_USERNAME=owner_username
ALLOWED_TELEGRAM_PLAYERS=user_one:User One,user_two:User Two
ALLOWED_TELEGRAM_CHANNEL_USERNAME=
BOT_USERNAME=your_bot_username
TELEGRAM_API_BASE_URL=https://api.telegram.org
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
OPENAI_TRANSCRIBE_MAX_UPLOAD_BYTES=25165824
OPENAI_TRANSCRIBE_MAX_CHUNK_SECONDS=900
OPENAI_TRANSCRIBE_OVERLAP_SECONDS=2
OPENAI_TRANSCRIBE_REENCODE_BITRATE_KBPS=24
OPENAI_TRANSCRIBE_FFMPEG_TIMEOUT_SECONDS=1800
FFMPEG_BIN=ffmpeg
FFPROBE_BIN=ffprobe
OPENAI_TTS_MODEL=gpt-4o-mini-tts
OPENAI_TTS_VOICE=alloy
OPENAI_TTS_RESPONSE_FORMAT=opus
OPENAI_TTS_TIMEOUT_SECONDS=180
OPENAI_TTS_CHUNK_CHARS=3500
OPENAI_VOICE_REWRITE_MODEL=gpt-4.1-nano
OPENAI_VOICE_REWRITE_TIMEOUT_SECONDS=90
OPENAI_VOICE_REWRITE_MAX_INPUT_CHARS=12000
OPENAI_VOICE_REWRITE_MAX_OUTPUT_TOKENS=900
CODEX_BIN=/home/your_user/.local/bin/codex
CODEX_WORKDIR=/home/your_user
CODEX_TIMEOUT_SECONDS=900
MAX_RETRIES=3
DATA_DIR=./data
-5
View File
@@ -1,5 +0,0 @@
.env
data/
logs/
run/
__pycache__/
-86
View File
@@ -1,86 +0,0 @@
# AGENTS
## Назначение
- `codex-agent-VPS` — переносимая версия Telegram-бота для запуска `codex` CLI на VPS.
- Папку можно ставить в любое место на Linux-сервере, если там есть `python3`, `systemd`, `codex` и доступ в интернет.
- Конфигурация делается через `.env`.
## Состав папки
- `README.md` — краткое описание структуры.
- `Agent-server-package/` — готовый набор файлов для копирования на VPS.
- `.env.example` — пример конфигурации.
- `AGENTS.md` — инструкция по установке и настройке.
## Требования к VPS
- Linux-сервер с `systemd`.
- Установленные `python3`, `curl`, `ffmpeg`.
- Установленный `codex` CLI.
- Выполненный `codex login` под тем пользователем, от которого будет работать сервис.
- Telegram bot token.
- Telegram usernames разрешённых пользователей.
## Установка через Codex
1. Скопировать содержимое `Agent-server-package/` на сервер в нужное место, например:
- `/home/your_user/codex-agent`
2. Установить `codex` CLI под рабочим пользователем.
3. Выполнить под этим же пользователем:
- `codex login`
4. Установить системные зависимости:
- `python3`
- `ffmpeg`
5. Взять `.env.example` из корня `codex-agent-VPS` и создать на сервере `.env`.
6. В `.env` заполнить:
- `TELEGRAM_BOT_TOKEN`
- `ALLOWED_TELEGRAM_USERNAME`
- `ALLOWED_TELEGRAM_PLAYERS`
- `BOT_USERNAME`
- `CODEX_BIN`
- `CODEX_WORKDIR`
7. Если нужны voice/audio и голосовые ответы, дополнительно задать:
- `OPENAI_API_KEY`
8. В `Agent-server-package/scripts/systemd/shine-agent-bot-coder.service` заменить:
- `your_user`
- `/home/your_user/codex-agent`
на реальные значения.
9. Скопировать unit в:
- `/etc/systemd/system/shine-agent-bot-coder.service`
10. Выполнить:
- `sudo systemctl daemon-reload`
- `sudo systemctl enable --now shine-agent-bot-coder`
11. Проверить:
- `sudo systemctl status shine-agent-bot-coder --no-pager`
- `sudo journalctl -u shine-agent-bot-coder -f`
## Настройка доступа
- `ALLOWED_TELEGRAM_USERNAME` — основной разрешённый пользователь.
- `ALLOWED_TELEGRAM_PLAYERS` — дополнительные разрешённые пользователи:
- `username1:Имя 1,username2:Имя 2`
- Все пользователи из whitelist в этой версии считаются полноправными.
- Все входящие задачи попадают в одну общую очередь и выполняются строго последовательно.
## Поведение агента
- Бот принимает текст, voice и audio.
- Для каждого пользователя ведётся отдельная история.
- Все задачи запускаются через `codex exec`.
- Рабочая директория задаётся через `CODEX_WORKDIR`.
- Вызов идёт без sandbox/approval ограничений: `--dangerously-bypass-approvals-and-sandbox`.
## Что обычно меняют при переносе
- `.env`
- `Agent-server-package/scripts/systemd/shine-agent-bot-coder.service`
- при необходимости `Agent-server-package/AGENT.md`
## Полезные команды
- Проверка установки Codex:
- `codex --version`
- `codex doctor`
- Self-test без Telegram:
- `python3 py_bot_service.py --selftest-codex "Ответь одной строкой: Codex работает"`
- Проверка сервиса:
- `sudo systemctl status shine-agent-bot-coder --no-pager`
- `sudo journalctl -u shine-agent-bot-coder -f`
## Примечания
- Если `codex doctor` пишет, что credentials не найдены, нужно выполнить `codex login`.
- Если `OPENAI_API_KEY` пустой, текстовые задачи через `codex` будут работать, а voice/audio и TTS-функции — нет.
- Если у пользователя в Telegram нет username, whitelist по username его не пропустит.
@@ -1,31 +0,0 @@
TELEGRAM_BOT_TOKEN=replace_me
OPENAI_API_KEY=
ALLOWED_TELEGRAM_USERNAME=owner_username
ALLOWED_TELEGRAM_PLAYERS=user_one:User One,user_two:User Two
ALLOWED_TELEGRAM_CHANNEL_USERNAME=
BOT_USERNAME=your_bot_username
TELEGRAM_API_BASE_URL=https://api.telegram.org
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
OPENAI_TRANSCRIBE_MAX_UPLOAD_BYTES=25165824
OPENAI_TRANSCRIBE_MAX_CHUNK_SECONDS=900
OPENAI_TRANSCRIBE_OVERLAP_SECONDS=2
OPENAI_TRANSCRIBE_REENCODE_BITRATE_KBPS=24
OPENAI_TRANSCRIBE_FFMPEG_TIMEOUT_SECONDS=1800
FFMPEG_BIN=ffmpeg
FFPROBE_BIN=ffprobe
OPENAI_TTS_MODEL=gpt-4o-mini-tts
OPENAI_TTS_VOICE=alloy
OPENAI_TTS_RESPONSE_FORMAT=opus
OPENAI_TTS_TIMEOUT_SECONDS=180
OPENAI_TTS_CHUNK_CHARS=3500
OPENAI_VOICE_REWRITE_MODEL=gpt-4.1-nano
OPENAI_VOICE_REWRITE_TIMEOUT_SECONDS=90
OPENAI_VOICE_REWRITE_MAX_INPUT_CHARS=12000
OPENAI_VOICE_REWRITE_MAX_OUTPUT_TOKENS=900
CODEX_BIN=/home/your_user/.local/bin/codex
CODEX_WORKDIR=/home/your_user
CODEX_TIMEOUT_SECONDS=900
MAX_RETRIES=3
DATA_DIR=./data
@@ -1,51 +0,0 @@
# AGENT.md для codex-agent-VPS
Ты запущен как обработчик входящего Telegram-сообщения от пользователя.
## Контекст
- `codex-agent-VPS` — Telegram-бот, который принимает сообщения, ведёт историю, ставит задачи в очередь и последовательно запускает `codex` CLI на VPS.
- Текстовые сообщения обрабатываются напрямую.
- Voice и audio сначала распознаются через OpenAI transcription, затем передаются как текстовая задача.
- История диалога хранится в JSONL-файле, путь передаётся в промпте.
- Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение.
- Основная реализация сервиса — Python-скрипт `py_bot_service.py`.
## Пользователи и доступ
- Разрешённые пользователи задаются через `ALLOWED_TELEGRAM_USERNAME` и `ALLOWED_TELEGRAM_PLAYERS`.
- Все разрешённые пользователи считаются полноправными.
- Для неизвестных пользователей в личном чате сервис отвечает вежливым отказом.
- Все входящие задачи попадают в одну общую очередь и выполняются строго по одной.
## Очередь и состояние
- Сервис ведёт состояние активной задачи и текущего файла истории.
- После рестарта сервис продолжает незавершённую обработку с учётом сохранённого состояния.
- Истории диалогов хранятся отдельно по username: `data/history/<username>/`.
- Архив истории после `/new`: `data/history/<username>/archive/`.
- После `/new` для этого же пользователя должен сбрасываться и контекст продолжения Codex-сессии; следующий запрос запускается как новая сессия, не через resume.
- Дедупликация Telegram update обязательна, чтобы одно сообщение не обрабатывалось повторно.
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус и повторяет его каждые 2 минуты.
## Голосовые ответы
- Озвучивание финальных ответов настраивается персонально командами `/voice_on` и `/voice_off`.
- Для новых пользователей озвучивание включено по умолчанию.
- Адаптация текста перед озвучкой настраивается командами `/voice_rewrite_on` и `/voice_rewrite_off`.
- Если озвучивание включено, после текстового финального ответа сервис дополнительно отправляет voice-файл через OpenAI TTS.
- Промежуточные статусы озвучивать не нужно.
## Команды
- `/status` — состояние очереди и персональных настроек.
- `/settings` — текущие пользовательские настройки.
- `/queue` — список задач в очереди.
- `/tasks` — список задач и предложений пользователя.
- `/new` — архивировать историю и начать новую Codex-сессию.
- `/stop` — остановить текущую задачу.
- `/cancel <id|all>` — удалить задачу по id или очистить очередь.
- `/restart` и `/restart_service` — отложенный рестарт после текущей задачи.
- `/restart_hard`, `/restart_now`, `/restart_force` — жёсткий рестарт прямо сейчас.
## Правила ответа
- Пиши содержательно и коротко.
- Не упоминай внутренние служебные детали, файловую систему и технические логи, если это не нужно пользователю.
- Если запрос требует действий с кодом или файлами, выполняй их в рабочей директории `CODEX_WORKDIR`.
- Если данных недостаточно, задай ровно один уточняющий вопрос.
- Если в промпте есть пометка retry, учитывай текущее состояние и продолжай аккуратно, а не начинай заново без причины.
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
[Unit]
Description=SHiNE Agent Bot Coder (Telegram + Codex queue worker)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=your_user
Group=your_user
WorkingDirectory=/home/your_user/codex-agent
Environment=HOME=/home/your_user
Environment=PATH=/home/your_user/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EnvironmentFile=/home/your_user/codex-agent/.env
ExecStart=/usr/bin/python3 /home/your_user/codex-agent/py_bot_service.py
Restart=always
RestartSec=5
TimeoutStopSec=20
SuccessExitStatus=143 0
StandardOutput=append:/home/your_user/codex-agent/logs/service.log
StandardError=append:/home/your_user/codex-agent/logs/service.log
[Install]
WantedBy=multi-user.target
-32
View File
@@ -1,32 +0,0 @@
# codex-agent-VPS
Переносимый комплект Telegram-бота для запуска `codex` CLI на VPS.
## Структура
- `README.md` — краткое описание структуры.
- `AGENTS.md` — инструкции по установке и настройке через Codex.
- `.env.example` — верхнеуровневый пример конфига.
- `Agent-server-package/` — готовый комплект файлов для копирования на другой сервер.
## Что копировать на сервер
На VPS обычно копируется содержимое папки:
- `Agent-server-package/`
Внутри неё лежат:
- `py_bot_service.py`
- `AGENT.md`
- `scripts/systemd/shine-agent-bot-coder.service`
## Что настраивать
- взять `.env.example` из корня `codex-agent-VPS/`
- создать на сервере `.env`
- вписать Telegram bot token
- вписать разрешённые usernames
- указать путь к `codex`
- указать рабочую директорию `CODEX_WORKDIR`
## Где инструкция
Полная инструкция по установке и настройке лежит в:
- `AGENTS.md`
-246
View File
@@ -1,246 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
GITHUB_USER="ai5590"
TOKEN_VAR_NAME="GIT_AI5590_CLASSIC_API_KEY"
print_line() {
echo "------------------------------------------------------------"
}
abort() {
echo
echo "Ошибка: $1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || abort "Не найдена команда '$1'. Установи её и запусти скрипт снова."
}
get_token() {
if [[ -z "${GIT_AI5590_CLASSIC_API_KEY:-}" ]]; then
abort "Не задана переменная окружения ${TOKEN_VAR_NAME}.
Перед запуском выполни:
export ${TOKEN_VAR_NAME}=\"ТВОЙ_GITHUB_TOKEN\""
fi
}
show_intro() {
print_line
echo "Этот скрипт создаст новый репозиторий в GitHub в аккаунте '${GITHUB_USER}',"
echo "затем инициализирует git в текущей папке (если нужно),"
echo "добавит файлы, кроме самого этого скрипта, создаст первый commit и отправит проект в GitHub."
echo
echo "Скрипт работает с содержимым ТЕКУЩЕЙ папки:"
echo " $(pwd)"
echo
echo "Для авторизации используется переменная окружения:"
echo " ${TOKEN_VAR_NAME}"
print_line
echo
}
ask_repo_name() {
local repo_name
read -r -p "Введите имя нового репозитория в GitHub: " repo_name
repo_name="$(echo "$repo_name" | xargs)"
[[ -n "$repo_name" ]] || abort "Имя репозитория не может быть пустым."
if [[ ! "$repo_name" =~ ^[A-Za-z0-9._-]+$ ]]; then
abort "Имя репозитория содержит недопустимые символы.
Разрешены: буквы, цифры, точка, дефис, подчёркивание."
fi
REPO_NAME="$repo_name"
}
ask_visibility() {
local answer
echo
read -r -p "Сделать репозиторий публичным? [y/N]: " answer
answer="${answer:-N}"
case "$answer" in
y|Y|yes|YES|да|Да|ДА)
REPO_PRIVATE="false"
REPO_VISIBILITY_TEXT="public"
;;
*)
REPO_PRIVATE="true"
REPO_VISIBILITY_TEXT="private"
;;
esac
}
ask_confirmation() {
echo
print_line
echo "Будет выполнено:"
echo "1. Создание GitHub-репозитория '${GITHUB_USER}/${REPO_NAME}' (${REPO_VISIBILITY_TEXT})"
echo "2. Подготовка git в текущей папке"
echo "3. Commit файлов из текущей папки, кроме самого этого скрипта"
echo "4. Push в ветку main"
print_line
echo
read -r -p "Продолжить? [y/N]: " confirm
confirm="${confirm:-N}"
case "$confirm" in
y|Y|yes|YES|да|Да|ДА) ;;
*) echo "Отменено пользователем."; exit 0 ;;
esac
}
check_not_inside_wrong_git_repo() {
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
local top
top="$(git rev-parse --show-toplevel)"
if [[ "$top" != "$(pwd)" ]]; then
abort "Ты запустил скрипт внутри уже существующего git-репозитория, но не в его корне.
Корень репозитория:
$top
Либо перейди в корень этого репозитория, либо запусти скрипт в папке, которая не вложена в другой git-репозиторий."
fi
fi
}
create_github_repo() {
echo
echo "Создаю репозиторий в GitHub..."
local http_code
local response_body_file
response_body_file="$(mktemp)"
http_code="$(
curl -sS \
-o "$response_body_file" \
-w "%{http_code}" \
-X POST "https://api.github.com/user/repos" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GIT_AI5590_CLASSIC_API_KEY}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "$(cat <<JSON
{
"name": "${REPO_NAME}",
"private": ${REPO_PRIVATE},
"auto_init": false
}
JSON
)"
)"
if [[ "$http_code" != "201" ]]; then
echo
echo "GitHub API вернул ошибку. HTTP code: $http_code"
echo "Ответ сервера:"
cat "$response_body_file"
rm -f "$response_body_file"
abort "Не удалось создать репозиторий '${GITHUB_USER}/${REPO_NAME}'."
fi
rm -f "$response_body_file"
echo "Репозиторий успешно создан: https://github.com/${GITHUB_USER}/${REPO_NAME}"
}
get_script_paths() {
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
PROJECT_PATH="$(pwd -P)"
SCRIPT_INSIDE_PROJECT="false"
SCRIPT_RELATIVE_PATH=""
case "$SCRIPT_PATH" in
"$PROJECT_PATH"/*)
SCRIPT_INSIDE_PROJECT="true"
SCRIPT_RELATIVE_PATH="${SCRIPT_PATH#$PROJECT_PATH/}"
;;
*)
SCRIPT_INSIDE_PROJECT="false"
;;
esac
}
prepare_git_repo() {
echo
echo "Подготавливаю git в текущей папке..."
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Git уже инициализирован."
else
git init
echo "Git инициализирован."
fi
get_script_paths
if [[ "$SCRIPT_INSIDE_PROJECT" == "true" ]]; then
echo "Скрипт находится внутри проекта и будет исключён из commit:"
echo " $SCRIPT_RELATIVE_PATH"
git add . ":!$SCRIPT_RELATIVE_PATH"
else
git add .
fi
if git diff --cached --quiet; then
echo "В staged нет изменений. Возможно, файлы уже были закоммичены ранее."
else
git commit -m "Initial commit"
echo "Создан commit: Initial commit"
fi
git branch -M main
local remote_url="https://${GITHUB_USER}:${GIT_AI5590_CLASSIC_API_KEY}@github.com/${GITHUB_USER}/${REPO_NAME}.git"
if git remote get-url origin >/dev/null 2>&1; then
echo "Remote 'origin' уже существует. Обновляю URL..."
git remote set-url origin "$remote_url"
else
git remote add origin "$remote_url"
fi
}
push_to_github() {
echo
echo "Отправляю проект в GitHub..."
git push -u origin main
echo
echo "Готово."
echo "Репозиторий: https://github.com/${GITHUB_USER}/${REPO_NAME}"
}
cleanup_remote_url() {
echo
echo "Убираю токен из remote URL, чтобы он не светился в git config..."
local safe_url="https://github.com/${GITHUB_USER}/${REPO_NAME}.git"
git remote set-url origin "$safe_url"
echo "Теперь origin = ${safe_url}"
}
main() {
require_command git
require_command curl
require_command realpath
get_token
check_not_inside_wrong_git_repo
show_intro
ask_repo_name
ask_visibility
ask_confirmation
create_github_repo
prepare_git_repo
push_to_github
cleanup_remote_url
}
main "$@"

Some files were not shown because too many files have changed in this diff Show More