Files
utun2/src/chat/chat_core.h
T

383 lines
28 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
* Порядок: utun_core_start → chat_service_start → команды чата → chat_service_stop.
* Сообщения отправляются через submit_message или submit_media_message; результат GUI — CHAT_EVT_MSG_SUBMITTED.
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
* GUI вызывает через gui_bridge_post_uasync() trampoline-функции,
* чей arg-структура несёт inst.
*/
#ifndef CHAT_CORE_H
#define CHAT_CORE_H
#include <stdint.h>
#include <stddef.h>
#include <sqlite3.h>
struct sqlite3;
struct UTUN_INSTANCE;
// ── Жизненный цикл ──
// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1.
int chat_service_start(struct UTUN_INSTANCE* inst);
/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата.
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
void chat_service_stop(struct UTUN_INSTANCE* inst);
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
* Для запуска всего чат-сервиса использовать chat_service_start(). */
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра.
void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Startup: восстановить производный attachment-индекс из подписанной истории и зарегистрировать сохранённые медиа.
* Вызывать после инициализации attachment и startup_backfill, чтобы SQL-отказ мог поставить повтор. Ключи из истории не восстанавливаются. */
void chat_media_backfill(struct UTUN_INSTANCE* inst);
/* Докачка недостающего медиа за последние storage_backfill_days дней и применение квоты.
* Также восстанавливает attachment-индекс/доступность сохранённых контейнеров; SQL-отказ ставит повтор канала. */
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Подписать завершения Merkle: первый обмен запускает общий scan после 2с тишины; отдельные каналы — batch до16 через2с.
* Повтор индекса не отменяет MSG_RECEIVED committed-истории; следующий batch может быть поставлен во время текущего.
* При переполнении очереди и для восстановления допускаются последующие общие scan. Destroy снимает подписки/таймеры. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
// Заимствованная БД текущего chat_core; NULL до init/после destroy.
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
// 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети.
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
// ── Отправка сообщения (GUI → uasync) ──
struct chat_msg_submit {
struct UTUN_INSTANCE* inst;
char channel_id[64]; /* десятичный ID канала */
char content_type[32]; /* тип содержимого сообщения */
char media_src[1024]; /* исходный файл вложения */
char media_dest[1024]; /* путь подготовленного локального файла */
uint8_t media_copy; /* 1 = скопировать исходный файл при регистрации */
uint8_t media_video; /* 1 = подготовить как видео (пробинг+транскод) */
int duration_ms; /* видео: длительность (0 = неизвестно) */
int width; /* видео: ширина */
int height; /* видео: высота */
uint8_t* data; /* содержимое сообщения, заимствовано обычным submit */
uint32_t data_len; /* размер data в байтах */
uint64_t timestamp; /* успешная отправка возвращает назначенное время */
uint8_t message_id[32]; /* успешная обычная отправка возвращает общий ID */
uint64_t expected_ver; /* >0: trampoline редактирует message_id вместо новой отправки. */
uint8_t reply_to[32]; /* общий ID оригинала в этом канале; нули = не ответ */
uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */
};
// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1.
int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Правка собственного text: ожидаемая версия защищает открытый редактор от устаревшего сохранения.
* 0 — сохранено/текст прежний, -1 — ошибка, -2 — версия изменилась. ID и порядок сообщения сохраняются. */
int chat_core_edit_message(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
uint64_t expected_ver, const char* text, size_t len);
/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0.
* req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки.
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Ручная загрузка по локальному номеру строки; внешняя SQL-транзакция запрещена.
* Manifest ограничен общим DB_SYNC_MESSAGE_MAX. Зашифрованный индекс восстанавливается из истории до wanted/загрузки;
* отсутствие plaintext сбрасывает ready с сохранением ключа. Отказ SQL логируется, состояние модуля атомарно сохраняется. */
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id);
// ── DB-операции (оставлены для интроспекции) ──
// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса.
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1.
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
// ── Создание канала (GUI → uasync) ──
struct chat_channel_create {
struct UTUN_INSTANCE* inst; /* Заимствован trampoline; прямой вызов использует свой аргумент inst. */
char channel_id[64]; /* NUL-строка: каноническое десятичное число 1..INT64_MAX. */
char name[128]; /* NUL-строка до127 байт, без молчаливого обрезания. */
uint64_t owner_node_id; /* node_id создающего instance. */
uint8_t x25519_pubkey[32]; /* Канальный ключ шифрования; должен соответствовать privkey. */
uint8_t x25519_privkey[32];/* Независимый приватный X25519-ключ канала. */
uint8_t ed25519_pubkey[32];/* Канальный ключ подписи; должен соответствовать raw seed. */
uint8_t ed25519_privkey[32];/* Raw Ed25519 seed, без преобразования sc_derive_ed25519_pubkey. */
uint8_t signature[64]; /* Подпись id\0 || name\0 || owner(native u64) || X25519 || Ed25519. */
};
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
/* Подготовить таблицы, группу и синхронизацию; 0/-1. Ошибка удаляет новые runtime-ресурсы,
* проверенные данные БД сохраняются. Членства на других узлах не даёт. */
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Создать новый канал:проверить пары ключей/подпись,одним commit сохранить описание,root JOIN/профиль/приглашение и Merkle.
* 0 — сохранено,-1 — отказ без публикации;req заимствован и не меняется. Владелец — этот instance.
* Внешняя транзакция и занятый ID запрещены. CHANNEL_UPDATED — после commit и попытки runtime-подготовки.
* Отказ подготовки не отменяет созданный канал:можно повторить ensure_channel_ready. Неудачный rollback
* с открытой транзакцией запрашивает stop/закрытие БД владельцем вне callback. */
int chat_core_create_channel(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req);
// Забирает u_malloc-структуру chat_channel_create, очищает ключи и освобождает её после вызова.
void chat_core_create_channel_trampoline(void* arg);
/* Сгенерировать независимые пары X25519/Ed25519 и создать канал. ID>0 — сохранён,0 — отказ.
* name — NUL-строка<=127байт,не обрезается. Проверяет RNG/генерацию/извлечение/подписи;
* временные приватные ключи очищаются. Внешняя транзакция отклоняется до генерации. */
uint64_t chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
// Забирает и освобождает u_malloc-структуру chat_create_auto_arg; instance должен дожить до выполнения.
void chat_core_create_channel_auto_trampoline(void* arg);
// ── Локальное удаление канала (GUI → uasync) ──
struct chat_delete_channel_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
};
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют.
* Исходящие video/register задачи сначала завершаются; удаление продолжит последний completion.
* Новые исходящие медиа этого канала в это время отклоняются. Внешняя SQL-транзакция запрещена до отмены ресурсов.
* Ключи/запросы/outbox удаляются триггером атомарно с метаданными; при SQL-ошибке данные и файлы сохраняются,
* сетевые ресурсы уже могут быть отменены — требуется повтор. Открытая после ошибки транзакция требует stop владельцем.
* CHANNEL_DELETED приходит после commit метаданных и попытки файловой очистки. Её I/O-ошибка логируется отдельно;
* остатки media_files сохраняются, собственный каталог plaintext можно очистить повторно после commit. */
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_delete_channel_trampoline(void* arg);
// ── Утилиты ──
// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет.
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
// Сохранить имя узла и обновить собственные member-записи каналов.
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
// Обновить адреса topo_node и собственные member-записи каналов.
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync.
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
// Сохранение key-value в ui_state (вызывается из uasync-потока)
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
void chat_core_set_sound_on_message_trampoline(void* arg);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
void chat_core_save_ui_state_trampoline(void* arg);
// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync)
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_set_setting_trampoline(void* arg);
// Запуск подключения к узлам выбранного канала (GUI → uasync)
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_connect_channel_trampoline(void* arg);
// Однократная попытка подключения к конкретному узлу канала (GUI → uasync)
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
void chat_core_connect_node_trampoline(void* arg);
struct chat_connect_node_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
};
/* Передать в uasync блок chat_msg_submit, выделенный u_calloc/u_malloc; trampoline освобождает его.
* data отдельно не освобождается: удобно разместить содержимое в хвосте того же блока.
* Непустой media_src выбирает подготовку вложения, иначе — обычную отправку. Результат — по request_id. */
void chat_core_submit_trampoline(void* arg);
// Трамплин для ручного запуска загрузки медиа (JNI → uasync)
struct attachment_dl_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
int64_t msg_id;
};
void chat_core_attachment_download_trampoline(void* arg);
// Обновление local_attrs для записи (вызывается из uasync-потока)
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json);
// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs)
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
uint64_t author_node_id);
void chat_core_mark_voice_played_trampoline(void* arg);
struct voice_played_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
uint64_t ts;
uint64_t author_node_id;
};
/* Снять текстовый статус NTP/connections:arg — заимствованный inst,ответ STATUS_REFRESH без конечного NUL.
* Динамический буфер живёт до возврата обработчика;отказ памяти/форматирования логируется без частичного события. */
void chat_core_collect_status_trampoline(void* arg);
// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD.
void chat_core_collect_cpu_load_trampoline(void* arg);
/* Список до250 ETCP-соединений:arg — заимствованный inst,ответ CONN_LIST [count:u16 native][entry:46]*.
* Entry:peer:u64,flags:u8,links:u8,rtt:u16,inflight_KiB:u16,name:32;числа native,links/inflight насыщаются. */
void chat_core_collect_conn_list_trampoline(void* arg);
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
/* Снять текстовые метрики соединения:arg — u_malloc chat_node_arg,забирается/освобождается,inst заимствован.
* Ответ CONN_METRICS без NUL,динамический буфер только до возврата обработчика;при OOM нет частичного события. */
void chat_core_collect_conn_metrics_trampoline(void* arg);
/* Диагностика карточки участника: запрос/codec находятся в chat_member.h. */
// ── Headless CLI API ──
/* JSON-списки:0 — полный JSON с NUL,-1 — ошибка;out_len без NUL,при отказе0,допустимый buf очищается.
* buf/out_len обязательны,buf_size>=3. SQL/память/переполнение не дают успешный префикс;
* Строки экранируются целиком;channels/members отклоняют скрытый NUL в канальных/профильных SQLite TEXT.
* Каналы:[{id,name,owner_id,owner_name,peers,msgs,online}];отложенная history-таблица означает msgs=0. */
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
* От новых к старым;count<=0 — до100 записей,offset>=0 — сколько пропустить.
* Канал — канонический ID1..INT64_MAX;общий контракт буфера как у channels_json. */
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/* Мемберы в JSON: [{node_id,name,online,connected,adm_tags,x25519,ed25519,addrs}].
* connected означает наличие транспорта в реестре,а не его UP/готовность группы;deleted показывается offline.
* addrs.proto=UDP/TCP/TCP+REALITY,IPv4/IPv6 текстовые;канал/буфер как у messages/channels_json. */
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/* Имя nodes с NUL,при необходимости обрезанное до sz-1 байт:0 — прочитано/узел отсутствует,-1 — ошибка.
* out/sz>0 обязательны;отсутствие узла и ошибка оставляют пустую строку (если буфер допустим). */
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
/* Ссылка avatars/<sha256>.json внутри дерева своего UID/канала;server=0 удаляет ссылку,file остаётся обязательной NUL-строкой.
* 0 — committed/семантически прежнее,-1 — отказ;чужая транзакция запрещена. Storage-роль сервера проверяется под блокировкой. */
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file);
/* Построить {name:string},включая Unicode/control escaping;0/-1,при ошибке out не меняется. */
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
/* Прочитать собственный подписанный профиль:0/-1,при отказе все выходы сохраняются.
* Проверяет SQL,размеры,JOIN/update-подписи и целый плоский JSON с уникальными строковыми полями.
* out требует capacity>=длина+1,signature64,timestamp — версия update_ts в секундах/монотонных шагах. */
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
uint8_t signature[64], uint64_t* timestamp);
/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ.
* Остальные подписанные поля сохраняются;неподписанные старые атрибуты не авторизуются новым обновлением.
* Имя/аватар меняются своими API. Чтение/проверка/версия/запись/Merkle защищены одним BEGIN IMMEDIATE.
* Семантический no-op сохраняет подпись/версию независимо от порядка ключей/escaping;ошибка не меняет БД/Merkle.
* Уведомления идут после commit,без SQLite cursor. Вызов в uasync вне чужой транзакции;
* неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. */
struct chat_profile_option { const char* key; const char* value; };
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
const struct chat_profile_option* options, size_t count);
struct chat_avatar_req {
struct UTUN_INSTANCE* inst;
uint64_t id, group, server;
char file[70];
};
void chat_core_set_my_avatar_trampoline(void* arg);
// ── Редактирование adm_tags (владелец канала) ──
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
#define CHAT_MEMBER_TAGS_KEY_SZ 64
struct chat_member_tags;
// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет.
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit.
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
// Получить значение ключа. Возвращает NULL если ключ не найден.
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
// Заменить все теги данными из JSON-строки (плоский, строковые значения).
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
* записать через member_sync_put() → синхронизируется всем. */
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
// Освободить без сохранения.
void chat_member_tags_free(struct chat_member_tags* t);
// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память).
struct chat_member_tags_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
char json[512];
};
void chat_member_tags_commit_trampoline(void* arg);
// ── Передача прав админа (владелец → другому узлу) ──
// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router).
// uasync-поток; NULL/неинициализированный inst отклоняется с ERROR. Постановка в транспорт не подтверждает импорт.
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
void chat_core_transfer_admin_trampoline(void* arg);
struct chat_transfer_admin_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t target_node_id;
};
#endif /* CHAT_CORE_H */