Browse Source

Add atomic updates for signed member profile options

master
evgeny 3 days ago
parent
commit
a33666b778
  1. 6
      src/chat/chat_core.h
  2. 60
      src/chat/chat_profile.c

6
src/chat/chat_core.h

@ -248,6 +248,12 @@ int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
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.
* Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */
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;

60
src/chat/chat_profile.c

@ -49,7 +49,13 @@ void chat_core_update_my_name_trampoline(void* arg) {
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
struct profile_json { char text[MS_USERINFO_MAX + 1]; size_t used; int avatar; };
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;
@ -82,6 +88,7 @@ static int profile_pair(struct profile_json* p, const char* key, const char* val
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);
}
@ -117,7 +124,9 @@ int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, ch
return result;
}
static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file) {
/* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */
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;
@ -128,7 +137,7 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in
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 };
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;
}
@ -144,11 +153,23 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in
}
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) {
@ -159,7 +180,12 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in
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);
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));
@ -174,6 +200,28 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in
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;
@ -183,7 +231,7 @@ void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
}
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);
if (ch) profile_update_channel(inst, ch, 0, 0, NULL, NULL, 0);
}
sqlite3_finalize(cs);
}
@ -201,7 +249,7 @@ int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
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);
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;

Loading…
Cancel
Save