294 lines
14 KiB
C
294 lines
14 KiB
C
/*
|
|
* chat_profile.c — профиль узла: имя, адреса, ui_state
|
|
*
|
|
* Вынесено из chat_core.c для уменьшения размера модуля.
|
|
*/
|
|
|
|
#include "chat_core_priv.h"
|
|
#include "chat_event.h"
|
|
#include "chat_member.h"
|
|
#include "../../lib/json_flat.h"
|
|
#include "../routing_layer/topo_node_sqlite.h"
|
|
#include "../routing_layer/topo_node.h"
|
|
#include "member_sync.h"
|
|
#include "../member_files/member_files.h"
|
|
|
|
#include "../utun_instance.h"
|
|
#include "../transport_layer/etcp.h"
|
|
#include "../transport_layer/etcp_connections.h"
|
|
#include "../transport_layer/secure_channel.h"
|
|
#include "../ntp_time.h"
|
|
#include "../../lib/u_async.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/platform_compat.h"
|
|
|
|
#include <openssl/evp.h>
|
|
|
|
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (cc) cc->my_node_id = node_id;
|
|
}
|
|
|
|
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !name) return;
|
|
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
|
|
name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
|
(uint64_t)ntp_time_get_seconds(inst), 0);
|
|
snprintf(inst->name, sizeof(inst->name), "%s", name);
|
|
chat_core_update_my_member(inst);
|
|
}
|
|
|
|
void chat_core_update_my_name_trampoline(void* arg) {
|
|
struct update_my_name_arg* a = (struct update_my_name_arg*)arg;
|
|
if (!a) return;
|
|
chat_core_update_my_name(a->inst, a->name);
|
|
u_free(arg);
|
|
}
|
|
|
|
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
|
|
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
|
|
|
|
struct profile_json {
|
|
char text[MS_USERINFO_MAX + 1];
|
|
size_t used;
|
|
int avatar;
|
|
const struct chat_profile_option* options;
|
|
size_t option_count;
|
|
};
|
|
|
|
static int profile_string(struct profile_json* p, const char* text) {
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = '"';
|
|
for (const unsigned char* c = (const unsigned char*)text; *c; c++) {
|
|
char escape = 0;
|
|
if (*c == '"' || *c == '\\') escape = *c;
|
|
if (*c == '\n') escape = 'n';
|
|
if (*c == '\r') escape = 'r';
|
|
if (*c == '\t') escape = 't';
|
|
if (*c == '\b') escape = 'b';
|
|
if (*c == '\f') escape = 'f';
|
|
if ((*c < 32 && !escape) || p->used + (escape ? 2 : 1) >= MS_USERINFO_MAX) return -1;
|
|
if (escape) p->text[p->used++] = '\\';
|
|
p->text[p->used++] = escape ? escape : *c;
|
|
}
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = '"'; p->text[p->used] = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int profile_pair(struct profile_json* p, const char* key, const char* value) {
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
if (p->used > 1) p->text[p->used++] = ',';
|
|
if (profile_string(p, key) < 0 || p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = ':';
|
|
return profile_string(p, value);
|
|
}
|
|
|
|
static int profile_copy_pair(const char* key, const char* value, void* arg) {
|
|
struct profile_json* p = arg;
|
|
if (!strcmp(key, "name") || (p->avatar && (!strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")))) return 0;
|
|
for (size_t i = 0; i < p->option_count; i++) if (!strcmp(key, p->options[i].key)) return 0;
|
|
return profile_pair(p, key, value);
|
|
}
|
|
|
|
int chat_core_name_userinfo(const char* name, char* out, size_t capacity) {
|
|
struct profile_json p = { .text = "{", .used = 1 };
|
|
if (!name || !out || profile_pair(&p, "name", name) < 0 || p.used + 2 > capacity || p.used >= MS_USERINFO_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot encode display name capacity=%zu", capacity); return -1;
|
|
}
|
|
p.text[p.used++] = '}'; p.text[p.used] = 0; memcpy(out, p.text, p.used + 1); return 0;
|
|
}
|
|
|
|
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) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->db || !channel || !out || !signature || !timestamp) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid read request"); return -1;
|
|
}
|
|
char table[80], sql[256]; peers_table_name(channel, table, sizeof(table));
|
|
snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts FROM \"%s\" WHERE node_id=?", table);
|
|
sqlite3_stmt* st = NULL; int result = -1;
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
const char* json = (const char*)sqlite3_column_text(st, 0); size_t size = json ? strlen(json) : 0;
|
|
if (json && size <= MS_USERINFO_MAX && size < capacity && sqlite3_column_bytes(st, 1) == 64 && sqlite3_column_int64(st, 2) > 0) {
|
|
memcpy(out, json, size + 1); memcpy(signature, sqlite3_column_blob(st, 1), 64);
|
|
*timestamp = (uint64_t)sqlite3_column_int64(st, 2); result = 0;
|
|
}
|
|
}
|
|
sqlite3_finalize(st);
|
|
}
|
|
if (result) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: signed local profile unavailable channel=%s capacity=%zu", channel, capacity);
|
|
return result;
|
|
}
|
|
|
|
/* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */
|
|
static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file,
|
|
const struct chat_profile_option* options, size_t option_count) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !cc->db) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: update before initialization"); return -1;
|
|
}
|
|
uint64_t myid = cc->my_node_id;
|
|
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
|
|
uint8_t join_sig[64]; uint64_t join_ts = 0;
|
|
if (topo_node_sqlite_member_get_join(cc->db, ch, myid, join_sig, &join_ts) != 0) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: own signed member missing channel=%s", ch); return -1;
|
|
}
|
|
struct profile_json profile = { .text = "{", .used = 1, .avatar = avatar, .options = options, .option_count = option_count };
|
|
if (profile_pair(&profile, "name", inst->name) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: display name exceeds profile limit channel=%s", ch); return -1;
|
|
}
|
|
char query[256]; snprintf(query, sizeof(query), "SELECT update_ts,userinfo FROM \"%s\" WHERE node_id=?", tbl);
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, query, -1, &st, NULL) != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: query failed channel=%s: %s", ch, sqlite3_errmsg(cc->db)); return -1;
|
|
}
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)myid);
|
|
if (sqlite3_step(st) != SQLITE_ROW) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot read own member channel=%s: %s", ch, sqlite3_errmsg(cc->db));
|
|
sqlite3_finalize(st); return -1;
|
|
}
|
|
uint64_t local_uts = (uint64_t)sqlite3_column_int64(st, 0);
|
|
const char* old = (const char*)sqlite3_column_text(st, 1);
|
|
char before[MS_USERINFO_MAX + 1] = "";
|
|
if (old && strlen(old) > MS_USERINFO_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored attributes exceed limit channel=%s", ch);
|
|
sqlite3_finalize(st); return -1;
|
|
}
|
|
if (old) memcpy(before, old, strlen(old) + 1);
|
|
if (old && old[0] && json_flat_parse(old, profile_copy_pair, &profile) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot preserve attributes channel=%s", ch);
|
|
sqlite3_finalize(st); return -1;
|
|
}
|
|
sqlite3_finalize(st);
|
|
for (size_t i = 0; i < option_count; i++) {
|
|
if (options[i].value && profile_pair(&profile, options[i].key, options[i].value) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: option exceeds profile limit channel=%s key=%s", ch, options[i].key);
|
|
return -1;
|
|
}
|
|
}
|
|
if (avatar && server) {
|
|
char id[17]; snprintf(id, sizeof(id), "%016llx", (unsigned long long)server);
|
|
if (profile_pair(&profile, "avatar_server", id) < 0 || profile_pair(&profile, "avatar_file", file) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: avatar exceeds profile limit channel=%s", ch); return -1;
|
|
}
|
|
}
|
|
if (profile.used >= MS_USERINFO_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: attributes exceed profile limit channel=%s", ch); return -1;
|
|
}
|
|
profile.text[profile.used++] = '}'; profile.text[profile.used] = 0;
|
|
if (option_count && !strcmp(before, profile.text)) {
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: options unchanged channel=%s", ch); return 0;
|
|
}
|
|
if (local_uts >= INT64_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: version exhausted channel=%s", ch); return -1; }
|
|
time_t seconds = ntp_time_get_seconds(inst);
|
|
uint64_t now = seconds > 0 ? (uint64_t)seconds : 0;
|
|
uint64_t update_ts = now > local_uts ? now : local_uts + 1;
|
|
uint8_t umsg[MS_USERINFO_MAX + 73], update_sig[64];
|
|
int ulen = member_sync_build_update_msg(join_sig, update_ts, profile.text, umsg, sizeof(umsg));
|
|
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot build signature channel=%s", ch); return -1; }
|
|
if (sc_ed25519_sign(inst->my_ed25519_privkey, umsg, ulen, update_sig) != SC_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: signature failed channel=%s", ch); return -1;
|
|
}
|
|
int rc = member_sync_put(inst, ch, myid, inst->my_keys.public_key, inst->my_ed25519_pubkey, join_sig, join_ts,
|
|
update_sig, update_ts, profile.text, NULL, NULL, 0, 0, NULL);
|
|
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: update failed channel=%s result=%d", ch, rc); return -1; }
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch, (unsigned long long)update_ts);
|
|
return 0;
|
|
}
|
|
|
|
/* Проверить заменяемые ключи до построения/подписи нового блока мембера. */
|
|
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
|
|
const struct chat_profile_option* options, size_t count) {
|
|
if (!inst || !channel || !channel[0] || !options || !count || count > MS_USERINFO_MAX / 4) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid options update count=%zu", count); return -1;
|
|
}
|
|
if (!chat_core_get_db(inst) || !sqlite3_get_autocommit(chat_core_get_db(inst))) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: options update requires initialized DB without outer transaction"); return -1;
|
|
}
|
|
for (size_t i = 0; i < count; i++) {
|
|
const char* key = options[i].key;
|
|
if (!key || !key[0] || strlen(key) > MS_USERINFO_MAX || (options[i].value && strlen(options[i].value) > MS_USERINFO_MAX)
|
|
|| !strcmp(key, "name") || !strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid/reserved option index=%zu key=%s", i, key ? key : "null"); return -1;
|
|
}
|
|
for (size_t j = 0; j < i; j++) if (!strcmp(key, options[j].key)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: duplicate option key=%s", key); return -1;
|
|
}
|
|
}
|
|
return profile_update_channel(inst, channel, 0, 0, NULL, options, count);
|
|
}
|
|
|
|
void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !cc->db) return;
|
|
sqlite3_stmt* cs = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: channel query failed: %s", sqlite3_errmsg(cc->db)); return;
|
|
}
|
|
while (sqlite3_step(cs) == SQLITE_ROW) {
|
|
const char* ch = (const char*)sqlite3_column_text(cs, 0);
|
|
if (ch) profile_update_channel(inst, ch, 0, 0, NULL, NULL, 0);
|
|
}
|
|
sqlite3_finalize(cs);
|
|
}
|
|
|
|
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file) {
|
|
if (!inst || !ch_id || !file || (server && !mf_avatar_manifest_valid(file))) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: invalid avatar reference"); return -1;
|
|
}
|
|
if (server) {
|
|
for (int i = 0; i < 64; i++) if (!((file[i] >= '0' && file[i] <= '9') || (file[i] >= 'a' && file[i] <= 'f'))) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: invalid avatar hash"); return -1;
|
|
}
|
|
uint64_t group = strtoull(ch_id, NULL, 10);
|
|
if (!member_files_member(inst, group, server, 1)) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: avatar server has no storage role channel=%s", ch_id); return -1;
|
|
}
|
|
}
|
|
int rc = profile_update_channel(inst, ch_id, 1, server, file, NULL, 0);
|
|
if (!rc) DEBUG_INFO(DEBUG_CATEGORY_CHAT, "profile: avatar published channel=%s server=%016llx file=%s",
|
|
ch_id, (unsigned long long)server, server ? file : "none");
|
|
return rc;
|
|
}
|
|
|
|
void chat_core_set_my_avatar_trampoline(void* arg) {
|
|
struct chat_avatar_req* q = arg;
|
|
char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)q->group);
|
|
int rc = chat_core_set_my_avatar(q->inst, ch, q->server, q->file);
|
|
uint8_t data[17]; mf_put64(data, q->id); mf_put64(data + 8, q->group); data[16] = rc ? 1 : 0;
|
|
chat_event_post(q->inst, CHAT_EVT_AVATAR_RESULT, data, sizeof(data));
|
|
u_free(q);
|
|
}
|
|
|
|
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst) {
|
|
if (inst)
|
|
topo_node_update_my_addresses(inst);
|
|
chat_core_update_my_member(inst);
|
|
}
|
|
|
|
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !key || !value) return;
|
|
sqlite3_stmt* st = NULL;
|
|
sqlite3_prepare_v2(cc->db,
|
|
"INSERT OR REPLACE INTO ui_state(key, value) VALUES(?, ?)", -1, &st, NULL);
|
|
if (st) {
|
|
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(st, 2, value, -1, SQLITE_STATIC);
|
|
sqlite3_step(st); sqlite3_finalize(st);
|
|
}
|
|
}
|
|
|
|
void chat_core_save_ui_state_trampoline(void* arg) {
|
|
struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg;
|
|
if (!a) return;
|
|
const char* key = a->data;
|
|
const char* value = key + strlen(key) + 1;
|
|
chat_core_save_ui_state(a->inst, key, value);
|
|
u_free(arg);
|
|
}
|