779 lines
35 KiB
C
779 lines
35 KiB
C
/*
|
|
* chat_core.c — жизненный цикл чата: init, destroy, per-instance состояние (inst->chat_core)
|
|
*
|
|
* Под-модули (каждый в своём .c):
|
|
* chat_msg.c — отправка сообщений, DB-операции для chat_sync
|
|
* chat_channel.c — создание/подключение каналов, db_sync instance
|
|
* chat_profile.c — имя узла, адреса, ui_state
|
|
* chat_status.c — сбор статуса NTP+connections
|
|
*
|
|
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
|
|
*/
|
|
|
|
#include "chat_core_priv.h"
|
|
#include "group_avatar.h"
|
|
#include "chat_event.h"
|
|
#include "chat_setting.h"
|
|
#include "chat_member.h"
|
|
#include "member_sync.h"
|
|
#include "chat_sync.h"
|
|
|
|
#include "../utun_instance.h"
|
|
#include "../ntp_time.h"
|
|
#include "../routing_layer/topo_node_sqlite.h"
|
|
#include "../transport_layer/secure_channel.h"
|
|
#include "../media_delivery/media_index.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/json_flat.h"
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include "../../lib/platform_compat.h"
|
|
|
|
/* ─── хелперы ─── */
|
|
|
|
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) {
|
|
size_t i = 0;
|
|
while (*ch_id && i < out_sz - 1) {
|
|
char c = *ch_id++;
|
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
|| (c >= '0' && c <= '9') || c == '_')
|
|
out[i++] = c;
|
|
else
|
|
out[i++] = '_';
|
|
}
|
|
out[i] = '\0';
|
|
}
|
|
|
|
void msg_table_name(const char* ch_id, char* buf, size_t sz) {
|
|
char san[64]; sanitize_ch_id(ch_id, san, sizeof(san));
|
|
snprintf(buf, sz, "msg_%s", san);
|
|
}
|
|
void peers_table_name(const char* ch_id, char* buf, size_t sz) {
|
|
char san[64]; sanitize_ch_id(ch_id, san, sizeof(san));
|
|
snprintf(buf, sz, "peers_%s", san);
|
|
}
|
|
|
|
static int db_exec(struct chat_core_ctx* cc, const char* sql) {
|
|
char* err = NULL;
|
|
int rc = sqlite3_exec(cc->db, sql, NULL, NULL, &err);
|
|
if (rc != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: sql error: %s", CC_ID, err);
|
|
sqlite3_free(err);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* ─── db_sync registry ─── */
|
|
|
|
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
|
if (!inst || !inst->db_sync || !ch_id || !ch_id[0]) return NULL;
|
|
return db_sync_instance_find(inst, strtoull(ch_id, NULL, 10));
|
|
}
|
|
|
|
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id) {
|
|
uint32_t msg_cnt = db_sync_count(si);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: si_register ch=%s mc=%u", CC_ID, ch_id, msg_cnt);
|
|
}
|
|
|
|
/* ─── жизненный цикл ─── */
|
|
|
|
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
|
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; }
|
|
|
|
if (inst->chat_core) return 0;
|
|
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
|
|
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
|
|
inst->chat_core = cc;
|
|
snprintf(cc->db_path, sizeof(cc->db_path), "%s", db_path);
|
|
|
|
if (inst->topo_sqlite_db) {
|
|
cc->db = inst->topo_sqlite_db; cc->shared_db = 1;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)cc->db);
|
|
} else {
|
|
int rc = sqlite3_open_v2(db_path, &cc->db,
|
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: cannot open DB %s: %s",
|
|
CC_ID, db_path, sqlite3_errmsg(cc->db));
|
|
sqlite3_close(cc->db); inst->chat_core = NULL; u_free(cc); return -1;
|
|
}
|
|
sqlite3_exec(cc->db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
|
|
sqlite3_exec(cc->db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
|
|
}
|
|
|
|
if (group_avatar_schema_init(cc->db) < 0) {
|
|
if (!cc->shared_db) sqlite3_close(cc->db);
|
|
inst->chat_core = NULL; u_free(cc); return -1;
|
|
}
|
|
cc->inst = inst;
|
|
cc->my_node_id = inst->node_id;
|
|
|
|
/* перенести chat-настройки из конфига (заполнены парсером) в per-instance состояние */
|
|
if (inst->config) memcpy(&inst->chat_settings, &inst->config->global.chat_settings, sizeof(inst->chat_settings));
|
|
|
|
sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst);
|
|
|
|
/* записать себя в nodes + local_identity */
|
|
{
|
|
const char* my_name = inst->name[0] ? inst->name : "Me";
|
|
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
|
|
my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
|
(uint64_t)now_sec, (time_t)now_sec);
|
|
sqlite3_stmt* st = NULL;
|
|
sqlite3_prepare_v2(cc->db,
|
|
"INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)"
|
|
" VALUES(1,?,?,?,?,?,?)", -1, &st, NULL);
|
|
if (st) {
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
|
|
sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC);
|
|
sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC);
|
|
sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC);
|
|
sqlite3_bind_int64(st, 5, now_sec);
|
|
sqlite3_bind_int64(st, 6, now_sec);
|
|
sqlite3_step(st); sqlite3_finalize(st);
|
|
}
|
|
}
|
|
|
|
cc->initialized = 1;
|
|
|
|
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
|
|
chat_member_init(inst);
|
|
|
|
/* create media_files table */
|
|
media_index_init(cc->db);
|
|
|
|
db_exec(cc,
|
|
"CREATE TABLE IF NOT EXISTS local_identity ("
|
|
" id INTEGER PRIMARY KEY CHECK (id = 1),"
|
|
" node_id INTEGER NOT NULL UNIQUE,"
|
|
" name TEXT NOT NULL,"
|
|
" x25519_pubkey BLOB NOT NULL,"
|
|
" x25519_privkey BLOB,"
|
|
" ed25519_pubkey BLOB,"
|
|
" created_at INTEGER DEFAULT (unixepoch()),"
|
|
" updated_at INTEGER DEFAULT (unixepoch())"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS accounts ("
|
|
" node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id),"
|
|
" display_name TEXT NOT NULL,"
|
|
" avatar_color TEXT DEFAULT '#4A90E2',"
|
|
" avatar_letter TEXT NOT NULL,"
|
|
" is_contact INTEGER DEFAULT 1,"
|
|
" created_at INTEGER DEFAULT (unixepoch())"
|
|
");"
|
|
|
|
"CREATE TABLE IF NOT EXISTS ui_state ("
|
|
" key TEXT PRIMARY KEY,"
|
|
" value TEXT"
|
|
");"
|
|
);
|
|
/* store media_base for block_req handler */
|
|
{
|
|
char mbase[512];
|
|
const char* ls = strrchr(db_path, '/');
|
|
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", (int)(ls - db_path), db_path);
|
|
else snprintf(mbase, sizeof(mbase), "%s", db_path);
|
|
char sql[1024]; snprintf(sql, sizeof(sql),
|
|
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
|
|
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
|
|
}
|
|
|
|
/* seed stub accounts if empty */
|
|
{
|
|
sqlite3_stmt* cnt_st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT COUNT(*) FROM accounts",
|
|
-1, &cnt_st, NULL) == SQLITE_OK) {
|
|
if (sqlite3_step(cnt_st) == SQLITE_ROW && sqlite3_column_int(cnt_st, 0) == 0) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: seeding 5 stub accounts", CC_ID);
|
|
struct { int64_t id; const char* name; const char* letter; const char* color; } accs[] = {
|
|
{1, "Alice", "A", "#4A90E2"}, {2, "Bob", "B", "#7ED321"},
|
|
{3, "Carol", "C", "#F5A623"}, {4, "Dave", "D", "#9013FE"},
|
|
{5, "Eve", "E", "#00B894"},
|
|
};
|
|
for (int i = 0; i < 5; i++) {
|
|
sqlite3_stmt* ins = NULL;
|
|
sqlite3_prepare_v2(cc->db,
|
|
"INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter,created_at)"
|
|
" VALUES(?,?,?,?,?)", -1, &ins, NULL);
|
|
if (ins) {
|
|
sqlite3_bind_int64(ins, 1, accs[i].id);
|
|
sqlite3_bind_text(ins, 2, accs[i].name, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(ins, 3, accs[i].color, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(ins, 4, accs[i].letter, -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(ins, 5, now_sec);
|
|
sqlite3_step(ins); sqlite3_finalize(ins);
|
|
}
|
|
}
|
|
}
|
|
sqlite3_finalize(cnt_st);
|
|
}
|
|
}
|
|
|
|
/* ensure infrastructure for all existing channels (survives restart) */
|
|
{
|
|
sqlite3_stmt* stmt = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels ORDER BY created_at ASC",
|
|
-1, &stmt, NULL) == SQLITE_OK) {
|
|
int loaded = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* ch = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (ch && ch[0]) { chat_core_ensure_channel_ready(inst, ch); loaded++; }
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: loaded %d channels from DB", CC_ID, loaded);
|
|
}
|
|
}
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: initialized, db=%s node_id=0x%016llx",
|
|
CC_ID, db_path, (unsigned long long)cc->my_node_id);
|
|
|
|
{ uint8_t nid[8]; memcpy(nid, &cc->my_node_id, 8); chat_event_post(inst, CHAT_EVT_MY_NODE_ID, nid, 8); }
|
|
|
|
chat_event_post(inst, CHAT_EVT_DB_READY, NULL, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void chat_core_destroy(struct UTUN_INSTANCE* inst) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized) return;
|
|
cc->initialized = 0;
|
|
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
|
|
|
|
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); }
|
|
inst->chat_core = NULL;
|
|
u_free(cc);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
|
|
}
|
|
|
|
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->db : NULL; }
|
|
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->initialized : 0; }
|
|
|
|
/* ─── chat settings (GUI → uasync) ─── */
|
|
|
|
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
|
|
chat_setting_set(inst, name, value);
|
|
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(inst);
|
|
}
|
|
|
|
void chat_core_set_setting_trampoline(void* arg) {
|
|
struct chat_setting_arg* a = (struct chat_setting_arg*)arg;
|
|
const char* name = a->data;
|
|
const char* value = name + strlen(name) + 1;
|
|
chat_core_set_setting(a->inst, name, value);
|
|
u_free(arg);
|
|
}
|
|
|
|
/* ─── per-channel «play sound on new message» ─── */
|
|
|
|
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return 1;
|
|
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) != SQLITE_OK) return 1;
|
|
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
|
|
int on = 1;
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
const char* v = (const char*)sqlite3_column_text(st, 0);
|
|
if (v) on = atoi(v) != 0;
|
|
}
|
|
sqlite3_finalize(st);
|
|
return on;
|
|
}
|
|
|
|
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
|
|
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
|
|
char val[2]; snprintf(val, sizeof(val), "%d", on ? 1 : 0);
|
|
chat_core_save_ui_state(inst, key, val);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: sound_on_message ch=%s = %d", CC_ID, ch_id, on ? 1 : 0);
|
|
chat_sync_broadcast_sound_setting(inst, ch_id, on ? 1 : 0);
|
|
}
|
|
|
|
void chat_core_set_sound_on_message_trampoline(void* arg) {
|
|
struct chat_sound_arg* a = (struct chat_sound_arg*)arg;
|
|
if (!a) return;
|
|
chat_core_set_sound_on_message(a->inst, a->ch_id, a->on);
|
|
u_free(arg);
|
|
}
|
|
|
|
/* ─── headless API ─── */
|
|
|
|
static void json_escape(const char* src, char* dst, size_t dst_sz) {
|
|
size_t i = 0; const char* s = src;
|
|
while (*s && i < dst_sz - 2) {
|
|
const char* escape = NULL;
|
|
switch (*s) {
|
|
case '"': escape = "\\\""; break;
|
|
case '\\': escape = "\\\\"; break;
|
|
case '\n': escape = "\\n"; break;
|
|
case '\r': escape = "\\r"; break;
|
|
case '\t': escape = "\\t"; break;
|
|
case '\b': escape = "\\b"; break;
|
|
case '\f': escape = "\\f"; break;
|
|
}
|
|
if (escape) {
|
|
if (i + 2 >= dst_sz) break;
|
|
dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue;
|
|
}
|
|
dst[i++] = *s++;
|
|
}
|
|
dst[i] = '\0';
|
|
}
|
|
|
|
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !buf || !out_len) return -1;
|
|
char* w = buf; char* end = buf + buf_size;
|
|
*w++ = '['; const char* sep = "";
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db,
|
|
"SELECT c.channel_id, c.name, c.owner_node_id, "
|
|
"(SELECT COUNT(*) FROM nodes WHERE node_id=c.owner_node_id AND name IS NOT NULL) AS has_owner_name, "
|
|
"(SELECT name FROM nodes WHERE node_id=c.owner_node_id) AS owner_name "
|
|
"FROM channels c ORDER BY c.created_at ASC",
|
|
-1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
|
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
|
const char* ch_id = (const char*)sqlite3_column_text(st, 0);
|
|
const char* ch_name = (const char*)sqlite3_column_text(st, 1);
|
|
uint64_t owner_id = (uint64_t)sqlite3_column_int64(st, 2);
|
|
const char* owner_name = (const char*)sqlite3_column_text(st, 4);
|
|
if (!ch_id) continue;
|
|
|
|
uint32_t msg_count = chat_core_count(inst, ch_id);
|
|
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
|
|
int peer_count = 0, online_count = 0;
|
|
sqlite3_stmt* ps = NULL;
|
|
char psql[200]; snprintf(psql, sizeof(psql), "SELECT node_id FROM \"%s\" WHERE deleted=0", peers_tbl);
|
|
if (sqlite3_prepare_v2(cc->db, psql, -1, &ps, NULL) == SQLITE_OK) {
|
|
while (sqlite3_step(ps) == SQLITE_ROW) {
|
|
uint64_t pid = (uint64_t)sqlite3_column_int64(ps, 0);
|
|
peer_count++;
|
|
if (chat_core_member_online(inst, ch_id, pid)) online_count++;
|
|
}
|
|
sqlite3_finalize(ps);
|
|
}
|
|
|
|
char esc_name[256], esc_owner[256];
|
|
json_escape(ch_name ? ch_name : "", esc_name, sizeof(esc_name));
|
|
json_escape(owner_name ? owner_name : "", esc_owner, sizeof(esc_owner));
|
|
|
|
w += snprintf(w, (size_t)(end - w),
|
|
"%s{\"id\":\"%s\",\"name\":\"%s\",\"owner_id\":\"0x%016llx\",\"owner_name\":\"%s\","
|
|
"\"peers\":%d,\"msgs\":%u,\"online\":%d}",
|
|
sep, ch_id, esc_name, (unsigned long long)owner_id, esc_owner, peer_count, msg_count, online_count);
|
|
sep = ",";
|
|
}
|
|
sqlite3_finalize(st);
|
|
*w++ = ']';
|
|
*w = '\0';
|
|
*out_len = (size_t)(w - buf);
|
|
return 0;
|
|
}
|
|
|
|
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) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
|
|
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
|
|
char* w = buf; char* end = buf + buf_size;
|
|
*w++ = '['; const char* sep = "";
|
|
char sql[256];
|
|
snprintf(sql, sizeof(sql),
|
|
"SELECT m.id, m.timestamp, m.node_id, CAST(m.data AS TEXT), n.name, m.local_attrs "
|
|
"FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id "
|
|
"ORDER BY m.timestamp DESC LIMIT ? OFFSET ?", tbl);
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
|
|
sqlite3_bind_int(st, 1, count > 0 ? count : 100);
|
|
sqlite3_bind_int(st, 2, offset);
|
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
|
int64_t id = sqlite3_column_int64(st, 0);
|
|
int64_t ts = sqlite3_column_int64(st, 1);
|
|
uint64_t author_id = (uint64_t)sqlite3_column_int64(st, 2);
|
|
const char* data = (const char*)sqlite3_column_text(st, 3);
|
|
const char* author_name = (const char*)sqlite3_column_text(st, 4);
|
|
const char* la = (const char*)sqlite3_column_text(st, 5);
|
|
if (!data) data = "";
|
|
if (!la) la = "";
|
|
const char* ct = "text";
|
|
char ctbuf[32] = {0};
|
|
const char* ctpos = strstr(data, "\"ct\":\"");
|
|
if (ctpos) { ctpos += 6; int ci = 0; while (*ctpos && *ctpos != '"' && ci < 31) ctbuf[ci++] = *ctpos++; ct = ctbuf; }
|
|
char esc_data[4096], esc_aname[256], esc_la[256];
|
|
json_escape(data, esc_data, sizeof(esc_data));
|
|
json_escape(author_name ? author_name : "", esc_aname, sizeof(esc_aname));
|
|
json_escape(la, esc_la, sizeof(esc_la));
|
|
w += snprintf(w, (size_t)(end - w),
|
|
"%s{\"id\":%lld,\"ts\":%lld,\"author_id\":\"0x%016llx\",\"author_name\":\"%s\","
|
|
"\"content_type\":\"%s\",\"data\":\"%.2000s\",\"local_attrs\":\"%s\"}",
|
|
sep, (long long)id, (long long)ts, (unsigned long long)author_id, esc_aname, ct, esc_data, esc_la);
|
|
sep = ",";
|
|
}
|
|
sqlite3_finalize(st);
|
|
*w++ = ']';
|
|
*w = '\0';
|
|
*out_len = (size_t)(w - buf);
|
|
return 0;
|
|
}
|
|
|
|
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1;
|
|
*out_len = 0;
|
|
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
|
|
char* w = buf; char* end = buf + buf_size;
|
|
*w++ = '['; const char* sep = "";
|
|
char sql[256];
|
|
snprintf(sql, sizeof(sql),
|
|
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
|
|
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
|
|
sqlite3_stmt* st = NULL;
|
|
sqlite3_stmt* as = NULL;
|
|
#define MEMBERS_APPEND(...) do { \
|
|
int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \
|
|
if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \
|
|
w += written; \
|
|
} while (0)
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
|
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
|
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
|
|
const char* name = (const char*)sqlite3_column_text(st, 1);
|
|
int online = chat_core_member_online(inst, ch_id, nid);
|
|
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(st, 2);
|
|
const uint8_t* ed25519 = sqlite3_column_blob(st, 3);
|
|
int x25519_len = sqlite3_column_bytes(st, 2);
|
|
int ed25519_len = sqlite3_column_bytes(st, 3);
|
|
const char* adm_tags = (const char*)sqlite3_column_text(st, 4);
|
|
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1];
|
|
json_escape(name ? name : "", esc_name, sizeof(esc_name));
|
|
x25519_hex[0] = ed25519_hex[0] = '\0';
|
|
if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]);
|
|
if (ed25519 && ed25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(ed25519_hex + i*2, 3, "%02x", ed25519[i]);
|
|
|
|
/* check connected */
|
|
int connected = 0;
|
|
if (inst && inst->connections) {
|
|
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&nid);
|
|
if (e) connected = 1;
|
|
}
|
|
|
|
json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags));
|
|
|
|
MEMBERS_APPEND(
|
|
"%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d,"
|
|
"\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[",
|
|
sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex);
|
|
sep = ",";
|
|
|
|
/* addresses */
|
|
char asql[200]; snprintf(asql, sizeof(asql),
|
|
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
|
|
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
|
|
const char* asep2 = "";
|
|
while (sqlite3_step(as) == SQLITE_ROW) {
|
|
int fam = sqlite3_column_int(as, 0);
|
|
int proto = sqlite3_column_int(as, 1);
|
|
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(as, 2);
|
|
int addr_len = sqlite3_column_bytes(as, 2);
|
|
int port = sqlite3_column_int(as, 3);
|
|
int rtt = sqlite3_column_int(as, 4);
|
|
char ip_str[64]; ip_str[0] = '\0';
|
|
if (fam == 4 && addr && addr_len >= 4)
|
|
snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]);
|
|
else if (fam == 6 && addr && addr_len >= 16)
|
|
snprintf(ip_str, sizeof(ip_str), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
|
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7],
|
|
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]);
|
|
else { snprintf(ip_str, sizeof(ip_str), "?"); }
|
|
MEMBERS_APPEND(
|
|
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}",
|
|
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt);
|
|
asep2 = ",";
|
|
}
|
|
sqlite3_finalize(as); as = NULL;
|
|
}
|
|
MEMBERS_APPEND("]}");
|
|
}
|
|
MEMBERS_APPEND("]");
|
|
sqlite3_finalize(st);
|
|
*out_len = (size_t)(w - buf);
|
|
return 0;
|
|
overflow:
|
|
sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size);
|
|
return -1;
|
|
#undef MEMBERS_APPEND
|
|
}
|
|
|
|
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !out) return -1;
|
|
out[0] = '\0';
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
const char* n = (const char*)sqlite3_column_text(st, 0);
|
|
if (n) snprintf(out, sz, "%s", n);
|
|
}
|
|
sqlite3_finalize(st);
|
|
return 0;
|
|
}
|
|
|
|
/* ─── adm_tags editing (channel owner only) ─── */
|
|
|
|
struct chat_member_tags_key {
|
|
char key[CHAT_MEMBER_TAGS_KEY_SZ];
|
|
char* val;
|
|
char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */
|
|
};
|
|
|
|
struct chat_member_tags {
|
|
char ch_id[64];
|
|
uint64_t node_id;
|
|
int key_count;
|
|
struct chat_member_tags_key keys[CHAT_MEMBER_TAGS_MAX_KEYS];
|
|
};
|
|
|
|
static int tags_find(struct chat_member_tags* t, const char* key) {
|
|
for (int i = 0; i < t->key_count; i++)
|
|
if (strcmp(t->keys[i].key, key) == 0) return i;
|
|
return -1;
|
|
}
|
|
|
|
static const char* tags_append_effective(const char* raw, uint64_t* ts_out) {
|
|
if (!raw || !raw[0]) { if (ts_out) *ts_out = 0; return "no"; }
|
|
const char* last = raw;
|
|
for (const char* p = raw; *p; p++)
|
|
if (*p == 'e' || *p == 'd') last = p;
|
|
if (ts_out) *ts_out = strtoull(last + 1, NULL, 10);
|
|
return *last == 'e' ? "yes" : "no";
|
|
}
|
|
|
|
static int tags_is_append_key(const char* key) {
|
|
return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0;
|
|
}
|
|
|
|
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
|
|
struct chat_member_tags* t = (struct chat_member_tags*)arg;
|
|
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1;
|
|
}
|
|
struct chat_member_tags_key* k = &t->keys[t->key_count];
|
|
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
|
|
k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val);
|
|
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
|
|
if (!k->val || (tags_is_append_key(key) && !k->raw)) {
|
|
u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k));
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1;
|
|
}
|
|
t->key_count++;
|
|
return 0;
|
|
}
|
|
|
|
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!cc || !cc->initialized || !ch_id || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; }
|
|
struct chat_member_tags* t = u_calloc(1, sizeof(*t));
|
|
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — OOM", CC_ID); return NULL; }
|
|
snprintf(t->ch_id, sizeof(t->ch_id), "%s", ch_id);
|
|
t->node_id = node_id;
|
|
|
|
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
|
|
sqlite3_stmt* st = NULL;
|
|
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl);
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
const char* tags = (const char*)sqlite3_column_text(st, 0);
|
|
if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) {
|
|
sqlite3_finalize(st); chat_member_tags_free(t); return NULL;
|
|
}
|
|
}
|
|
sqlite3_finalize(st);
|
|
}
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load ch=%s nid=0x%016llx keys=%d",
|
|
CC_ID, ch_id, (unsigned long long)node_id, t->key_count);
|
|
return t;
|
|
}
|
|
|
|
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) {
|
|
if (!t || !key) return -1;
|
|
if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; }
|
|
if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
|
|
|
|
int idx = tags_find(t, key);
|
|
if (!val) {
|
|
if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; }
|
|
return 0;
|
|
}
|
|
char* copy = u_strdup(val);
|
|
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; }
|
|
if (idx >= 0) {
|
|
u_free(t->keys[idx].val); t->keys[idx].val = copy;
|
|
} else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) {
|
|
struct chat_member_tags_key* k = &t->keys[t->key_count++];
|
|
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
|
|
k->val = copy;
|
|
k->raw = NULL;
|
|
} else {
|
|
u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) {
|
|
if (!t || !key) return NULL;
|
|
int idx = tags_find(t, key);
|
|
return idx >= 0 ? t->keys[idx].val : NULL;
|
|
}
|
|
|
|
int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
|
|
if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1;
|
|
}
|
|
struct chat_member_tags parsed = {0};
|
|
int rc = json_flat_parse(json, tags_json_cb, &parsed);
|
|
if (rc != 0) {
|
|
for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); }
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1;
|
|
}
|
|
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
|
|
t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys));
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d",
|
|
CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count);
|
|
return 0;
|
|
}
|
|
|
|
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
|
|
}
|
|
char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1;
|
|
#define TAGS_ADVANCE(expr) do { \
|
|
int written = (expr); \
|
|
if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \
|
|
} \
|
|
off += written; \
|
|
} while (0)
|
|
int vi = tags_find(t, "ver");
|
|
if (vi >= 0) ver = atoi(t->keys[vi].val) + 1;
|
|
TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver));
|
|
|
|
for (int i = 0; i < t->key_count; i++) {
|
|
struct chat_member_tags_key* k = &t->keys[i];
|
|
if (strcmp(k->key, "ver") == 0) continue;
|
|
|
|
const char* out_val;
|
|
if (k->raw) {
|
|
uint64_t old_ts = 0;
|
|
const char* old_eff = tags_append_effective(k->raw, &old_ts);
|
|
int old_yes = (*old_eff == 'y');
|
|
int new_yes = (k->val[0] == 'y'); /* val = "yes" or "yes:<ts>" */
|
|
if (new_yes != old_yes) {
|
|
uint64_t ts = (uint64_t)ntp_time_get_seconds(cc->inst);
|
|
size_t oldl = strlen(k->raw);
|
|
char* nraw = u_malloc(oldl + 24);
|
|
if (!nraw) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — OOM for raw", CC_ID); return -1; }
|
|
snprintf(nraw, oldl + 24, "%s%c%llu", k->raw, new_yes ? 'e' : 'd', (unsigned long long)ts);
|
|
u_free(k->raw); k->raw = nraw;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit append %s=%c%llu ch=%s nid=0x%016llx old_last=%c",
|
|
CC_ID, k->key, new_yes ? 'e' : 'd', (unsigned long long)ts, t->ch_id, (unsigned long long)t->node_id, old_yes ? 'e' : 'd');
|
|
}
|
|
out_val = k->raw;
|
|
} else {
|
|
out_val = k->val;
|
|
}
|
|
char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1];
|
|
if (strlen(out_val) > MS_ADM_TAGS_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1;
|
|
}
|
|
json_escape(k->key, escaped_key, sizeof(escaped_key));
|
|
json_escape(out_val, escaped_value, sizeof(escaped_value));
|
|
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value));
|
|
}
|
|
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}"));
|
|
#undef TAGS_ADVANCE
|
|
|
|
uint8_t ch_ed_priv[32];
|
|
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
|
|
}
|
|
uint8_t sign_msg[MS_ADM_TAGS_MAX + 8];
|
|
int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg));
|
|
if (mlen < 0) return -1;
|
|
uint8_t sig[64];
|
|
if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1;
|
|
}
|
|
|
|
char peers_tbl[80]; peers_table_name(t->ch_id, peers_tbl, sizeof(peers_tbl));
|
|
sqlite3_stmt* st = NULL;
|
|
char sql[384]; snprintf(sql, sizeof(sql),
|
|
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage, source"
|
|
" FROM \"%s\" WHERE node_id=?", peers_tbl);
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1;
|
|
}
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)t->node_id);
|
|
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st);
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member not found ch=%s nid=0x%016llx", CC_ID, t->ch_id, (unsigned long long)t->node_id); return -1;
|
|
}
|
|
const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(st, 0);
|
|
const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(st, 1);
|
|
const uint8_t* jsig = (const uint8_t*)sqlite3_column_blob(st, 2);
|
|
uint64_t jts = (uint64_t)sqlite3_column_int64(st, 3);
|
|
const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(st, 4);
|
|
uint64_t uts = (uint64_t)sqlite3_column_int64(st, 5);
|
|
const char* uinfo = (const char*)sqlite3_column_text(st, 6);
|
|
int stg = sqlite3_column_int(st, 7);
|
|
int src = sqlite3_column_int(st, 8);
|
|
if (!x25 || !ed) { sqlite3_finalize(st);
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member has no keys ch=%s", CC_ID, t->ch_id); return -1;
|
|
}
|
|
if (src != 0) { sqlite3_finalize(st);
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — cannot set flags on placeholder member (source=%d) ch=%s nid=0x%016llx",
|
|
CC_ID, src, t->ch_id, (unsigned long long)t->node_id);
|
|
return -1;
|
|
}
|
|
const char* stg_val = chat_member_tags_get(t, "storage");
|
|
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
|
|
|
|
int rc = member_sync_put(inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
|
|
uinfo ? uinfo : "", json, sig, stg, 0, NULL);
|
|
sqlite3_finalize(st);
|
|
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d",
|
|
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off);
|
|
return 0;
|
|
}
|
|
|
|
void chat_member_tags_free(struct chat_member_tags* t) {
|
|
if (!t) return;
|
|
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
|
|
u_free(t);
|
|
}
|
|
|
|
/* ─── trampoline for chatgui ─── */
|
|
|
|
void chat_member_tags_commit_trampoline(void* arg) {
|
|
struct chat_member_tags_req* req = (struct chat_member_tags_req*)arg;
|
|
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
|
|
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
|
|
if (!t) { u_free(arg); return; }
|
|
if (req->json[0]) chat_member_tags_replace(t, req->json);
|
|
chat_member_tags_commit(req->inst, t);
|
|
chat_member_tags_free(t);
|
|
u_free(arg);
|
|
}
|