|
|
|
|
@ -11,6 +11,7 @@
|
|
|
|
|
#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" |
|
|
|
|
@ -48,64 +49,148 @@ void chat_core_update_my_name_trampoline(void* arg) {
|
|
|
|
|
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
|
|
|
|
|
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */ |
|
|
|
|
|
|
|
|
|
void chat_core_update_my_member(struct UTUN_INSTANCE* inst) { |
|
|
|
|
struct chat_core_ctx* cc = CC(inst); |
|
|
|
|
if (!cc || !cc->initialized || !cc->db || !inst) return; |
|
|
|
|
struct profile_json { char text[MS_USERINFO_MAX + 1]; size_t used; int avatar; }; |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
uint64_t myid = cc->my_node_id; |
|
|
|
|
const char* my_name = inst->name[0] ? inst->name : ""; |
|
|
|
|
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", my_name); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
sqlite3_stmt* cs = NULL; |
|
|
|
|
sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL); |
|
|
|
|
if (!cs) return; |
|
|
|
|
while (sqlite3_step(cs) == SQLITE_ROW) { |
|
|
|
|
const char* ch = (const char*)sqlite3_column_text(cs, 0); |
|
|
|
|
if (!ch) continue; |
|
|
|
|
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; |
|
|
|
|
return profile_pair(p, key, value); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file) { |
|
|
|
|
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) continue; |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
uint64_t local_uts = 0; |
|
|
|
|
{ char q[256]; snprintf(q, sizeof(q), "SELECT update_ts FROM \"%s\" WHERE node_id=?", tbl); |
|
|
|
|
struct profile_json profile = { .text = "{", .used = 1, .avatar = avatar }; |
|
|
|
|
if (profile_pair(&profile, "name", inst->name) < 0) return -1; |
|
|
|
|
{ char q[256]; snprintf(q, sizeof(q), "SELECT update_ts,userinfo FROM \"%s\" WHERE node_id=?", tbl); |
|
|
|
|
sqlite3_stmt* ps = NULL; |
|
|
|
|
if (sqlite3_prepare_v2(cc->db, q, -1, &ps, NULL) == SQLITE_OK) { |
|
|
|
|
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid); |
|
|
|
|
if (sqlite3_step(ps) == SQLITE_ROW) local_uts = (uint64_t)sqlite3_column_int64(ps, 0); |
|
|
|
|
if (sqlite3_step(ps) == SQLITE_ROW) { |
|
|
|
|
local_uts = (uint64_t)sqlite3_column_int64(ps, 0); |
|
|
|
|
const char* old = (const char*)sqlite3_column_text(ps, 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(ps); return -1; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
sqlite3_finalize(ps); |
|
|
|
|
} else { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: query failed channel=%s: %s", ch, sqlite3_errmsg(cc->db)); 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; |
|
|
|
|
|
|
|
|
|
uint64_t now = (uint64_t)ntp_time_get_seconds(inst); |
|
|
|
|
uint64_t update_ts = (now > local_uts) ? now : (local_uts + 1); /* строго больше локального */ |
|
|
|
|
|
|
|
|
|
uint8_t umsg[8192]; |
|
|
|
|
int ulen = member_sync_build_update_msg(join_sig, update_ts, juser, umsg, (int)sizeof(umsg)); |
|
|
|
|
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: build_update_msg failed ch=%s", CC_ID, ch); continue; } |
|
|
|
|
int ulen = member_sync_build_update_msg(join_sig, update_ts, profile.text, umsg, (int)sizeof(umsg)); |
|
|
|
|
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: build_update_msg failed ch=%s", CC_ID, ch); return -1; } |
|
|
|
|
uint8_t update_sig[64]; memset(update_sig, 0, 64); |
|
|
|
|
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, inst->my_ed25519_privkey, 32); |
|
|
|
|
if (pkey) { |
|
|
|
|
EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); |
|
|
|
|
if (mdctx) { |
|
|
|
|
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1) |
|
|
|
|
EVP_DigestSign(mdctx, update_sig, &(size_t){64}, umsg, (size_t)ulen); |
|
|
|
|
EVP_MD_CTX_free(mdctx); |
|
|
|
|
} |
|
|
|
|
EVP_PKEY_free(pkey); |
|
|
|
|
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 r2 = member_sync_put(inst, ch, myid, inst->my_keys.public_key, |
|
|
|
|
inst->my_ed25519_pubkey, join_sig, join_ts, |
|
|
|
|
update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL); |
|
|
|
|
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2); |
|
|
|
|
update_sig, update_ts, profile.text, NULL, NULL, 0, 0, NULL); |
|
|
|
|
if (r2 < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2); 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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
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 && (strlen(file) != 68 || strcmp(file + 64, ".png")))) { |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
|