SHA256
Compare commits
3
Commits
4b0a934e51
...
8f32e82d14
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
8f32e82d14 | ||
|
|
4d80ab639e | ||
|
|
95bbd2852e |
+21
-5
@@ -6,6 +6,8 @@ import java.util.Map;
|
||||
public final class ChannelMetaTextParser {
|
||||
public static final int MAX_TITLE_CHARS = 50;
|
||||
public static final int MAX_DESCRIPTION_CHARS = 250;
|
||||
private static final String LEGACY_PREFIX = "<SHiNE:";
|
||||
private static final String SHORT_PREFIX = "<S:";
|
||||
|
||||
private ChannelMetaTextParser() {}
|
||||
|
||||
@@ -19,15 +21,16 @@ public final class ChannelMetaTextParser {
|
||||
boolean seenAvatar = false;
|
||||
|
||||
int offset = 0;
|
||||
while (text.startsWith("<SHiNE:", offset)) {
|
||||
while (startsWithTagPrefix(text, offset)) {
|
||||
int end = text.indexOf('>', offset);
|
||||
if (end < 0) throw new IllegalArgumentException("bad_channel_meta_tag");
|
||||
String body = text.substring(offset + "<SHiNE:".length(), end);
|
||||
int prefixLength = tagPrefixLength(text, offset);
|
||||
String body = text.substring(offset + prefixLength, end);
|
||||
if (body.startsWith("title;")) {
|
||||
if (seenTitle) throw new IllegalArgumentException("duplicate_channel_meta_title");
|
||||
seenTitle = true;
|
||||
title = parseTitleTag(body);
|
||||
} else if (body.startsWith("avatar;")) {
|
||||
} else if (body.startsWith("avatar;") || body.startsWith("ava;")) {
|
||||
if (seenAvatar) throw new IllegalArgumentException("duplicate_channel_meta_avatar");
|
||||
seenAvatar = true;
|
||||
Avatar avatar = parseAvatarTag(body);
|
||||
@@ -61,11 +64,14 @@ public final class ChannelMetaTextParser {
|
||||
}
|
||||
|
||||
private static Avatar parseAvatarTag(String body) {
|
||||
Map<String, String> fields = parseFields(body.substring("avatar;".length()));
|
||||
String rawFields = body.startsWith("ava;")
|
||||
? body.substring("ava;".length())
|
||||
: body.substring("avatar;".length());
|
||||
Map<String, String> fields = parseFields(rawFields);
|
||||
if (!"1".equals(fields.get("v"))) throw new IllegalArgumentException("bad_channel_meta_avatar_version");
|
||||
String ar = String.valueOf(fields.getOrDefault("ar", "")).trim();
|
||||
String sha256 = String.valueOf(fields.getOrDefault("sha256", "")).trim().toLowerCase();
|
||||
String sizeRaw = String.valueOf(fields.getOrDefault("size", "")).trim();
|
||||
String sizeRaw = String.valueOf(fields.containsKey("sz") ? fields.get("sz") : fields.getOrDefault("size", "")).trim();
|
||||
if (!ar.matches("^[A-Za-z0-9_-]{43}$")) throw new IllegalArgumentException("bad_channel_meta_avatar_ar");
|
||||
if (!sha256.matches("^[0-9a-f]{64}$")) throw new IllegalArgumentException("bad_channel_meta_avatar_sha256");
|
||||
long size;
|
||||
@@ -78,6 +84,16 @@ public final class ChannelMetaTextParser {
|
||||
return new Avatar(ar, sha256, size);
|
||||
}
|
||||
|
||||
private static boolean startsWithTagPrefix(String text, int offset) {
|
||||
return text.startsWith(LEGACY_PREFIX, offset) || text.startsWith(SHORT_PREFIX, offset);
|
||||
}
|
||||
|
||||
private static int tagPrefixLength(String text, int offset) {
|
||||
if (text.startsWith(LEGACY_PREFIX, offset)) return LEGACY_PREFIX.length();
|
||||
if (text.startsWith(SHORT_PREFIX, offset)) return SHORT_PREFIX.length();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseFields(String raw) {
|
||||
Map<String, String> out = new HashMap<>();
|
||||
for (String part : String.valueOf(raw == null ? "" : raw).split(";")) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Подключение других устройств по QR и типизированные сессии
|
||||
|
||||
## Зачем
|
||||
|
||||
QR-подключение других устройств сейчас есть как заготовка, но сценарий нужно довести до устойчивого состояния. Параллельно надо аккуратно оформить типизированные сессии homeserver-ов в PDA.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Довести QR-сценарий до стабильного подключения нового устройства.
|
||||
2. Нормально описать и хранить устройство как отдельную типизированную сессию.
|
||||
3. Согласовать это с серверной и UI-логикой.
|
||||
4. Проверить, что подключение работает одинаково на новом и повторном устройстве.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- в планах есть `сессионные homeserver-ы в PDA`;
|
||||
- в планах есть `подключение других устройств через QR`;
|
||||
- базовая заготовка уже существует, но сценарий считается нестабильным.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от текущих документов в `TODO/medium/`;
|
||||
- отдельно проверить, какие поля уже есть в PDA и UI.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Solana_Architecture/README.md`;
|
||||
- `TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md`;
|
||||
- `TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md`.
|
||||
@@ -1,28 +0,0 @@
|
||||
# ESP32 как личное файловое хранилище
|
||||
|
||||
## Зачем
|
||||
|
||||
Планируется использовать ESP32 как личное файловое хранилище SHiNE для переписок и вложений.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Продумать формат хранения файлов на устройстве.
|
||||
2. Согласовать загрузку и чтение файлов между UI, сервером и устройством.
|
||||
3. Проверить, как устройство показывает статусы и ошибки.
|
||||
4. Свести это с существующим homeserver/UI-прототипом.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- в списке будущих фич уже есть отдельная задача по ESP32S3 file storage;
|
||||
- для UI homeserver уже есть отдельная документация и скетч должны держаться синхронно.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
|
||||
- от документации по ESP32 UI homeserver.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
|
||||
- `TODO/README.md`;
|
||||
- документацию по ESP32 UI homeserver, если добавятся экраны или статусы.
|
||||
@@ -1,61 +0,0 @@
|
||||
# TODO
|
||||
|
||||
Папка для короткого списка ближайших и среднесрочных задач, которые уже обсуждались и пока отложены.
|
||||
|
||||
## Как использовать
|
||||
|
||||
- Один markdown-файл = одна задача.
|
||||
- В файле коротко фиксируем:
|
||||
- зачем это нужно;
|
||||
- что именно сделать;
|
||||
- что уже есть в коде;
|
||||
- откуда продолжать;
|
||||
- какие документы потом надо обновить.
|
||||
- Это не активная разработка. Тут только план и контекст.
|
||||
- Старую папку `docs/Future_Features/` считать архивной и больше не использовать как источник новых задач.
|
||||
|
||||
## Текущие задачи
|
||||
|
||||
- `2026-06-26_1800_корректное_завершение_за_30с.md` - дать сервису до 30 секунд на корректное завершение опасных операций перед рестартом.
|
||||
- `2026-06-26_1810_подключение_устройств_по_qr.md` - довести подключение других устройств по QR и перевести это в нормальные типизированные сессии.
|
||||
- `2026-06-26_1815_esp32_файловое_хранилище.md` - использовать ESP32 как личное файловое хранилище для переписок и вложений.
|
||||
|
||||
## Децентрализация
|
||||
|
||||
Текущий 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/2026-05-25_1106_telegram_agent_players.md` - разрешённые пользователи Telegram для агента, отдельные папки игроков, персональные истории и публикация краткого вопроса/ответа в общий канал.
|
||||
- `near/2026-05-25_1106_wallet_topup_solana_arweave.md` - пополнение Solana и Arweave через внешний сервис покупки с подсказкой и копированием адреса.
|
||||
|
||||
### medium
|
||||
|
||||
- `medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md` - репосты в каналах и тредах.
|
||||
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
||||
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
||||
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
|
||||
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
|
||||
- `medium/2026-08-09_esp32_переход_на_sendsignal_и_client_key.md` - перевести ESP32 и wallet-extension на единый `SendSignal`, обязательный `client key` и подготовить почву для будущего E2E-шифрования payload.
|
||||
- `medium/2026-07-22_переход_с_sqlite_на_postgresql.md` - завершить зачистку хвостов после перевода серверной БД с `SQLite` на `PostgreSQL`.
|
||||
|
||||
### dao_запуск
|
||||
|
||||
- `dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md` - ESP32 как аппаратный кошелёк: постоянная device-сессия на сервере, подтверждение операций на экране, делегированные сессии для браузера/телефона.
|
||||
|
||||
### far
|
||||
|
||||
- `far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через SHiNE/WebRTC DataChannel и обмен файлами по чанкам с адресацией по `SHA-256`.
|
||||
@@ -1,114 +0,0 @@
|
||||
# Homeserver: технические команды и передача файлов через SHiNE/WebRTC
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Идея на дальнее будущее: дать возможность обращаться к homeserver не только как к участнику сети SHiNE, но и как к удалённой технической точке управления.
|
||||
|
||||
Цели:
|
||||
- отправлять на homeserver технические команды в текстовом виде;
|
||||
- получать текстовый ответ на команду;
|
||||
- при наличии WebRTC DataChannel передавать части файлов в обе стороны;
|
||||
- хранить полученные файлы на SD-карте homeserver;
|
||||
- использовать единый механизм доставки как через сервер SHiNE, так и напрямую через DataChannel.
|
||||
|
||||
## Горизонт
|
||||
|
||||
`far` - идея без ближайшего срока реализации. Сейчас приоритет ниже, чем запуск и стабилизация основного проекта.
|
||||
|
||||
## Что именно имеется в виду
|
||||
|
||||
### 1. Единая модель технической команды
|
||||
|
||||
Техническая команда должна иметь единый смысл независимо от транспорта доставки:
|
||||
- через любой доступный сервер SHiNE;
|
||||
- через уже установленный WebRTC DataChannel.
|
||||
|
||||
Если конкретный транспорт недоступен, ответ по нему может не прийти. Это считается нормальным поведением протокола.
|
||||
|
||||
### 2. Команда как короткоживущий подписанный сигнал
|
||||
|
||||
У команды должны быть:
|
||||
- `commandId`;
|
||||
- временная метка;
|
||||
- TTL около 10 секунд;
|
||||
- криптографическая подпись.
|
||||
|
||||
Смысл такой:
|
||||
- если команда быстро дошла, homeserver подтверждает принятие;
|
||||
- если не дошла вовремя, команда считается протухшей;
|
||||
- отправитель может безопасно послать повтор;
|
||||
- при повторе homeserver отвечает либо `команда принята`, либо `уже выполнено ранее`.
|
||||
|
||||
Это даёт дедупликацию и безопасный resend без повторного выполнения действия.
|
||||
|
||||
### 3. Текстовые технические команды
|
||||
|
||||
Базовый сценарий похож на короткий удалённый shell-протокол, но на уровне строго ограниченных команд:
|
||||
- отправил строку-команду;
|
||||
- получил строку-ответ.
|
||||
|
||||
Команды не обязаны исполнять произвольный shell. Предпочтительная модель - белый список операций с контролируемым форматом аргументов и ответа.
|
||||
|
||||
### 4. Передача файлов только при наличии DataChannel
|
||||
|
||||
Если между устройствами есть WebRTC DataChannel, через него можно передавать технические сообщения для файлового обмена.
|
||||
|
||||
Предварительная модель:
|
||||
- имя файла = `SHA-256` содержимого;
|
||||
- можно запросить диапазон байт `from..to`;
|
||||
- можно отправить диапазон байт `from..to`;
|
||||
- homeserver хранит полученные данные на SD-карте;
|
||||
- если DataChannel нет, на запрос файловой передачи возвращается ответ в духе `не могу передать, нет data channel`.
|
||||
|
||||
Фактически файл-обмен должен быть частным случаем общего протокола технических команд.
|
||||
|
||||
### 5. Установка data-соединения по явной команде
|
||||
|
||||
Нужна техническая команда уровня:
|
||||
- `установить data-соединение`.
|
||||
|
||||
Ответ:
|
||||
- либо `да`, после чего запускается обычная процедура `offer/answer/ICE`;
|
||||
- либо `нет` и причина отказа.
|
||||
|
||||
### 6. Доставка на пользовательские сессии
|
||||
|
||||
Логика должна быть совместима с общей моделью SHiNE, где технические сигналы можно отправлять на конкретные активные сессии пользователя.
|
||||
|
||||
Идея:
|
||||
- на любую активную сессию пользователя можно посылать техническую команду;
|
||||
- контакт пользователя может инициировать такую техническую коммуникацию так же, как он уже инициирует звонок или другой служебный сигнал.
|
||||
|
||||
## Что нужно будет сделать при возврате к задаче
|
||||
|
||||
- Спроектировать отдельный формат технических команд и ack-ответов.
|
||||
- Решить, будет ли это новый тип служебных сообщений в существующем протоколе блокчейн/сигналинга или отдельная ветка поверх уже имеющихся transport-операций.
|
||||
- Отдельно продумать авторизацию: кто именно из контактов и какие команды имеет право слать.
|
||||
- Ограничить набор допустимых команд, чтобы не превратить механизм в небезопасный удалённый shell.
|
||||
- Спроектировать протокол чанков файлов: размер чанка, нумерация, повторная отправка, контроль целостности, дозагрузка, завершение файла.
|
||||
- Продумать хранение на SD-карте: временные файлы, сборка чанков, проверка итогового `SHA-256`, очистка мусора.
|
||||
- Продумать поведение при отсутствии DataChannel, таймаутах и дублирующихся командах.
|
||||
- Проверить, как это лучше встраивать в текущие клиентские сессии, звонки и homeserver-логику.
|
||||
|
||||
## Вопросы для будущего уточнения
|
||||
|
||||
- Это должен быть строго служебный протокол или пользователь сможет вызывать его и вручную из UI.
|
||||
- Нужен ли доступ только к заранее разрешённым каталогам/файлам.
|
||||
- Нужна ли двусторонняя синхронизация файлов или достаточно ручных команд `запросить кусок` / `отправить кусок`.
|
||||
- Нужно ли разрешать передачу файлов через сервер SHiNE как fallback, или файл-обмен должен идти только через DataChannel.
|
||||
- Какой максимальный размер файлов и допустимый объём хранения на SD-карте.
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Пока только зафиксирована идея и базовая концепция. Реализация не начиналась.
|
||||
|
||||
## Какие документы нужно будет обновить при реализации
|
||||
|
||||
- `docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд.
|
||||
- `docs/API/` если изменится публичный серверный API или появятся новые операции.
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий.
|
||||
- Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве.
|
||||
|
||||
## С какого места продолжать позже
|
||||
|
||||
Возвращаться к задаче только после стабилизации запуска проекта и базовых текущих функций. Начинать с проектирования протокола команд и матрицы прав доступа, а уже потом переходить к DataChannel-файлообмену.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Кошелёк и пополнение баланса сияния
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
- Ориентир:
|
||||
среднесрочно
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить кошелёк для внутреннего баланса сияния и пополнение этого баланса через блокчейн-логику проекта. Задача связана с регистрацией пользователя и будущим учётом баланса.
|
||||
|
||||
## Предполагаемый сценарий
|
||||
|
||||
1. Пользователь регистрируется и получает/подключает нужные кошельки.
|
||||
2. В интерфейсе появляется баланс сияния.
|
||||
3. Пользователь открывает пополнение баланса сияния.
|
||||
4. Система создаёт или принимает блокчейн-операцию пополнения.
|
||||
5. После подтверждения баланса UI обновляет значение.
|
||||
|
||||
## Что нужно продумать
|
||||
|
||||
1. Что именно является единицей баланса сияния.
|
||||
2. Где хранится состояние баланса: в существующем блокчейне SHiNE, Solana-модуле или комбинированно.
|
||||
3. Какая операция отвечает за пополнение.
|
||||
4. Нужно ли делать отдельную регистрацию кошелька сияния или использовать существующую регистрацию пользователя.
|
||||
5. Как баланс восстанавливается после перезагрузки клиента.
|
||||
6. Какие права нужны для пополнения и списания.
|
||||
7. Нужна ли история операций баланса.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Пополнение баланса сияния должно идти через основной блокчейн SHiNE или через Solana-программу.
|
||||
2. Нужна ли конвертация из SOL/AR в сияние.
|
||||
3. Кто может выпускать или начислять сияние.
|
||||
4. Нужно ли поддерживать перевод сияния между пользователями.
|
||||
5. Нужны ли лимиты, комиссии или статусы подтверждения.
|
||||
6. Какой экран должен показывать баланс: регистрация, профиль, кошелёк или отдельная страница.
|
||||
7. Нужно ли отображать неподтверждённый баланс отдельно от подтверждённого.
|
||||
|
||||
## Важное ограничение
|
||||
|
||||
Если для баланса сияния потребуется новый формат блокчейн-блока или изменение существующего формата, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение формата блокчейна.
|
||||
|
||||
Если потребуется новый серверный API или изменение существующих `op`, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение API.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- `docs/Blockchain/`, если появятся или изменятся блоки баланса.
|
||||
- `docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат.
|
||||
- `docs/API/`, если меняется серверный API.
|
||||
- после реализации отдельно согласовать ручную проверку.
|
||||
- Документацию Solana-регистрации, если баланс будет связан с Solana-модулем.
|
||||
|
||||
## Минимальная проверка в будущем
|
||||
|
||||
1. Новый пользователь видит корректный начальный баланс.
|
||||
2. Пополнение создаёт правильную операцию.
|
||||
3. Баланс обновляется после подтверждения.
|
||||
4. После перезагрузки UI баланс остаётся корректным.
|
||||
5. Ошибочные или повторные операции не начисляют баланс дважды.
|
||||
@@ -1,44 +0,0 @@
|
||||
# ESP32S3 как личное файловое хранилище SHiNE
|
||||
|
||||
## Горизонт
|
||||
|
||||
Среднесрочный: ближайшие недели или 1-2 месяца.
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Нужно проработать маленький физический сервер на ESP32S3 как персональное или доверенное файловое хранилище SHiNE.
|
||||
|
||||
Идея: при обмене сообщениями пользователи смогут использовать такой сервер для хранения своих файлов, вложений, файлов общих переписок и связанных данных.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
- Описать роль ESP32S3-сервера в общей архитектуре ключей и сессий.
|
||||
- Определить, какие ключи может хранить такое устройство.
|
||||
- Решить, хранит ли устройство только файлы или также подписывает пользовательские операции.
|
||||
- Описать протокол загрузки, скачивания и удаления файлов.
|
||||
- Определить правила шифрования файлов до отправки на устройство.
|
||||
- Продумать индексацию файлов для личных и общих переписок.
|
||||
- Решить, как устройство авторизуется на основном сервере SHiNE.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
- ESP32S3 должен работать как полностью локальное устройство или как публично доступный мини-сервер?
|
||||
- Нужен ли внешний relay, если устройство находится за NAT?
|
||||
- Какие ограничения по размеру файла считаем допустимыми?
|
||||
- Хранит ли устройство метаданные переписок или только зашифрованные blob-файлы?
|
||||
- Как восстанавливать доступ, если устройство потеряно или заменено?
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Код не реализован. Идея зафиксирована как будущая задача после описания модели ключей.
|
||||
|
||||
## Документы, которые нужно обновить при возврате
|
||||
|
||||
- `docs/Keys/README.md`
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
- `docs/API/`
|
||||
- `docs/Blockchain/`, если появятся новые блоки или команды для файлов.
|
||||
|
||||
## С какого места продолжать
|
||||
|
||||
Начать с короткого протокольного документа: роли устройства, авторизация, шифрование файлов, минимальные API-операции и сценарии восстановления.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Сессионные homeserver-ы в PDA пользователя
|
||||
|
||||
- Статус:
|
||||
`future`
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
|
||||
- Ориентир:
|
||||
после завершения первого этапа по пользовательским сессиям
|
||||
|
||||
- Основание:
|
||||
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
|
||||
|
||||
Это нужно, чтобы:
|
||||
|
||||
- хранить несколько homeserver-ов у одного пользователя одновременно;
|
||||
- различать обычные клиентские сессии и серверные сессии по явному типу;
|
||||
- дать расширяемый формат записи с версией;
|
||||
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
|
||||
|
||||
## Целевая идея
|
||||
|
||||
В пользовательском PDA должен появиться список записей сессий, где каждая запись содержит как минимум:
|
||||
|
||||
- `sessionType` (`u8`);
|
||||
- `sessionVersion` (`u8`);
|
||||
- `sessionName`;
|
||||
- `sessionPubKey`.
|
||||
|
||||
Предварительные значения:
|
||||
|
||||
- тип `1` - обычная пользовательская сессия;
|
||||
- тип `100` - homeserver пользователя;
|
||||
- версия `1` - первая рабочая версия формата записи сессии.
|
||||
|
||||
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
||||
|
||||
Важно: homeserver-ов у одного пользователя может быть несколько.
|
||||
|
||||
## Архитектурный принцип
|
||||
|
||||
Внутренний протокол взаимодействия должен оставаться транспортным.
|
||||
|
||||
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен:
|
||||
|
||||
- доставлять сообщения между сессиями;
|
||||
- доставлять сигналы звонков между сессиями;
|
||||
- хранить и маршрутизировать адресацию;
|
||||
- не принимать на себя бизнес-логику содержимого внутренних команд.
|
||||
|
||||
## Что уже подтверждается текущим кодом
|
||||
|
||||
- Личные сообщения уже доставляются по всем сессиям целевого пользователя с отдельным учётом доставки на каждую сессию.
|
||||
- Подтверждение доставки DM уже идёт отдельно по каждой сессии.
|
||||
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
|
||||
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
|
||||
|
||||
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а".
|
||||
|
||||
## Что нужно сделать при возврате к задаче
|
||||
|
||||
1. Согласовать финальный бинарный формат записи сессии в PDA пользователя.
|
||||
2. Проверить, не меняет ли это уже опубликованный формат пользовательской PDA-записи.
|
||||
3. Если формат PDA меняется, заранее предупредить пользователя и получить отдельное подтверждение.
|
||||
4. Решить, где именно хранится массив сессий:
|
||||
- в основной записи пользователя;
|
||||
- в отдельной PDA-структуре расширения;
|
||||
- или в смешанной схеме с базовой записью и внешними индексами.
|
||||
5. Зафиксировать ограничения:
|
||||
- максимальное число сессий;
|
||||
- максимальную длину `sessionName`;
|
||||
- правила удаления и обновления записи;
|
||||
- правила ротации `sessionPubKey`.
|
||||
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
||||
7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
||||
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
||||
9. После реализации обязательно обновить документацию.
|
||||
|
||||
## Что нужно обновить при реализации
|
||||
|
||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||
- `docs/Solana_Architecture/README.md`
|
||||
- `docs/Инициализация_Solana_регистрации/README.md`
|
||||
- `docs/Keys/README.md`
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится адресация DM по типам сессий
|
||||
- `docs/API/`, если появятся новые серверные операции или изменятся ответы
|
||||
|
||||
## Что пока не делать
|
||||
|
||||
- Не включать это автоматически в основной deploy сервера.
|
||||
- Не менять сейчас Solana PDA-формат без отдельного подтверждения.
|
||||
- Не добавлять временные поля в публичный API "на всякий случай".
|
||||
|
||||
## С какого места продолжать
|
||||
|
||||
Продолжать после завершения первой части:
|
||||
|
||||
1. описать минимальный формат записи пользовательской сессии;
|
||||
2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии;
|
||||
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Подключение других устройств через QR
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
- Ориентир:
|
||||
позже, не сейчас
|
||||
- Статус:
|
||||
`future`
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Нужно нормально довести подключение другого устройства через QR-код. Сейчас есть полуготовая заготовка, но сценарий работает нестабильно и требует отдельной доработки.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- В UI уже есть экраны:
|
||||
- `shine-UI/js/pages/connect-device-view.js`
|
||||
- `shine-UI/js/pages/device-qr-view.js`
|
||||
- Есть сервис переноса ключей через QR:
|
||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
||||
- Логика частично собрана, но её нельзя считать завершённой или надёжной.
|
||||
|
||||
## Что нужно будет сделать потом
|
||||
|
||||
1. Проверить и довести формат QR-передачи.
|
||||
2. Проверить сканирование и ручной ввод QR-текста.
|
||||
3. Проверить перенос `device`, `blockchain`, `root` ключей только по реальному наличию на исходном устройстве.
|
||||
4. Проверить, что после переноса очищается старая история нужного логина и не ломается вход.
|
||||
5. Отдельно проверить сценарий без `BarcodeDetector`.
|
||||
6. Довести экран подтверждения на втором устройстве.
|
||||
|
||||
## Что сейчас важно
|
||||
|
||||
- Не считать эту часть готовой.
|
||||
- Не возвращать её в активную разработку без отдельной команды пользователя.
|
||||
- Если вернёмся к задаче, сначала нужно понять, что именно уже работает, а что нет, и потом починить целиком.
|
||||
|
||||
## Что обновить при возврате
|
||||
|
||||
- после реализации отдельно согласовать ручную проверку
|
||||
- `shine-UI/js/pages/connect-device-view.js`
|
||||
- `shine-UI/js/pages/device-qr-view.js`
|
||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
||||
- документацию по ключам, если формат переноса меняется
|
||||
@@ -1,29 +0,0 @@
|
||||
# Перенести старые сессионные сигналы на `SendSignal`
|
||||
|
||||
## Контекст
|
||||
|
||||
В проект добавлен новый общий межсессионный transport `SendSignal`.
|
||||
|
||||
Первое текущее применение:
|
||||
|
||||
- `remote AddBlock via homeserver session`
|
||||
|
||||
Старые сценарии пока оставлены на прежнем транспорте, чтобы не ломать уже работающий код.
|
||||
|
||||
## Что перенести позже
|
||||
|
||||
1. Звонковые сигналы, которые сейчас идут через `CallSignalToSession`.
|
||||
2. Старый wallet/ESP32 обмен, где технические команды всё ещё привязаны к call-like транспорту.
|
||||
3. Остальные доверенные межсессионные команды одного пользователя.
|
||||
|
||||
## Что важно учесть при переносе
|
||||
|
||||
- не ломать обратную совместимость работающих звонков;
|
||||
- сохранить текущую маршрутизацию по `sessionId`;
|
||||
- договориться о едином `signalType`;
|
||||
- отдельно описать миграцию клиентских обработчиков событий:
|
||||
- `IncomingCallSignal` -> `IncomingSignal`
|
||||
|
||||
## С какого сценария продолжать
|
||||
|
||||
Начинать перенос со звонков, но только после отдельной ручной проверки того, что `SendSignal` стабильно отработал на `remote AddBlock`.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Переход с SQLite на PostgreSQL
|
||||
|
||||
## Зачем
|
||||
|
||||
Переход runtime-сервера на `PostgreSQL` уже выполнен, но после него остались хвосты в документации, именах, комментариях и части прямых SQL-запросов.
|
||||
|
||||
Этот TODO теперь нужен не для самого перехода, а для доведения проекта до полностью консистентного состояния после ухода от `SQLite`.
|
||||
|
||||
## Что сделать
|
||||
|
||||
- Дочистить документацию, где ещё описан `SQLite` как текущий runtime.
|
||||
- Убрать или переименовать legacy-названия и комментарии, которые уже не соответствуют PostgreSQL runtime.
|
||||
- Постепенно перенести оставшиеся прямые SQL-запросы из хэндлеров в DAO/service.
|
||||
- Проверить case-insensitive сравнения, уникальные ограничения и индексы уже в чисто PostgreSQL модели.
|
||||
- Отдельно пройтись по TODO/служебным документам и убрать ссылки на удалённые SQLite-классы как на актуальный код.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- Доступ к БД в основном проходит через DAO-слой, а не полностью размазан по проекту.
|
||||
- Основная серверная логика уже разделена по модулям.
|
||||
- Runtime-сервер уже работает только с `PostgreSQL`.
|
||||
- Пустая БД инициализируется автоматически через `schema_v1`.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- Продолжать с зачистки legacy-документации и комментариев.
|
||||
- Затем добрать оставшиеся прямые SQL-запросы вне DAO.
|
||||
- После этого можно отдельно решать вопрос косметического переименования `*V2`, `DbController` и других переходных сущностей.
|
||||
|
||||
## Что потом обновить
|
||||
|
||||
- Серверную документацию по БД и миграциям.
|
||||
- Инструкции по локальному запуску сервера.
|
||||
- Скрипты деплоя и настройки окружения.
|
||||
|
||||
## Что временно отключено и что вернуть потом
|
||||
|
||||
- В серверном runtime временно снята проверка `channelName must not contain only digits`
|
||||
в `SHiNE-server/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java`.
|
||||
- Причина: на боевой истории уже есть блоки с числовыми именами каналов, и сервер
|
||||
должен уметь с нуля восстановить `blockchain_state` и `.bch`, подтягивая старые
|
||||
блоки от других sync-серверов.
|
||||
- Что осталось как текущее поведение:
|
||||
- UI по-прежнему не даёт создать новый канал только из цифр;
|
||||
- сервер принимает такие имена, чтобы не ломать replay старых блоков.
|
||||
- Что нужно сделать отдельным следующим шагом:
|
||||
- вернуть серверное продуктовое правило для новых каналов;
|
||||
- сделать это совместимо со старой историей, чтобы импорт/реплей существующих
|
||||
блоков не падал на старых числовых channel name.
|
||||
- Какие документы обновить при возврате:
|
||||
- `docs/libs/shine-server-bd/POSTGRES_RUNTIME_SCHEMA_V1.md`;
|
||||
- UI/серверные документы по правилам имён каналов, если появится отдельная спецификация.
|
||||
- С какого сценария продолжать:
|
||||
- повторить cold start тест на `t2`: пустая PostgreSQL schema, удалённые `.bch`,
|
||||
новый запуск, ожидание полной синхронизации от `t1`/`t3`.
|
||||
- Последняя полная рабочая точка с этим временным компромиссом:
|
||||
- ветка `migration-postgres`, коммит будет создан после этой записи.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Пополнение Solana и Arweave через внешний сервис покупки
|
||||
|
||||
- Горизонт:
|
||||
`near`
|
||||
- Ориентир:
|
||||
сегодня/завтра
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить удобное пополнение кошельков на экране регистрации/кошелька: для Solana и Arweave дать отдельные действия `Пополнить`, которые ведут на международный сервис покупки криптовалюты с карты и помогают пользователю скопировать адрес кошелька.
|
||||
|
||||
## Пользовательский сценарий
|
||||
|
||||
1. Пользователь видит адрес кошелька Solana или Arweave.
|
||||
2. Нажимает `Пополнить`.
|
||||
3. Открывается промежуточное окно с инструкцией:
|
||||
- сейчас пользователь перейдёт на страницу покупки/пополнения;
|
||||
- нужно указать или проверить адрес кошелька;
|
||||
- после оплаты нужно закрыть внешнюю страницу и вернуться назад;
|
||||
- Solana обычно приходит быстро, ориентир 10-15 секунд после подтверждения сети;
|
||||
- Arweave может идти дольше, точное время нужно уточнить по выбранному сервису.
|
||||
4. В окне есть кнопки:
|
||||
- `Скопировать адрес и перейти`;
|
||||
- `Перейти без копирования`.
|
||||
5. Для Solana и Arweave используются разные окна/инструкции и, возможно, разные внешние ссылки.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
1. Найти текущий экран, где показываются кошельки при регистрации и пополнении.
|
||||
2. Найти текущую ссылку покупки Arweave, если она уже есть в UI.
|
||||
3. Выбрать международный сервис покупки Solana с карты, не российский.
|
||||
4. Проверить, поддерживает ли сервис deep link с предзаполненным адресом кошелька.
|
||||
5. Если deep link невозможен, реализовать промежуточное окно с копированием адреса.
|
||||
6. Добавить отдельные действия для Solana и Arweave.
|
||||
7. Сделать текст инструкции коротким и понятным.
|
||||
8. Проверить, что адрес копируется в буфер обмена в браузере.
|
||||
9. Проверить мобильный сценарий и desktop-сценарий.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Какой сервис покупки Solana использовать: тот же провайдер, что для Arweave, или другой международный on-ramp.
|
||||
2. Нужно ли разрешать покупку только SOL или также USDC/SPL-токены на Solana.
|
||||
3. Где именно показывать кнопку `Пополнить`: только регистрация, настройки кошелька или оба места.
|
||||
4. Нужно ли показывать предупреждение о комиссиях и стороннем сервисе.
|
||||
5. Нужно ли открывать внешнюю страницу в новой вкладке или в текущем окне.
|
||||
6. Нужно ли логировать факт нажатия `Пополнить` на сервере.
|
||||
7. Какой точный текст использовать для времени прихода Arweave.
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
- On-ramp-сервисы меняют ссылки и параметры, поэтому deep link нужно проверять перед реализацией.
|
||||
- Clipboard API может требовать HTTPS и пользовательский жест.
|
||||
- Нельзя обещать точное время поступления средств: лучше писать ориентир и зависимость от сети/провайдера.
|
||||
- Внешний сервис может быть недоступен в отдельных странах или для отдельных карт.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- Документацию UI/кошельков, если такая есть.
|
||||
- после реализации отдельно согласовать ручную проверку.
|
||||
- `docs/API/`, только если появится новый серверный API или логирование.
|
||||
|
||||
## Минимальная проверка
|
||||
|
||||
1. На Solana-кошельке открывается правильное окно пополнения.
|
||||
2. Кнопка `Скопировать адрес и перейти` копирует Solana-адрес и открывает внешний сервис.
|
||||
3. Кнопка `Перейти без копирования` открывает внешний сервис без копирования.
|
||||
4. Аналогичный сценарий работает для Arweave.
|
||||
5. На мобильном экране текст и кнопки не перекрываются.
|
||||
6. Возврат назад в приложение не ломает состояние регистрации/кошелька.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Убрать временную очистку `signed_messages_v2` в миграции БД v11
|
||||
|
||||
## Зачем это нужно
|
||||
|
||||
При переходе на новый DM-протокол `SHiNE_DM v1` была добавлена временная миграция БД `v11`, которая при первом старте на старой базе полностью очищает:
|
||||
|
||||
- `signed_messages_v2`
|
||||
- `signed_message_session_delivery`
|
||||
|
||||
Это сделано как защитный reset, потому что старые DM-строки и backlog могли быть несовместимы с новым форматом, новой логикой tombstone и новым клиентским E2EE-разбором.
|
||||
|
||||
## Что именно потом сделать
|
||||
|
||||
- найти историческое место, где была добавлена временная миграция `migrateToV11()`, и убрать её остатки из runtime-логики/документации;
|
||||
- удалить helper `clearLegacySignedMessagesForDmV11(...)`;
|
||||
- поднять версию схемы дальше обычным образом уже без destructive-cleanup;
|
||||
- при необходимости заменить это на нормальную точечную миграцию старых DM-записей или совсем убрать поддержку старой истории.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- `LATEST_SCHEMA_VERSION = 11`;
|
||||
- при миграции в `v11` выполняется полная очистка DM-таблиц;
|
||||
- в коде прямо оставлен комментарий, что это временная мера.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
Продолжать от коммита, в котором была добавлена миграция `v11` для очистки DM-таблиц после перехода на `SHiNE_DM v1`.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится стратегия миграции старой истории;
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`, если появится отдельное правило совместимости/конвертации;
|
||||
- при необходимости `docs/API/12_Direct_Messages_Push_Calls_API.md`, если затронется поведение backlog/доставки.
|
||||
+8
@@ -1,5 +1,13 @@
|
||||
# Восстановить полную логику `shine_login_guard`
|
||||
|
||||
|
||||
Тоесть сделать что бы нормально проверялись логины пользователей
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Статус: отложено.
|
||||
|
||||
## Зачем это нужно
|
||||
@@ -0,0 +1 @@
|
||||
Передачу билетов владельцами со счёта на счёт
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Баланс в салане
|
||||
|
||||
баланс в Арвив / и турбо
|
||||
|
||||
Балан лимит МБ / оно же сияния токены SHN
|
||||
@@ -0,0 +1,3 @@
|
||||
Сделать поддержку нескольких залогиненных аккаунтов враз тоесть что бы можно было менять акаунт под которым заходить
|
||||
- подумать о деталях, а так хорошая тема - и тестировать удобнее станет
|
||||
-
|
||||
-10
@@ -27,13 +27,3 @@
|
||||
|
||||
- `BlockchainTmpRecoveryOnStartup` и `BlockchainResyncRecoveryOnStartup` уже умеют добирать незавершённые хвосты после старта.
|
||||
- `AddBlock` уже стал crash-safe через `tmp_bch` / `write_check` / `write_pending`.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- начать с `systemd`-юнита и базового shutdown-hook в сервере;
|
||||
- затем проверить, что текущие операции реально завершаются в отведённые 30 секунд.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `deploy/`;
|
||||
- `docs/Blockchain/sync-between-servers.md`, если изменится поведение остановки/восстановления.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Там были какието разные апи для ошибок звонка
|
||||
и для старта тестовых соединений
|
||||
|
||||
убрать короче лишнее
|
||||
|
||||
|
||||
и сделать номальное апи что бы клиент мог
|
||||
высылать уведомления об ошибках на сервер
|
||||
предлогал отправить уведомление
|
||||
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Сделать что бы если сервера с исходящими сообщениями не могут доставить на входящие то
|
||||
- делать повторные попытки через время
|
||||
- если так и не получилось и никужа не доставлено уведомлять пользователя
|
||||
|
||||
- Так же как вариант собирать подписи что полученно входящее сообщение с серверов пользователя полчателя
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя
|
||||
И всё это можно сделать внутри ESP32
|
||||
|
||||
Хотя не понятно надо ли так делать - потому что вроде удобно,
|
||||
но тем не менее и сложно как то объяснить такой функционал людям
|
||||
@@ -1,43 +0,0 @@
|
||||
# Постоянный server-to-server WS и DM sync
|
||||
|
||||
## Зачем
|
||||
|
||||
Текущий production-режим SHiNE рассчитан на один основной сервер. Межсерверная синхронизация относится к будущей децентрализации и не должна блокировать выкладку односерверной production-версии.
|
||||
|
||||
Сейчас синхронизация между серверами работает в основном как periodic sync и one-shot push. Для нормальной репликации в будущем ещё нужен постоянный межсерверный канал:
|
||||
|
||||
- живое подключение к партнёру;
|
||||
- push новых блоков;
|
||||
- push DM;
|
||||
- ACK на доставку;
|
||||
- backoff/reconnect;
|
||||
- стартовый backfill.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Поднять постоянное WebSocket-соединение между партнёрскими серверами.
|
||||
2. Сделать push новых блоков сразу после `AddBlock`.
|
||||
3. Сделать push DM-блоков между серверами.
|
||||
4. Добавить ACK и повторную отправку при сбое.
|
||||
5. Ввести стартовый обмен курсорами и добор хвоста.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- `ListBlockchainHeads`;
|
||||
- `GetBlockchainBlock`;
|
||||
- `GetSyncUserProfile`;
|
||||
- базовый periodic sync;
|
||||
- базовый backfill хвоста;
|
||||
- базовый full resync при divergence.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от текущего `sync_servers` bootstrap и `PeriodicBlockchainSyncService`;
|
||||
- дальше выделить отдельный межсерверный transport layer.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Blockchain/sync-between-servers.md`;
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`;
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`;
|
||||
- `docs/API/`.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Децентрализация
|
||||
|
||||
Папка для задач, которые нужны для будущего режима с несколькими серверами, 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, перенесённый в контекст децентрализации.
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Запись блокчейнов в Arweave
|
||||
|
||||
## Зачем
|
||||
|
||||
Для будущей децентрализации нужно долговременное внешнее хранение блокчейнов, чтобы данные не зависели только от одного серверного диска.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Определить, какие блокчейны и какие диапазоны блоков записываются в Arweave.
|
||||
2. Зафиксировать формат пачки блоков, метаданных, ссылок и контрольных хэшей.
|
||||
3. Добавить безопасный механизм публикации без хранения приватного JWK в git.
|
||||
4. Добавить проверку уже загруженных диапазонов, чтобы не плодить дубли.
|
||||
5. Описать восстановление блокчейна из Arweave при потере локальных данных.
|
||||
|
||||
## Важные ограничения
|
||||
|
||||
- Любое изменение формата блокчейна требует отдельного предупреждения и явного подтверждения пользователя.
|
||||
- Добавление данных в блокчейн должно выполняться только через `AddBlock`.
|
||||
- Секреты Arweave нельзя хранить в репозитории.
|
||||
|
||||
## Документы, которые потом нужно обновить
|
||||
|
||||
- `docs/Blockchain/README.md`;
|
||||
- `docs/Blockchain/CHANGELOG.md`;
|
||||
- документы deploy/секретов в `deploy/`, если появятся новые параметры.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Межсерверная передача сообщений
|
||||
|
||||
## Зачем
|
||||
|
||||
Когда у 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`.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Межсерверные звонки
|
||||
|
||||
## Зачем
|
||||
|
||||
В будущем пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера.
|
||||
|
||||
## Что сделать
|
||||
|
||||
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 маршрутизации.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,23 +0,0 @@
|
||||
# Односерверный 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,6 @@
|
||||
Сделать все эти: друг, близкий друг, и статусы типо сияет, точно сияет, реальный человек и т.д.
|
||||
|
||||
и как вариант не реальный человек тк
|
||||
- украли акаунт
|
||||
- сразу был нереальным мошенником
|
||||
- умер
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
# Новая логика контента в блокчейне 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, не разрушая старую модель сообщений.
|
||||
|
||||
## Отдельный вопрос для будущего
|
||||
|
||||
Отзывы о людях как о людях — полезная идея, но её стоит дополнительно обдумать.
|
||||
|
||||
Например, на вкладке связей в будущем можно:
|
||||
|
||||
- писать человеку отзыв;
|
||||
- смотреть все отзывы о человеке;
|
||||
- выводить сначала отзывы близких друзей, родственников, друзей и контактов, а уже потом остальные.
|
||||
|
||||
Но этот слой нужно делать осторожно, чтобы он не стал слишком жёстким или неприятным для людей.
|
||||
|
||||
Поэтому отзывы о людях как отдельная социальная механика требуют дополнительного обсуждения и проектирования.
|
||||
-670
@@ -1,670 +0,0 @@
|
||||
# ТЗ: новая контентная модель блокчейна 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` продолжают работать как раньше;
|
||||
- старые клиенты смогут игнорировать новые типы как неизвестные;
|
||||
- новые клиенты смогут постепенно включать поддержку нового функционала.
|
||||
|
||||
Итог:
|
||||
|
||||
- это расширение формата блокчейна;
|
||||
- это не миграция со сломом старых блоков;
|
||||
- это можно внедрять поэтапно.
|
||||
@@ -0,0 +1,2 @@
|
||||
Доделть мелочи
|
||||
и разместить проект нормально на гитхаб
|
||||
@@ -0,0 +1,7 @@
|
||||
# Межсерверные звонки
|
||||
|
||||
Доделать Звонки что бы работало как сигнал о том что вызов идёт.
|
||||
|
||||
и
|
||||
Пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера.
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
client.version=1.5.35
|
||||
server.version=1.4.11
|
||||
client.version=1.5.37
|
||||
server.version=1.4.12
|
||||
|
||||
@@ -160,20 +160,20 @@
|
||||
|
||||
### Вложения в сообщениях
|
||||
|
||||
Для `TEXT_POST`, `TEXT_REPLY`, `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` вложения записываются в начало текста сообщения одним или несколькими тегами `SHiNE:attach v=1` или `v=2`.
|
||||
Для `TEXT_POST`, `TEXT_REPLY`, `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` вложения записываются в начало текста сообщения одним или несколькими тегами `S:att v=1`.
|
||||
|
||||
Пример текстового содержимого body:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>
|
||||
<SHiNE:attach;v=1;name=report.pdf;size=845221;sha256=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc;ar=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD>
|
||||
<S:att;v=1;nm=photo.jpg;sz=248193;sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>
|
||||
<S:att;v=1;nm=report.pdf;sz=845221;sha256=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc;ar=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD>
|
||||
Текст сообщения
|
||||
```
|
||||
|
||||
Пример вложения с отдельным preview-файлом:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=2;name=video.mp4;size=5820193;sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;previewAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;previewSha256=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee>
|
||||
<S:att;v=1;nm=video.mp4;sz=5820193;sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;preAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;preSha256=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee>
|
||||
```
|
||||
|
||||
Сервер хранит это как обычный `TEXT`-блок. Отображение карусели, картинок, видео и карточек файлов делает клиент. Полная спецификация тега находится в `docs/Blockchain/15_TEXT_Attachments.md`.
|
||||
@@ -183,8 +183,8 @@
|
||||
Для публичного канала начальный профиль пишется одним блоком `TECH_CREATE_CHANNEL`. Поле `channelDescription` содержит meta-текст:
|
||||
|
||||
```text
|
||||
<SHiNE:title;v=1;Человекочитаемое имя канала>
|
||||
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
<S:title;v=1;Человекочитаемое имя канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
Описание канала
|
||||
```
|
||||
|
||||
@@ -197,12 +197,12 @@
|
||||
Текстовое содержимое body использует тот же формат полного снимка профиля:
|
||||
|
||||
```text
|
||||
<SHiNE:title;v=1;Новое имя канала>
|
||||
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
<S:title;v=1;Новое имя канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
Новое описание канала
|
||||
```
|
||||
|
||||
Каждый `TEXT_CHANNEL_META` является полным состоянием профиля на момент записи. Если аватара нет, тег `SHiNE:avatar` не пишется. Если описания нет, после meta-тегов не добавляется хвостовой текст. Полная спецификация находится в `docs/Blockchain/16_TEXT_Channel_Meta.md`.
|
||||
Каждый `TEXT_CHANNEL_META` является полным состоянием профиля на момент записи. Если аватара нет, тег `S:ava` не пишется. Если описания нет, после meta-тегов не добавляется хвостовой текст. Полная спецификация находится в `docs/Blockchain/16_TEXT_Channel_Meta.md`.
|
||||
|
||||
## 7. Хватает ли функций сейчас
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ Payload включает:
|
||||
Для публичных каналов (`channelType=1`) поле является начальным снимком профиля канала и использует тот же текстовый meta-формат, что `TEXT_CHANNEL_META`:
|
||||
|
||||
```text
|
||||
<SHiNE:title;v=1;Название канала>
|
||||
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||
<S:title;v=1;Название канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||
Описание канала.
|
||||
```
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ TEXT-тип хранит сообщения, материалы и редакт
|
||||
|
||||
7. `subType=90` — `TEXT_CHANNEL_META`
|
||||
- скрытый технический снимок профиля канала;
|
||||
- содержит line-поля + текст с тегами `SHiNE:title`/`SHiNE:avatar` и описанием;
|
||||
- содержит line-поля + текст с тегами `S:title`/`S:ava` и описанием;
|
||||
- не отображается как обычное сообщение ленты;
|
||||
- применяется сервером к текущему состоянию канала.
|
||||
|
||||
@@ -73,7 +73,7 @@ TEXT-тип хранит сообщения, материалы и редакт
|
||||
- Такой edit трактуется как логическое удаление содержимого сообщения.
|
||||
- Для удаления используется именно edit-блок; отдельного `DELETE`-подтипа нет.
|
||||
|
||||
## Вложения в текстовых сообщениях (`SHiNE:attach v=1/v=2`)
|
||||
## Вложения в текстовых сообщениях (`S:att v=1`)
|
||||
|
||||
Для `TEXT_POST`, `TEXT_REPLY`, `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` клиент может хранить вложения как технические строки в начале обычного `text`.
|
||||
|
||||
@@ -84,31 +84,31 @@ TEXT-тип хранит сообщения, материалы и редакт
|
||||
Один блок вложения:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=1;name=report.pdf;size=845221;sha256=0123...;ar=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
|
||||
<S:att;v=1;nm=report.pdf;sz=845221;sha256=0123...;ar=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
|
||||
```
|
||||
|
||||
Для файлов с отдельным превью клиент может использовать расширенный вариант:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=2;name=video.mp4;size=5820193;sha256=bbbb...;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;previewAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;previewSha256=eeee...>
|
||||
<S:att;v=1;nm=video.mp4;sz=5820193;sha256=bbbb...;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;preAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;preSha256=eeee...>
|
||||
```
|
||||
|
||||
Несколько вложений идут подряд в самом начале текста, по одному блоку на строку. После последнего блока идёт обычный пользовательский текст.
|
||||
|
||||
Обязательные поля:
|
||||
|
||||
- `v=1` или `v=2`
|
||||
- `name` - имя файла, закодированное через `encodeURIComponent`
|
||||
- `size` - размер файла в байтах
|
||||
- `v=1`
|
||||
- `nm` - имя файла, закодированное через `encodeURIComponent`
|
||||
- `sz` - размер файла в байтах
|
||||
- `sha256` - SHA-256 исходного файла в hex
|
||||
- `ar` - короткий Arweave Transaction ID, без полного URL
|
||||
- для `v=2` дополнительно могут присутствовать `previewAr` и `previewSha256`
|
||||
- при наличии превью дополнительно могут присутствовать `preAr` и `preSha256`
|
||||
|
||||
Правила клиента:
|
||||
|
||||
- если сообщение начинается с одного или нескольких валидных `<SHiNE:attach;...>` блоков, новый клиент скрывает эти блоки и показывает карточки вложений;
|
||||
- если сообщение начинается с одного или нескольких валидных attach-блоков (`<SHiNE:attach;...>`, `<S:attach;...>` или `<S:att;...>`), новый клиент скрывает эти блоки и показывает карточки вложений;
|
||||
- если блок битый, клиент может игнорировать только этот блок и продолжить разбор остальных;
|
||||
- старые клиенты без поддержки вложений могут показывать технические строки как обычный текст;
|
||||
- пустой пользовательский текст допустим, если перед ним есть хотя бы один валидный attach-блок.
|
||||
|
||||
Файлы хранятся вне блокчейна, в Arweave. В блокчейне остаются только `txId`, имя, размер, SHA-256 и, при наличии отдельного preview-файла, `previewAr/previewSha256`.
|
||||
Файлы хранятся вне блокчейна, в Arweave. В блокчейне остаются только `txId`, имя, размер, SHA-256 и, при наличии отдельного preview-файла, `preAr/preSha256`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Вложения в TEXT-сообщениях (`SHiNE:attach v=1/v=2`)
|
||||
# Вложения в TEXT-сообщениях (`S:att v=1`)
|
||||
|
||||
Документ фиксирует текущий формат вложений в текстовых блоках SHiNE.
|
||||
|
||||
@@ -15,23 +15,23 @@
|
||||
|
||||
## Общий вид
|
||||
|
||||
Один attach-блок:
|
||||
Канонический attach-блок:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=1;name=report.pdf;size=845221;sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef;ar=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
|
||||
<S:att;v=1;nm=report.pdf;sz=845221;sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef;ar=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
|
||||
```
|
||||
|
||||
Блок с превью:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=2;name=video.mp4;size=5820193;sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;previewAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;previewSha256=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee>
|
||||
<S:att;v=1;nm=video.mp4;sz=5820193;sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;preAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;preSha256=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee>
|
||||
```
|
||||
|
||||
Несколько вложений идут подряд в самом начале текста:
|
||||
|
||||
```text
|
||||
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>
|
||||
<SHiNE:attach;v=1;name=report.pdf;size=845221;sha256=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc;ar=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD>
|
||||
<S:att;v=1;nm=photo.jpg;sz=248193;sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>
|
||||
<S:att;v=1;nm=report.pdf;sz=845221;sha256=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc;ar=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD>
|
||||
Текст сообщения
|
||||
```
|
||||
|
||||
@@ -41,18 +41,18 @@
|
||||
|
||||
## Поля
|
||||
|
||||
Обязательные поля:
|
||||
Обязательные поля канонического формата:
|
||||
|
||||
- `v=1` - версия формата attach-блока;
|
||||
- `name` - имя файла, закодированное через `encodeURIComponent`;
|
||||
- `size` - размер файла в байтах;
|
||||
- `nm` - имя файла, закодированное через `encodeURIComponent`;
|
||||
- `sz` - размер файла в байтах;
|
||||
- `sha256` - SHA-256 исходного файла в hex, 64 символа;
|
||||
- `ar` - короткий Arweave Transaction ID, 43 символа, без gateway URL.
|
||||
|
||||
Дополнительные поля для `v=2`:
|
||||
Дополнительные поля для превью:
|
||||
|
||||
- `previewAr` - короткий Arweave Transaction ID файла-превью;
|
||||
- `previewSha256` - SHA-256 файла-превью в hex.
|
||||
- `preAr` - короткий Arweave Transaction ID файла-превью;
|
||||
- `preSha256` - SHA-256 файла-превью в hex.
|
||||
|
||||
## Хранение файла
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
- SHA-256;
|
||||
- Arweave `txId`.
|
||||
|
||||
Опционально для видео или больших изображений может храниться отдельный второй файл-превью. В таком случае основной attach-блок остаётся одним, но дополнительно указывает `previewAr/previewSha256`.
|
||||
Опционально для видео или больших изображений может храниться отдельный второй файл-превью. В таком случае основной attach-блок остаётся одним, но дополнительно указывает `preAr/preSha256`.
|
||||
|
||||
## Создание вложения в UI
|
||||
|
||||
@@ -78,7 +78,7 @@ UI поддерживает три сценария:
|
||||
- основной видеофайл;
|
||||
- отдельное изображение-превью.
|
||||
|
||||
После успешной загрузки в журнале хранится один элемент основного файла, но с полями `previewAr/previewSha256`. В UI такой элемент помечается как файл `С превью`.
|
||||
После успешной загрузки в журнале хранится один элемент основного файла, но с полями `preAr/preSha256`. В UI такой элемент помечается как файл `С превью`.
|
||||
|
||||
При ручном добавлении существующего `txId` для видео UI также позволяет вручную указать `txId` файла-превью.
|
||||
|
||||
@@ -99,7 +99,7 @@ UI поддерживает три сценария:
|
||||
- изображение показывает как ограниченное по размеру превью;
|
||||
- видео показывает как превью с кнопкой воспроизведения и открывает большой HTML5-плеер по нажатию;
|
||||
- обычный файл показывает как карточку с именем, расширением, размером и скачиванием;
|
||||
6. если у видео есть `previewAr/previewSha256`, использует отдельный preview-файл как `poster` и как большую превью-плитку в журнале загрузок;
|
||||
6. если у видео есть `preAr/preSha256`, использует отдельный preview-файл как `poster` и как большую превью-плитку в журнале загрузок;
|
||||
7. при перелистывании останавливает воспроизводящееся видео;
|
||||
8. если attach-блок битый, игнорирует только этот блок и продолжает отображать сообщение.
|
||||
|
||||
@@ -118,7 +118,9 @@ UI поддерживает три сценария:
|
||||
|
||||
- `ar` допускает только короткий Arweave `txId`, полный URL не используется;
|
||||
- MIME type не записывается, поэтому UI определяет image/video/file по расширению имени файла;
|
||||
- старые блоки `v=1` без превью остаются валидными и читаются без изменений;
|
||||
- `previewAr/previewSha256` используются только если присутствуют оба поля и оба валидны;
|
||||
- старые блоки `<SHiNE:attach ...>`, промежуточные `<S:attach ...>` и новые `<S:att ...>` читаются одинаково;
|
||||
- старые поля `name/size/previewAr/previewSha256` и новые `nm/sz/preAr/preSha256` читаются одинаково;
|
||||
- старые блоки `v=2` продолжают читаться как legacy-форма вложения с превью;
|
||||
- `preAr/preSha256` используются только если присутствуют оба поля и оба валидны;
|
||||
- MIME type, width, height, duration и thumbnail не записываются в блокчейн отдельными полями;
|
||||
- проверка существующего `txId` скачивает файл локально через gateway, поэтому UI ограничивает максимальный размер такой проверки.
|
||||
|
||||
@@ -18,15 +18,15 @@
|
||||
В начале текста могут идти технические теги, после них обычный текст описания:
|
||||
|
||||
```text
|
||||
<SHiNE:title;v=1;Название канала>
|
||||
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||
<S:title;v=1;Название канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||
Описание канала.
|
||||
```
|
||||
|
||||
Поддерживаемые теги:
|
||||
|
||||
- `<SHiNE:title;v=1;...>` — человекочитаемое имя канала.
|
||||
- `<SHiNE:avatar;v=1;size=...;sha256=...;ar=...>` — аватар канала в Arweave.
|
||||
- `<S:title;v=1;...>` — человекочитаемое имя канала.
|
||||
- `<S:ava;v=1;sz=...;sha256=...;ar=...>` — аватар канала в Arweave.
|
||||
|
||||
## Правила
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
- Длина описания — максимум 250 Unicode code points.
|
||||
- `avatar.ar` — Arweave transaction id из 43 символов.
|
||||
- `avatar.sha256` — 64 hex-символа.
|
||||
- `avatar.size` — положительный размер файла в байтах.
|
||||
- `avatar.sz` — положительный размер файла в байтах.
|
||||
|
||||
Если meta-блок невалиден, сервер не применяет его целиком.
|
||||
|
||||
@@ -59,3 +59,9 @@
|
||||
## Замена старых команд
|
||||
|
||||
Команда `/.desc` больше не используется и не применяется сервером. Описание канала меняется только через `TEXT_CHANNEL_META`.
|
||||
|
||||
## Совместимость
|
||||
|
||||
- Новый UI пишет сокращённые теги `<S:title ...>` и `<S:ava ...>`.
|
||||
- Серверное чтение поддерживает и старые теги `<SHiNE:title ...>` / `<SHiNE:avatar ...>`.
|
||||
- Для размера аватара чтение поддерживает и старое поле `size`, и новое поле `sz`.
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# История изменений документации блокчейна
|
||||
|
||||
## 2026-08-12 13:00:00 +0400
|
||||
- Базовый коммит-ориентир: `working tree`.
|
||||
- Канонический текстовый формат служебных тегов сокращён:
|
||||
- `TEXT_CHANNEL_META` теперь пишет `<S:title>` и `<S:ava>`;
|
||||
- вложения теперь пишутся как `<S:att;v=1;nm=...;sz=...;sha256=...;ar=...>`;
|
||||
- превью во вложениях задаётся полями `preAr/preSha256` без отдельной новой версии `v=2`;
|
||||
- DM-вставки теперь пишутся с префиксом `<S:`.
|
||||
- Зафиксирована обратная совместимость чтения:
|
||||
- старые теги `<SHiNE:...>` продолжают поддерживаться;
|
||||
- старые поля `name/size/previewAr/previewSha256` продолжают поддерживаться;
|
||||
- legacy-вложения `v=2` продолжают читаться как вложения с превью.
|
||||
|
||||
## 2026-08-09 23:30:06 +0400
|
||||
- Базовый коммит-ориентир: `ee185cf`.
|
||||
- Нумерация `STATUS_ACTION` уточнена под дневник действий:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
8. [15_STATUS_ACTION_Blocks.md](./15_STATUS_ACTION_Blocks.md)
|
||||
Статусные действия пользователя (`msg_type=5`).
|
||||
9. [16_TEXT_Attachments.md](./16_TEXT_Attachments.md)
|
||||
Вложения в TEXT-сообщениях через `SHiNE:attach v=1/v=2`, включая опциональные `previewAr/previewSha256` для видео и крупных изображений.
|
||||
Вложения в TEXT-сообщениях через `S:att v=1`, включая опциональные `preAr/preSha256` для видео и крупных изображений.
|
||||
10. [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md)
|
||||
Скрытый `TEXT_CHANNEL_META` для профиля канала.
|
||||
11. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Figma
|
||||
|
||||
Эта папка хранит рабочие инструкции по переносу экранов SHiNE в Figma и по обратному переносу изменений из Figma в код.
|
||||
|
||||
## Что здесь лежит
|
||||
|
||||
- `README.md` — точка входа и краткий регламент.
|
||||
- `TRANSFER_UI_SCREENS.md` — подробная инструкция по переносу экранов UI в Figma и обратно.
|
||||
|
||||
## Когда читать
|
||||
|
||||
Читать перед любыми задачами вида:
|
||||
- перенести экран из `shine-UI` в Figma;
|
||||
- собрать новый Figma-файл для экранов SHiNE;
|
||||
- перенести изменения из Figma обратно в код;
|
||||
- уточнить, каким способом переносить экраны: по одному или пачкой.
|
||||
|
||||
## Ключевое правило
|
||||
|
||||
Для экранов SHiNE безопасный рабочий способ на текущий момент:
|
||||
- переносить экраны в Figma по одному;
|
||||
- не пытаться сразу переносить длинный auth-flow пачкой;
|
||||
- после каждого переноса визуально проверять результат в самой Figma;
|
||||
- только после удачного одного экрана переходить к следующему.
|
||||
|
||||
## Про Miro
|
||||
|
||||
Отдельной папки `Miro` пока нет.
|
||||
|
||||
Причина:
|
||||
- практики по Miro в проекте пока мало;
|
||||
- устойчивого процесса ещё нет;
|
||||
- как только появится стабильный сценарий работы с Miro, его нужно будет оформить аналогично Figma.
|
||||
@@ -1,222 +0,0 @@
|
||||
# Перенос экранов UI в Figma и обратно
|
||||
|
||||
## Зачем нужен этот документ
|
||||
|
||||
Этот документ фиксирует практический опыт, который уже был получен на переносе стартового экрана, экрана регистрации и дальнейших попытках.
|
||||
|
||||
Главная цель:
|
||||
- чтобы агент не повторял неудачные попытки;
|
||||
- чтобы переносы делались одинаково;
|
||||
- чтобы изменения из Figma можно было уверенно переносить назад в `shine-UI`.
|
||||
|
||||
## Где находится основной UI
|
||||
|
||||
- основной клиентский UI: `shine-UI/`
|
||||
- маршруты и список pre-auth экранов: `shine-UI/js/router.js`
|
||||
- экраны: `shine-UI/js/pages/`
|
||||
- общие стили: `shine-UI/styles/main.css`, `shine-UI/styles/layout.css`, `shine-UI/styles/components.css`
|
||||
|
||||
## Что считать успешным переносом в Figma
|
||||
|
||||
Успешный перенос экрана в Figma — это не просто фон и прямоугольники.
|
||||
|
||||
Нужно, чтобы:
|
||||
- были видны все ключевые текстовые элементы;
|
||||
- кнопки были перенесены как отдельные элементы;
|
||||
- поля ввода были явно видны;
|
||||
- экран был узнаваем визуально;
|
||||
- пользователь мог вручную подправить макет в Figma;
|
||||
- после правок можно было понять, что именно переносить обратно в код.
|
||||
|
||||
## Текущий рабочий способ
|
||||
|
||||
На текущем проекте лучший практический способ такой:
|
||||
|
||||
1. Переносить только один экран за раз.
|
||||
2. Сначала читать конкретный `js/pages/<screen>.js`.
|
||||
3. Затем читать связанные стили из `styles/components.css` и `styles/layout.css`.
|
||||
4. После этого вручную собирать экран в Figma как отдельный frame с явными элементами.
|
||||
5. Проверять в Figma, что не получился только фон без текста и контролов.
|
||||
6. Только после успешной проверки переходить к следующему экрану.
|
||||
|
||||
## Почему нельзя переносить пачкой
|
||||
|
||||
Был получен негативный опыт:
|
||||
- при переносе сразу многих экранов в Figma часть экранов отображалась как фон без нормальных надписей и элементов;
|
||||
- длинные экраны с большим количеством текста и форм разваливались;
|
||||
- автогенерация давала внешний вид, непригодный для ручной доработки.
|
||||
|
||||
Поэтому правило такое:
|
||||
- auth-flow, регистрация, вход, onboarding — переносить по одному экрану;
|
||||
- после каждого экрана ждать визуального подтверждения пользователя;
|
||||
- не объединять 5-10 экранов в один проход без отдельного разрешения и без промежуточной проверки.
|
||||
|
||||
## Рекомендуемый порядок переноса в Figma
|
||||
|
||||
### Вперёд: код -> Figma
|
||||
|
||||
1. Определить точный экран.
|
||||
2. Найти файл экрана в `shine-UI/js/pages/`.
|
||||
3. Найти используемые CSS-классы через поиск по файлу экрана.
|
||||
4. Вытащить:
|
||||
- тексты;
|
||||
- состав кнопок;
|
||||
- поля ввода;
|
||||
- карточки;
|
||||
- блоки статуса;
|
||||
- последовательность секций.
|
||||
5. Если экран длинный, всё равно переносить его как один frame, но собирать блоками сверху вниз.
|
||||
6. В Figma создавать отдельный экран рядом с уже существующими экранами, а не смешивать всё в одну кучу.
|
||||
7. После создания экрана проверить метаданные/скриншот Figma, если инструмент это позволяет.
|
||||
|
||||
### Назад: Figma -> код
|
||||
|
||||
1. Снять актуальный скриншот изменённого Figma-экрана.
|
||||
2. Получить метаданные узла, если это помогает понять структуру.
|
||||
3. Сравнить Figma с текущим кодом экрана.
|
||||
4. Переносить обратно в код только реальные изменения:
|
||||
- порядок блоков;
|
||||
- тексты;
|
||||
- размеры/отступы;
|
||||
- наличие или отсутствие карточек;
|
||||
- подписи кнопок;
|
||||
- видимость блоков.
|
||||
5. Не придумывать новые UX-решения без отдельного подтверждения пользователя, если их нет в Figma.
|
||||
6. После правок проверять экран локально или как минимум по коду и зависимостям.
|
||||
|
||||
## Что переносить вручную
|
||||
|
||||
Вручную, а не автогенерацией, нужно переносить:
|
||||
- экраны регистрации;
|
||||
- экраны входа;
|
||||
- длинные формы;
|
||||
- экраны с несколькими карточками;
|
||||
- экраны с длинными объясняющими текстами;
|
||||
- экраны, где важен порядок блоков.
|
||||
|
||||
Причина:
|
||||
- именно они чаще всего ломаются при слишком автоматическом переносе.
|
||||
|
||||
## Какие ошибки уже были
|
||||
|
||||
### Ошибка 1. Перенос пачкой
|
||||
|
||||
Проблема:
|
||||
- несколько экранов были добавлены сразу;
|
||||
- пользователь увидел, что на экранах в Figma «какая-то ерунда».
|
||||
|
||||
Вывод:
|
||||
- переносить по одному.
|
||||
|
||||
### Ошибка 2. Видно только фон
|
||||
|
||||
Проблема:
|
||||
- frame создавался, фон и свечения были видны;
|
||||
- тексты и элементы либо не появлялись, либо получались непригодными.
|
||||
|
||||
Вывод:
|
||||
- при сложных экранах собирать элементы вручную и явно.
|
||||
|
||||
### Ошибка 3. Слишком вольная реконструкция
|
||||
|
||||
Проблема:
|
||||
- экран формально был перенесён, но визуально не соответствовал ожиданию пользователя.
|
||||
|
||||
Вывод:
|
||||
- для SHiNE важнее узнаваемый и редактируемый экран, чем «формально похожий» экран.
|
||||
|
||||
## Обязательные проверки после переноса в Figma
|
||||
|
||||
После каждого нового экрана агент должен проверить:
|
||||
- виден ли заголовок;
|
||||
- видны ли кнопки;
|
||||
- видны ли поля ввода;
|
||||
- не исчезли ли длинные тексты;
|
||||
- не сломан ли порядок секций;
|
||||
- не оказался ли на холсте только фон и пустые прямоугольники.
|
||||
|
||||
Если хотя бы один пункт не выполнен:
|
||||
- не считать перенос завершённым;
|
||||
- либо переделать экран сразу;
|
||||
- либо остановиться и показать пользователю только после исправления.
|
||||
|
||||
## Правила для длинных экранов
|
||||
|
||||
Если экран длинный, например регистрация:
|
||||
- высота frame может быть больше стандартной мобильной высоты;
|
||||
- секции должны идти в правильном вертикальном порядке;
|
||||
- отдельные карточки должны быть вынесены в отдельные блоки;
|
||||
- тексты лучше упрощённо располагать вручную, чем терять их совсем.
|
||||
|
||||
## Правила для экрана регистрации
|
||||
|
||||
Экран `register-view` особенно чувствительный.
|
||||
|
||||
При переносе нужно отдельно учитывать:
|
||||
- заголовок и стрелку назад;
|
||||
- поля логина и пароля;
|
||||
- строку статуса длины пароля;
|
||||
- строку статуса проверки логина;
|
||||
- кнопку проверки логина;
|
||||
- отдельную карточку первого сервера;
|
||||
- отдельную карточку FAQ;
|
||||
- нижние кнопки `Назад` и `Далее`.
|
||||
|
||||
## Правила для экрана входа
|
||||
|
||||
Для экранов входа важно не смешивать:
|
||||
- экран выбора способа входа;
|
||||
- вход по логину/паролю;
|
||||
- вход через другое устройство;
|
||||
- вход по QR.
|
||||
|
||||
Каждый из них переносить отдельно.
|
||||
|
||||
## Что делать после правок пользователя в Figma
|
||||
|
||||
Если пользователь изменил экран в Figma:
|
||||
|
||||
1. Считать Figma источником визуальной правки.
|
||||
2. Сначала понять, что именно изменено:
|
||||
- тексты;
|
||||
- порядок блоков;
|
||||
- наличие блоков;
|
||||
- размеры;
|
||||
- отступы;
|
||||
- логика flow.
|
||||
3. Переносить эти изменения назад в код минимально необходимыми правками.
|
||||
4. Если из Figma следует уже не только визуальная, но и UX-логическая правка, отдельно проверить, что она согласована пользователем.
|
||||
|
||||
## Когда нужно отдельно согласовать ручную проверку
|
||||
|
||||
Если после изменения по Figma:
|
||||
- поменялась логика flow;
|
||||
- поменялась регистрация/вход;
|
||||
- нужен реальный прогон на test2;
|
||||
- затронута интеграция с Solana;
|
||||
|
||||
тогда нужно отдельно согласовать ручную проверку с пользователем.
|
||||
|
||||
## Что пока не оформлено для Miro
|
||||
|
||||
По Miro пока нет устойчивого процесса.
|
||||
|
||||
Из того, что уже понятно:
|
||||
- пока не стоит обещать такой же отлаженный перенос, как для Figma;
|
||||
- сначала нужно накопить хотя бы 2-3 реальных сценария работы;
|
||||
- только после этого оформлять отдельную папку и регламент.
|
||||
|
||||
## Краткая памятка для агента
|
||||
|
||||
Если задача звучит как:
|
||||
- «перенеси экран в Figma»;
|
||||
- «добавь экран в Figma»;
|
||||
- «я поправил экран в Figma, перенеси назад»;
|
||||
|
||||
то агент должен:
|
||||
|
||||
1. Прочитать этот документ.
|
||||
2. Работать по одному экрану.
|
||||
3. Не переносить auth-flow пачкой.
|
||||
4. Проверять результат после каждого экрана.
|
||||
5. При переносе обратно в код не гадать, а опираться на Figma-правки.
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md` — логика протокола, роли API, серверное поведение, routing по `access_servers`
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md` — точный бинарный формат контейнера `SHiNE_DM`
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<SHiNE:...>` вставок внутри plaintext DM после расшифровки
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
|
||||
|
||||
Исторический устаревший документ сохранён отдельно:
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
- если формат понятен, но расшифровка не удалась, показывает `Не удалось расшифровать сообщение`;
|
||||
- если `body` повреждён или структурно битый, показывает `Сообщение повреждено`.
|
||||
|
||||
После успешной расшифровки plaintext может дополнительно содержать специальные клиентские вставки `<SHiNE:...>` в начале текста.
|
||||
После успешной расшифровки plaintext может дополнительно содержать специальные клиентские вставки `<S:...>` в начале текста. Legacy-вставки `<SHiNE:...>` также продолжают поддерживаться при чтении.
|
||||
Эти вставки относятся уже к уровню UI/plaintext, а не к уровню серверного DM-envelope.
|
||||
|
||||
### 2.4. Источник истины по пользователю
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
# Личные сообщения (DM) — v0.5 устаревшая спецификация
|
||||
|
||||
## Статус документа
|
||||
|
||||
Этот документ устарел и сохранён только как историческое описание ранее реализованной схемы DM.
|
||||
|
||||
Aктуальная целевая спецификация:
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
|
||||
Что в этом документе считать устаревшим:
|
||||
|
||||
- трактовку `encryptedBody` как фактически одинакового содержимого пары;
|
||||
- отсутствие нормального E2EE-шифрования DM;
|
||||
- старую модель удаления через пустую ревизию без терминального tombstone;
|
||||
- старую трактовку обновления только как общей пары без отдельной будущей модели перешифровки;
|
||||
- все упоминания legacy-формата read-receipt как части целевой архитектуры следующего этапа.
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
Сейчас в проекте реализованы:
|
||||
|
||||
- новый формат контентных личных сообщений `SHiNE_DM`;
|
||||
- ревизии сообщений через `revisionTimeMs`;
|
||||
- редактирование сообщения через повторную отправку той же логической пары;
|
||||
- удаление сообщения через пустую ревизию;
|
||||
- `upsert` последней версии сообщения на сервере.
|
||||
|
||||
Сейчас в проекте **не реализованы**:
|
||||
|
||||
- вложения в DM;
|
||||
- upload/download файлов для DM;
|
||||
- UI-кнопка прикрепления файла;
|
||||
- серверное хранение файловых связей для DM.
|
||||
|
||||
Черновик будущих вложений вынесен отдельно:
|
||||
|
||||
- `docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
|
||||
|
||||
## Общая схема
|
||||
|
||||
Личное сообщение по-прежнему отправляется парой signed-блоков:
|
||||
|
||||
- `type=1` — входящий блок для получателя;
|
||||
- `type=2` — исходящая копия для отправителя.
|
||||
|
||||
Read-receipt пока остаются в legacy-формате:
|
||||
|
||||
- `type=3` — входящее подтверждение прочтения;
|
||||
- `type=4` — исходящая копия подтверждения.
|
||||
|
||||
Ключи сообщения:
|
||||
|
||||
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||
- `messageKey = baseKey|messageType`
|
||||
|
||||
Логический идентификатор письма задаётся парой:
|
||||
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
|
||||
Эти поля не меняются при редактировании или удалении. Меняется только:
|
||||
|
||||
- `revisionTimeMs`
|
||||
- содержимое `encryptedBody`
|
||||
|
||||
Сервер хранит только последнюю версию записи для каждого `messageKey`.
|
||||
|
||||
## Формат контентного DM: `SHiNE_DM`
|
||||
|
||||
Префикс бинарного блока:
|
||||
|
||||
- `SHiNE_DM`
|
||||
|
||||
Поля идут в big-endian порядке:
|
||||
|
||||
1. `formatVersionMajor` (`u8`) = `1`
|
||||
2. `formatVersionMinor` (`u8`) = `0`
|
||||
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
|
||||
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
|
||||
5. `timeMs` (`u64`)
|
||||
6. `nonce` (`u32`)
|
||||
7. `messageType` (`u16`) — только `1` или `2`
|
||||
8. `revisionTimeMs` (`u64`)
|
||||
9. `attachmentsCount` (`u8`)
|
||||
10. `encryptedBodyLen` (`u32`)
|
||||
11. `encryptedBody` (`bytes`)
|
||||
12. `signature` (`64 bytes`, Ed25519)
|
||||
|
||||
### Ограничения
|
||||
|
||||
- `attachmentsCount` сейчас всегда должен быть `0`
|
||||
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
|
||||
- `revisionTimeMs` не может быть отрицательным
|
||||
|
||||
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
|
||||
|
||||
- `ATTACHMENTS_DISABLED`
|
||||
|
||||
## Legacy read-receipt: `SHiNE_dm2`
|
||||
|
||||
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
|
||||
|
||||
1. `toLoginLen` (`u8`) + `toLogin`
|
||||
2. `fromLoginLen` (`u8`) + `fromLogin`
|
||||
3. `timeMs` (`u64`)
|
||||
4. `nonce` (`u32`)
|
||||
5. `messageType` (`u16`) — `3` или `4`
|
||||
6. `payloadLen` (`u16`)
|
||||
7. `payloadBytes`
|
||||
8. `signature`
|
||||
|
||||
## Редактирование
|
||||
|
||||
Редактирование делается новой отправкой той же логической пары сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются теми же;
|
||||
- `messageType` остаётся `1/2`;
|
||||
- `revisionTimeMs` становится больше;
|
||||
- `encryptedBody` содержит новую версию текста.
|
||||
|
||||
Если на сервер приходит более старая ревизия, она игнорируется.
|
||||
|
||||
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
|
||||
|
||||
## Удаление
|
||||
|
||||
Удаление личного сообщения делается как новая ревизия того же сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются прежними;
|
||||
- `revisionTimeMs` увеличивается;
|
||||
- `attachmentsCount = 0`;
|
||||
- `encryptedBodyLen = 0`;
|
||||
- `encryptedBody` пустой.
|
||||
|
||||
В UI такое сообщение не показывается.
|
||||
|
||||
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
|
||||
|
||||
## Поведение сервера
|
||||
|
||||
Для контентных DM сервер:
|
||||
|
||||
1. принимает пару signed-блоков `type=1/2`;
|
||||
2. валидирует формат, подпись и совпадение ключевых полей пары;
|
||||
3. проверяет, что для обеих сторон пары совпадают:
|
||||
- `fromLogin`
|
||||
- `toLogin`
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
- `revisionTimeMs`
|
||||
- `encryptedBody`
|
||||
4. делает `upsert` последней версии в `signed_messages_v2`;
|
||||
5. сбрасывает pending-доставку по сессиям для новой ревизии;
|
||||
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
|
||||
|
||||
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
|
||||
|
||||
## Хранение в БД
|
||||
|
||||
Основная таблица:
|
||||
|
||||
- `signed_messages_v2`
|
||||
|
||||
Для контентных DM в ней используются:
|
||||
|
||||
- `message_key`
|
||||
- `base_key`
|
||||
- `target_login`
|
||||
- `from_login`
|
||||
- `to_login`
|
||||
- `time_ms`
|
||||
- `nonce`
|
||||
- `message_type`
|
||||
- `revision_time_ms`
|
||||
- `raw_block`
|
||||
- `created_at_ms`
|
||||
|
||||
Отдельных таблиц файлов для DM сейчас нет.
|
||||
|
||||
## События и доставка
|
||||
|
||||
Запрос на отправку по WebSocket остаётся прежним:
|
||||
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage` как алиас
|
||||
|
||||
Клиент отправляет:
|
||||
|
||||
- `incomingBlobB64`
|
||||
- `outgoingBlobB64`
|
||||
|
||||
Событие в активные сессии:
|
||||
|
||||
- `SignedMessageArrived`
|
||||
|
||||
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
|
||||
|
||||
Подтверждение доставки в сессию:
|
||||
|
||||
- `AckSessionDelivery`
|
||||
|
||||
WebPush и локальные уведомления сейчас работают так:
|
||||
|
||||
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
|
||||
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
|
||||
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
|
||||
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
|
||||
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
|
||||
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
|
||||
|
||||
## Правила UI
|
||||
|
||||
UI сейчас работает так:
|
||||
|
||||
- показывает только текст `encryptedBody`;
|
||||
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
||||
- не показывает удалённые сообщения;
|
||||
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
|
||||
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
|
||||
- не показывает и не принимает вложения.
|
||||
|
||||
## Что обязательно помнить
|
||||
|
||||
- вложения в DM сейчас отключены на уровне протокола и UI;
|
||||
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
||||
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
||||
@@ -18,7 +18,7 @@
|
||||
Если в начале plaintext стоит один или несколько специальных блоков формата:
|
||||
|
||||
```text
|
||||
<SHiNE:...>
|
||||
<S:...>
|
||||
```
|
||||
|
||||
то клиент трактует их как технические вставки.
|
||||
@@ -26,14 +26,14 @@
|
||||
Техническими считаются только блоки, которые:
|
||||
|
||||
- стоят строго в начале plaintext;
|
||||
- начинаются с точного префикса `<SHiNE:`;
|
||||
- начинаются с префикса `<S:` или legacy-префикса `<SHiNE:`;
|
||||
- заканчиваются первым символом `>`.
|
||||
|
||||
Если текст не начинается с `<SHiNE:`, никакие технические вставки не ищутся.
|
||||
Если текст не начинается с `<S:` или `<SHiNE:`, никакие технические вставки не ищутся.
|
||||
|
||||
## 2. Правило скрытия
|
||||
|
||||
Все корректно распознанные блоки `<SHiNE:...>` в начале plaintext:
|
||||
Все корректно распознанные блоки `<S:...>` или `<SHiNE:...>` в начале plaintext:
|
||||
|
||||
- не показываются пользователю как сырой текст;
|
||||
- используются клиентом для UI-логики;
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
Перед отправкой обычного текстового сообщения клиент обязан проверить:
|
||||
|
||||
- если пользовательский текст начинается с `<SHiNE:`
|
||||
- если пользовательский текст начинается с `<S:` или `<SHiNE:`
|
||||
|
||||
то клиент автоматически превращает начало в:
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
Общий вид:
|
||||
|
||||
```text
|
||||
<SHiNE:kind;key=value;key=value;...>
|
||||
<S:kind;key=value;key=value;...>
|
||||
```
|
||||
|
||||
Правила:
|
||||
@@ -69,14 +69,15 @@
|
||||
- параметры отделяются `;`;
|
||||
- ключ и значение отделяются `=`;
|
||||
- значения не экранируются в v1;
|
||||
- формат чувствителен к точному префиксу `<SHiNE:`.
|
||||
- канонический новый префикс: `<S:`;
|
||||
- legacy-префикс `<SHiNE:` продолжает поддерживаться при чтении.
|
||||
|
||||
## 5. Тип `reply`
|
||||
|
||||
Формат:
|
||||
|
||||
```text
|
||||
<SHiNE:reply;v=1;id=user1|user2|1720612345678|77>Текст ответа
|
||||
<S:reply;v=1;id=user1|user2|1720612345678|77>Текст ответа
|
||||
```
|
||||
|
||||
Где поле `id` — это логический идентификатор сообщения:
|
||||
@@ -91,14 +92,14 @@ fromLogin|toLogin|timeMs|nonce
|
||||
- после него может идти обычный текст ответа;
|
||||
- официальный UI формирует такой блок при отправке ответа через пункт `Ответить` в меню сообщения;
|
||||
- если клиент не находит сообщение, на которое ссылается `reply`, он просто не показывает reply-preview;
|
||||
- в таком случае само сообщение отображается как обычный текст без блока `<SHiNE:reply...>`.
|
||||
- в таком случае само сообщение отображается как обычный текст без блока `<S:reply...>`.
|
||||
|
||||
## 6. Тип `call`
|
||||
|
||||
### Успешный звонок
|
||||
|
||||
```text
|
||||
<SHiNE:call;v=1;status=completed;duration=367>
|
||||
<S:call;v=1;status=completed;duration=367>
|
||||
```
|
||||
|
||||
Где:
|
||||
@@ -108,7 +109,7 @@ fromLogin|toLogin|timeMs|nonce
|
||||
### Неуспешный звонок
|
||||
|
||||
```text
|
||||
<SHiNE:call;v=1;status=failed;reason=offline>
|
||||
<S:call;v=1;status=failed;reason=offline>
|
||||
```
|
||||
|
||||
Допустимые причины в v1:
|
||||
@@ -130,7 +131,7 @@ fromLogin|toLogin|timeMs|nonce
|
||||
|
||||
Официальный UI SHiNE в v1:
|
||||
|
||||
- скрывает все корректные `<SHiNE:...>` блоки в начале plaintext;
|
||||
- скрывает все корректные `<S:...>` и legacy `<SHiNE:...>` блоки в начале plaintext;
|
||||
- для `call` строит специальный человекочитаемый текст:
|
||||
- `Звонок: M:SS`
|
||||
- `Звонок: H:MM:SS`
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
Данная документация местами устарела и не соответствует реальному коду
|
||||
Данная документация местами устарела и не соответствует реальному коду
|
||||
Особенно в отношениии работы с Блокчейном
|
||||
@@ -1,12 +0,0 @@
|
||||
shine-server-bd — это библиотека реалезующая всю работу с БД:
|
||||
|
||||
хранит пользователей/сессии/параметры/кэш IP→гео и данные блокчейна (состояние + блоки), предоставляя единый PostgreSQL runtime-контроллер соединений, набор DAO под каждую таблицу (Singleton, методы с Connection для транзакций и без Connection — сами открывают/закрывают), и простые entity-модели как контейнеры данных для маппинга ResultSet↔Java.
|
||||
|
||||
Логика структуры классов (в двух словах):
|
||||
|
||||
shine.db.DbController / shine.db.PostgresDbController — вход в runtime БД: читает `db.url/db.user/db.password`, подключается только к PostgreSQL и выдаёт новые `Connection`.
|
||||
shine.db.DatabaseInitializer — проверяет наличие `db_schema_version` и при пустой БД автоматически накатывает `postgres/schema_v1.sql`.
|
||||
|
||||
|
||||
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()).
|
||||
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, CurrentUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO, SignedMessagesDAO; плюс сервисные DAO под recovery/resync.
|
||||
@@ -1,91 +0,0 @@
|
||||
# PostgreSQL runtime schema v1
|
||||
|
||||
Дата фиксации: `2026-07-24`
|
||||
|
||||
## Назначение
|
||||
|
||||
Это целевая серверная runtime-схема PostgreSQL для SHiNE без опоры на SQLite.
|
||||
|
||||
Схема `v1` нужна как стартовая точка большого механического переноса DAO и runtime-запросов
|
||||
с существующей SQLite-логики на PostgreSQL.
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
- Источник истины по пользователям: `solana_user_pda_current`.
|
||||
- Legacy-таблицы старого runtime для пользователей и личных сообщений в новой схеме не создаются.
|
||||
- Основная таблица серверных личных сообщений: `signed_messages`.
|
||||
- Таблица `blockchain_state` сохраняется как runtime-state таблица сервера:
|
||||
она не является identity-слоем и не мигрируется как legacy SQLite data.
|
||||
- Триггеры по `blocks` сохраняются и переписываются под PostgreSQL.
|
||||
|
||||
## Таблицы sync-модуля Solana users
|
||||
|
||||
- `solana_sync_state`
|
||||
- `solana_sync_tx_history`
|
||||
- `solana_user_pda_current`
|
||||
- `solana_user_pda_history`
|
||||
|
||||
## Таблицы server runtime
|
||||
|
||||
- `db_schema_version`
|
||||
- `active_sessions`
|
||||
- `esp_pairing_settings`
|
||||
- `esp_pairing_requests`
|
||||
- `users_params`
|
||||
- `ip_geo_cache`
|
||||
- `test_free_avatar_uploads`
|
||||
- `sync_servers`
|
||||
- `blockchain_state`
|
||||
- `blocks`
|
||||
- `connections_state`
|
||||
- `message_stats`
|
||||
- `reactions_state`
|
||||
- `channel_names_state`
|
||||
- `chat200_state`
|
||||
- `chat200_members_state`
|
||||
- `user_push_tokens`
|
||||
- `signed_direct_message_replay`
|
||||
- `signed_direct_messages_history`
|
||||
- `signed_messages`
|
||||
- `signed_message_session_delivery`
|
||||
|
||||
## Триггеры
|
||||
|
||||
Схема `v1` уже включает PostgreSQL-версии триггеров:
|
||||
|
||||
- `trg_blocks_line_integrity_bi`
|
||||
- `trg_blocks_connection_state_ai`
|
||||
- `trg_blocks_message_stats_like_ai`
|
||||
- `trg_blocks_message_stats_reply_ai`
|
||||
- `trg_blocks_edit_apply_ai`
|
||||
|
||||
## Что не входит в v1
|
||||
|
||||
- полная зачистка legacy-документации, старых названий и TODO-хвостов;
|
||||
- переименование Java DAO/классов `*V2` в runtime-коде;
|
||||
- перенос прямых SQL-запросов из хэндлеров в DAO/service;
|
||||
- переключение всего runtime-кода на новый `DbProvider`.
|
||||
|
||||
Это отдельные механические шаги поверх уже утверждённой схемы.
|
||||
|
||||
## Совместимость со старыми блоками каналов
|
||||
|
||||
В runtime-сервере сознательно нет жёсткой серверной проверки
|
||||
`channelName must not contain only digits`.
|
||||
|
||||
Причина: в уже существующей истории блокчейна есть каналы с числовыми именами,
|
||||
и при холодном восстановлении сервера с пустой БД и без `.bch` такие блоки должны
|
||||
успешно переигрываться от других sync-серверов.
|
||||
|
||||
Сейчас правило "новый публичный канал не должен состоять только из цифр" остаётся
|
||||
на уровне UI/продуктовых требований и должно быть позже возвращено на сервере
|
||||
отдельным совместимым способом, который не ломает replay исторических блоков.
|
||||
|
||||
## Инициализация пустой БД
|
||||
|
||||
Если сервер подключается к PostgreSQL через `db.url=jdbc:postgresql:...` и в выбранной БД ещё нет таблицы `db_schema_version`,
|
||||
он сам автоматически накатывает `schema_v1.sql` из classpath-ресурса:
|
||||
|
||||
- ресурс: `shine-server-db/src/main/resources/postgres/schema_v1.sql`
|
||||
- признак пустой схемы: отсутствует `db_schema_version`
|
||||
- стартовая версия схемы: `1`
|
||||
@@ -257,8 +257,8 @@ export function render({ navigate }) {
|
||||
topupButton.textContent = 'Пополнить кошелёк';
|
||||
topupButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const walletAddress = await deriveUserWalletAddress();
|
||||
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
|
||||
await deriveUserWalletAddress();
|
||||
navigate('topup-view');
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = `Не удалось подготовить кошелёк: ${error?.message || 'unknown'}`;
|
||||
@@ -301,10 +301,8 @@ export function render({ navigate }) {
|
||||
: `Для регистрации пополните этот кошелёк соланами примерно на ${RECOMMENDED_TOPUP_SOL} SOL. Сейчас на кошельке ${formatSol(currentBalance, 6)} SOL. Минимум для продолжения: ${MIN_REQUIRED_SOL} SOL.`;
|
||||
status.style.display = '';
|
||||
if (isTestContour) {
|
||||
const openTopup = window.confirm('Открыть страницу пополнения с вашим кошельком?');
|
||||
if (openTopup) {
|
||||
window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
const openTopup = window.confirm('Перейти на экран пополнения и затем продолжить регистрацию?');
|
||||
if (openTopup) navigate('topup-view');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { state } from '../state.js';
|
||||
import {
|
||||
createSolanaWalletFromPrivateBase58,
|
||||
formatSol,
|
||||
getBalanceSol,
|
||||
getTopupSiteUrl,
|
||||
requestAirdropSol,
|
||||
transferSol,
|
||||
} from '../services/solana-wallet-service.js';
|
||||
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
|
||||
|
||||
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };
|
||||
export const pageMeta = { id: 'topup-view', title: 'Пополнение solana счета', showAppChrome: false };
|
||||
const DEVNET_ENDPOINT = 'https://api.devnet.solana.com';
|
||||
const SENDER_PRIVATE_32_BASE58 = '6xqAuKYvA8qrCdAkcw7Y8aMgvBnYk8JLxWLma5BzbAvu';
|
||||
const REGISTRATION_TOPUP_AMOUNT_SOL = 0.02;
|
||||
|
||||
// Канонический Solana-адрес пополнения = публичный device-ключ из сгенерированного набора ключей.
|
||||
// Тот же путь, что в registration-payment-view (deriveUserWalletAddress); не выводим адрес
|
||||
@@ -39,6 +43,10 @@ export function render({ navigate }) {
|
||||
const status = document.createElement('p');
|
||||
status.className = 'meta-muted';
|
||||
status.textContent = 'Проверяем кошелек...';
|
||||
status.style.whiteSpace = 'pre-wrap';
|
||||
status.style.overflowWrap = 'anywhere';
|
||||
status.style.wordBreak = 'break-word';
|
||||
status.style.fontSize = '13px';
|
||||
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'ghost-btn';
|
||||
@@ -63,42 +71,54 @@ export function render({ navigate }) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<p class="auth-copy">Кнопка «Пополнить» в кошельке будет переводить на отдельный сайт. Пока доступно тестовое пополнение.</p>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="meta-muted">1. Вы можете открыть промо-сайт пополнения с подставленным кошельком.</p>
|
||||
<p class="meta-muted">2. Либо нажать «Тестовое пополнение» и получить 1 SOL через DevNet airdrop.</p>
|
||||
</div>
|
||||
<a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" target="_blank" rel="noreferrer">Открыть сайт пополнения</a>
|
||||
<p class="auth-copy">Можете или пополнить счёт тестовыми соланами и продолжить регистрацию.</p>
|
||||
<div class="card stack" style="padding:12px; max-width:320px;">
|
||||
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (client key = Solana wallet)</div>
|
||||
</div>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="meta-muted">Или можете отдельно открыть страницу тестового пополнения.</p>
|
||||
<a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" target="_blank" rel="noreferrer">Открыть страницу тестового пополнения</a>
|
||||
</div>
|
||||
`;
|
||||
card.children[3].append(walletRow);
|
||||
card.children[1].append(walletRow);
|
||||
|
||||
const testButton = document.createElement('button');
|
||||
testButton.className = 'ghost-btn';
|
||||
testButton.type = 'button';
|
||||
testButton.textContent = 'Тестовое пополнение (1 SOL)';
|
||||
testButton.textContent = 'Тестовое пополнение для регистрации';
|
||||
testButton.addEventListener('click', async () => {
|
||||
const address = String(walletValue.value || '').trim();
|
||||
if (!address) {
|
||||
window.alert('Адрес кошелька не найден.');
|
||||
return;
|
||||
}
|
||||
if (!senderKeypair) {
|
||||
status.textContent = 'Ошибка: тестовый кошелёк отправителя ещё не инициализирован.';
|
||||
return;
|
||||
}
|
||||
testButton.disabled = true;
|
||||
try {
|
||||
const drop = await requestAirdropSol({
|
||||
endpoint: state.entrySettings.solanaServer,
|
||||
address,
|
||||
amountSol: 1,
|
||||
status.textContent = 'Отправляем тестовое пополнение для регистрации...';
|
||||
const tx = await transferSol({
|
||||
endpoint: DEVNET_ENDPOINT,
|
||||
fromKeypair: senderKeypair,
|
||||
toAddress: address,
|
||||
amountSol: REGISTRATION_TOPUP_AMOUNT_SOL,
|
||||
});
|
||||
const bal = await getBalanceSol({
|
||||
endpoint: state.entrySettings.solanaServer,
|
||||
endpoint: DEVNET_ENDPOINT,
|
||||
address,
|
||||
});
|
||||
state.registrationPayment.balanceSOL = String(bal.sol);
|
||||
status.textContent = `Тестовое пополнение выполнено. Новый баланс: ${formatSol(bal.sol, 6)} SOL. Signature: ${drop.signature}`;
|
||||
status.style.fontSize = '12px';
|
||||
status.textContent = [
|
||||
'Тестовое пополнение для регистрации выполнено.',
|
||||
`Кошелёк пополнен на ${REGISTRATION_TOPUP_AMOUNT_SOL} SOL.`,
|
||||
`Новый баланс: ${formatSol(bal.sol, 6)} SOL.`,
|
||||
`Signature: ${tx.signature}`,
|
||||
].join('\n');
|
||||
} catch (error) {
|
||||
status.style.fontSize = '13px';
|
||||
status.textContent = `Ошибка тестового пополнения: ${error?.message || 'unknown'}`;
|
||||
} finally {
|
||||
testButton.disabled = false;
|
||||
@@ -108,12 +128,13 @@ export function render({ navigate }) {
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'primary-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.textContent = 'Продолжить регистрацию';
|
||||
backButton.addEventListener('click', () => navigate('registration-payment-view'));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
actions.append(testButton, backButton);
|
||||
let senderKeypair = null;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -132,6 +153,8 @@ export function render({ navigate }) {
|
||||
});
|
||||
state.registrationPayment.balanceSOL = String(balance.sol);
|
||||
status.textContent = `Текущий баланс: ${formatSol(balance.sol, 6)} SOL`;
|
||||
const sender = await createSolanaWalletFromPrivateBase58(SENDER_PRIVATE_32_BASE58);
|
||||
senderKeypair = sender.keypair;
|
||||
} catch (error) {
|
||||
status.textContent = `Не удалось получить баланс: ${error?.message || 'unknown'}`;
|
||||
}
|
||||
@@ -139,7 +162,7 @@ export function render({ navigate }) {
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Пополнение счета',
|
||||
title: 'Пополнение solana счета',
|
||||
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
||||
}),
|
||||
card,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from './arweave-file-service.js';
|
||||
|
||||
const ATTACH_PREFIX = '<SHiNE:attach;';
|
||||
const ATTACH_BLOCK_RE = /^<SHiNE:attach;([^>]*)>\n?/u;
|
||||
const ATTACH_BLOCK_RE = /^<(?:SHiNE|S):(attach|att);([^>]*)>\n?/u;
|
||||
export const MAX_MESSAGE_ATTACHMENTS = 10;
|
||||
const RECENT_UNAVAILABLE_MS = 20 * 60 * 1000;
|
||||
const IMAGE_EXTENSIONS = new Set(['apng', 'avif', 'bmp', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp']);
|
||||
@@ -33,8 +32,8 @@ function normalizeName(name) {
|
||||
}
|
||||
|
||||
function normalizePreview(input = {}) {
|
||||
const previewTxId = String(input.previewAr || input.ar || input.txId || '').trim();
|
||||
const previewSha256Hex = String(input.previewSha256 || input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
const previewTxId = String(input.preAr || input.previewAr || input.ar || input.txId || '').trim();
|
||||
const previewSha256Hex = String(input.preSha256 || input.previewSha256 || input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
if (!previewTxId || !previewSha256Hex) return null;
|
||||
if (!validateArweaveTxId(previewTxId)) return null;
|
||||
if (!validateSha256Hex(previewSha256Hex)) return null;
|
||||
@@ -47,9 +46,11 @@ function normalizePreview(input = {}) {
|
||||
export function normalizeAttachment(input = {}) {
|
||||
const txId = String(input.ar || input.txId || '').trim();
|
||||
const sha256Hex = String(input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
const size = Number(input.size || input.sizeBytes || 0);
|
||||
const name = normalizeName(input.name || input.fileName || 'file');
|
||||
const size = Number(input.sz || input.size || input.sizeBytes || 0);
|
||||
const name = normalizeName(input.nm || input.name || input.fileName || 'file');
|
||||
const preview = normalizePreview(input.preview || {
|
||||
preAr: input.preAr,
|
||||
preSha256: input.preSha256,
|
||||
previewAr: input.previewAr,
|
||||
previewSha256: input.previewSha256,
|
||||
});
|
||||
@@ -75,9 +76,9 @@ export function buildAttachmentBlock(attachment) {
|
||||
const item = normalizeAttachment(attachment);
|
||||
const encodedName = encodeURIComponent(item.name);
|
||||
const previewFields = item.preview
|
||||
? `;previewAr=${item.preview.ar};previewSha256=${item.preview.sha256}`
|
||||
? `;preAr=${item.preview.ar};preSha256=${item.preview.sha256}`
|
||||
: '';
|
||||
return `<SHiNE:attach;v=${item.preview ? 2 : 1};name=${encodedName};size=${item.size};sha256=${item.sha256};ar=${item.ar}${previewFields}>`;
|
||||
return `<S:att;v=1;nm=${encodedName};sz=${item.size};sha256=${item.sha256};ar=${item.ar}${previewFields}>`;
|
||||
}
|
||||
|
||||
export function composeMessageWithAttachments(text, attachments = []) {
|
||||
@@ -105,16 +106,22 @@ export function parseMessageAttachments(rawText) {
|
||||
let rest = String(rawText || '');
|
||||
const attachments = [];
|
||||
|
||||
while (rest.startsWith(ATTACH_PREFIX)) {
|
||||
while (true) {
|
||||
const match = rest.match(ATTACH_BLOCK_RE);
|
||||
if (!match) break;
|
||||
const fields = parseFields(match[1]);
|
||||
const fields = parseFields(match[2]);
|
||||
try {
|
||||
const version = Number(fields.v || 0);
|
||||
if (version !== 1 && version !== 2) {
|
||||
throw new Error('unsupported attachment version');
|
||||
}
|
||||
attachments.push(normalizeAttachment({
|
||||
name: decodeURIComponent(String(fields.name || 'file')),
|
||||
size: fields.size,
|
||||
nm: decodeURIComponent(String(fields.nm || fields.name || 'file')),
|
||||
sz: fields.sz || fields.size,
|
||||
sha256: fields.sha256,
|
||||
ar: fields.ar,
|
||||
preAr: fields.preAr,
|
||||
preSha256: fields.preSha256,
|
||||
previewAr: fields.previewAr,
|
||||
previewSha256: fields.previewSha256,
|
||||
}));
|
||||
|
||||
@@ -795,7 +795,7 @@ function composeChannelMetaText({ title = '', description = '', avatar = null }
|
||||
const cleanTitle = validateChannelMetaTitle(title);
|
||||
const cleanDescription = normalizeChannelMetaDescription(description);
|
||||
const rows = [];
|
||||
if (cleanTitle) rows.push(`<SHiNE:title;v=1;${cleanTitle}>`);
|
||||
if (cleanTitle) rows.push(`<S:title;v=1;${cleanTitle}>`);
|
||||
if (avatar?.ar) {
|
||||
const size = Number(avatar.size || 0);
|
||||
const sha256 = String(avatar.sha256 || '').trim().toLowerCase();
|
||||
@@ -803,7 +803,7 @@ function composeChannelMetaText({ title = '', description = '', avatar = null }
|
||||
if (!Number.isInteger(size) || size <= 0) throw new Error('Некорректный размер аватара.');
|
||||
if (!/^[0-9a-f]{64}$/u.test(sha256)) throw new Error('Некорректный SHA-256 аватара.');
|
||||
if (!/^[A-Za-z0-9_-]{43}$/u.test(ar)) throw new Error('Некорректный Arweave txId аватара.');
|
||||
rows.push(`<SHiNE:avatar;v=1;size=${size};sha256=${sha256};ar=${ar}>`);
|
||||
rows.push(`<S:ava;v=1;sz=${size};sha256=${sha256};ar=${ar}>`);
|
||||
}
|
||||
if (cleanDescription) rows.push(cleanDescription);
|
||||
return rows.join('\n');
|
||||
|
||||
@@ -11,6 +11,16 @@ function defaultParsed(rawText = '') {
|
||||
};
|
||||
}
|
||||
|
||||
function hasTechPrefix(text = '', offset = 0) {
|
||||
return String(text || '').startsWith('<SHiNE:', offset) || String(text || '').startsWith('<S:', offset);
|
||||
}
|
||||
|
||||
function getTechPrefixLength(text = '', offset = 0) {
|
||||
if (String(text || '').startsWith('<SHiNE:', offset)) return 7;
|
||||
if (String(text || '').startsWith('<S:', offset)) return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseReplyId(value = '') {
|
||||
const parts = String(value || '').split('|');
|
||||
if (parts.length !== 4) return null;
|
||||
@@ -57,41 +67,45 @@ function buildCallDisplayText(callSummary) {
|
||||
|
||||
export function sanitizeUserDmTextForSend(rawText = '') {
|
||||
const text = String(rawText || '');
|
||||
return text.startsWith('<SHiNE:') ? `< SHiNE:${text.slice(7)}` : text;
|
||||
if (text.startsWith('<SHiNE:')) return `< SHiNE:${text.slice(7)}`;
|
||||
if (text.startsWith('<S:')) return `< S:${text.slice(3)}`;
|
||||
return text;
|
||||
}
|
||||
|
||||
export function buildDmCallTechBlock({ status = '', durationSec = 0, reason = '' } = {}) {
|
||||
const cleanStatus = String(status || '').trim().toLowerCase();
|
||||
if (cleanStatus === 'completed') {
|
||||
return `<SHiNE:call;v=1;status=completed;duration=${Math.max(0, Math.floor(Number(durationSec || 0)))}>`;
|
||||
return `<S:call;v=1;status=completed;duration=${Math.max(0, Math.floor(Number(durationSec || 0)))}>`;
|
||||
}
|
||||
const cleanReason = String(reason || '').trim().toLowerCase();
|
||||
return `<SHiNE:call;v=1;status=failed;reason=${cleanReason || 'connect_failed'}>`;
|
||||
return `<S:call;v=1;status=failed;reason=${cleanReason || 'connect_failed'}>`;
|
||||
}
|
||||
|
||||
export function buildDmReplyTechBlock({ baseKey = '' } = {}) {
|
||||
const cleanBaseKey = String(baseKey || '').trim();
|
||||
if (!cleanBaseKey) return '';
|
||||
return `<SHiNE:reply;v=1;id=${cleanBaseKey}>`;
|
||||
return `<S:reply;v=1;id=${cleanBaseKey}>`;
|
||||
}
|
||||
|
||||
export function parseDmTechBlocks(rawText = '') {
|
||||
const text = String(rawText || '');
|
||||
if (!text.startsWith('<SHiNE:')) return defaultParsed(text);
|
||||
if (!hasTechPrefix(text)) return defaultParsed(text);
|
||||
|
||||
const blocks = [];
|
||||
let cursor = 0;
|
||||
let replyRef = null;
|
||||
let callSummary = null;
|
||||
|
||||
while (text.startsWith('<SHiNE:', cursor)) {
|
||||
while (hasTechPrefix(text, cursor)) {
|
||||
const end = text.indexOf('>', cursor);
|
||||
if (end < 0) {
|
||||
if (!blocks.length) return defaultParsed(text);
|
||||
break;
|
||||
}
|
||||
|
||||
const inner = text.slice(cursor + 7, end);
|
||||
const prefixLength = getTechPrefixLength(text, cursor);
|
||||
if (!prefixLength) break;
|
||||
const inner = text.slice(cursor + prefixLength, end);
|
||||
const segments = inner.split(';').map((part) => String(part || '').trim()).filter(Boolean);
|
||||
if (!segments.length) {
|
||||
if (!blocks.length) return defaultParsed(text);
|
||||
|
||||
Reference in New Issue
Block a user