You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

415 lines
25 KiB

#include "group_avatar.h"
#include "chat_core_priv.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../member_files/member_files.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/etcp_router.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include <openssl/sha.h>
#define GA_STATE 1
#define GA_GET 2
#define GA_DATA 3
#define GA_WIRE_VERSION 1
#define GA_STATE_BYTES 44
#define GA_DATA_HEADER 112
#define GA_STREAMS 16
#define GA_RETRY_TB 150000ULL
#define GA_EXCHANGE_TB 300000ULL
struct ga_peer {
struct ga_peer* next;
struct group_avatar* owner;
uint64_t group, peer, epoch, advertised_at, requested_at, rx_at, tx_at;
uint64_t remote_version;
uint8_t remote_hash[32];
int state_pending, get_pending, reply_pending, seen;
uint16_t tx_offset, rx_offset;
struct group_avatar_record *tx, *rx;
struct queue_waiter_handle waiter;
void* pump;
};
struct group_avatar { struct UTUN_INSTANCE* inst; struct ga_peer* peers; void* timer; unsigned tx_count, rx_count; int closing; };
static void ga_pump(void* arg);
static void ga_schedule(struct ga_peer* peer);
static struct group_avatar* ga_context(struct UTUN_INSTANCE* inst) { return inst && CC(inst) ? CC(inst)->group_avatar : NULL; }
static void ga_channel(uint64_t group, char text[32]) { snprintf(text, 32, "%llu", (unsigned long long)group); }
static int ga_message(uint64_t group, const struct group_avatar_record* record, uint8_t* bytes) {
const char domain[] = "group-avatar-v1"; size_t off = sizeof(domain) - 1;
memcpy(bytes, domain, off); mf_put64(bytes + off, group); off += 8;
mf_put64(bytes + off, record->version); off += 8;
bytes[off++] = (uint8_t)record->size; bytes[off++] = (uint8_t)(record->size >> 8);
memcpy(bytes + off, record->data, record->size); return (int)(off + record->size);
}
static int ga_verify(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
if (!inst || !CC(inst) || !group || !r->version || r->version > INT64_MAX || r->size > GROUP_AVATAR_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: invalid record group=%llu", (unsigned long long)group); return -1;
}
uint8_t hash[32], pub[32], bytes[GROUP_AVATAR_MAX + 64]; char ch[32]; ga_channel(group, ch);
SHA256(r->data, r->size, hash);
if (memcmp(hash, r->hash, 32) || topo_node_sqlite_channel_get(CC(inst)->db, ch, NULL, 0, NULL, NULL, pub, NULL) < 0 ||
sc_ed25519_verify(pub, bytes, ga_message(group, r, bytes), r->signature) != SC_OK) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: hash/signature rejected group=%llu version=%llu bytes=%u",
(unsigned long long)group, (unsigned long long)r->version, r->size); return -1;
}
return 0;
}
int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* r) {
if (!inst || !CC(inst) || !CC(inst)->db || !r) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid read"); return -1; }
memset(r, 0, sizeof(*r)); char ch[32]; ga_channel(group, ch); sqlite3_stmt* st = NULL;
const char* sql = "SELECT avatar_version,avatar_data,avatar_hash,avatar_signature FROM channels WHERE channel_id=?";
int rc = -1;
if (sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, ch, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
r->version = (uint64_t)sqlite3_column_int64(st, 0); int size = sqlite3_column_bytes(st, 1);
if ((!r->version && !size && !sqlite3_column_bytes(st, 2) && !sqlite3_column_bytes(st, 3)) ||
(r->version && r->version <= INT64_MAX && size <= GROUP_AVATAR_MAX &&
sqlite3_column_bytes(st, 2) == 32 && sqlite3_column_bytes(st, 3) == 64)) {
r->size = (uint16_t)size;
if (r->version) {
if (size) memcpy(r->data, sqlite3_column_blob(st, 1), size);
memcpy(r->hash, sqlite3_column_blob(st, 2), 32); memcpy(r->signature, sqlite3_column_blob(st, 3), 64);
}
rc = 0;
}
}
sqlite3_finalize(st);
}
if (!rc && r->version) rc = ga_verify(inst, group, r);
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: stored record unavailable/invalid group=%llu: %s",
(unsigned long long)group, sqlite3_errmsg(CC(inst)->db));
return rc;
}
static int ga_compare(uint64_t a, const uint8_t ah[32], uint64_t b, const uint8_t bh[32]) {
return a != b ? (a > b ? 1 : -1) : memcmp(ah, bh, 32);
}
static void ga_changed(struct UTUN_INSTANCE* inst, uint64_t group) {
struct group_avatar* ctx = ga_context(inst);
for (struct ga_peer* p = ctx ? ctx->peers : NULL; p; p = p->next) if (p->group == group) {
p->state_pending = 1; ga_schedule(p);
}
char ch[32]; ga_channel(group, ch); uint8_t event[33]; event[0] = (uint8_t)strlen(ch);
memcpy(event + 1, ch, event[0]); chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, event, event[0] + 1);
}
int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* r) {
if (!r || ga_verify(inst, group, r) < 0) return -1;
struct group_avatar_record current;
if (group_avatar_get(inst, group, &current) < 0) return -1;
int newer = ga_compare(r->version, r->hash, current.version, current.hash);
if (r->version == current.version && memcmp(r->hash, current.hash, 32))
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: concurrent versions group=%llu version=%llu accept=%d",
(unsigned long long)group, (unsigned long long)r->version, newer > 0);
if (newer <= 0) return 0;
sqlite3_stmt* st = NULL; char ch[32]; ga_channel(group, ch); int rc = -1;
const char* sql = "UPDATE channels SET avatar_version=?,avatar_data=?,avatar_hash=?,avatar_signature=? WHERE channel_id=?";
if (sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)r->version); sqlite3_bind_blob(st, 2, r->data, r->size, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, r->hash, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 4, r->signature, 64, SQLITE_STATIC);
sqlite3_bind_text(st, 5, ch, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_DONE && sqlite3_changes(CC(inst)->db) == 1) rc = 1;
sqlite3_finalize(st);
}
if (rc < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: database update failed group=%llu: %s",
(unsigned long long)group, sqlite3_errmsg(CC(inst)->db));
else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: saved group=%llu version=%llu bytes=%u",
(unsigned long long)group, (unsigned long long)r->version, r->size); ga_changed(inst, group);
}
return rc;
}
int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size) {
if (!inst || size > GROUP_AVATAR_MAX || (size && !data) || !member_files_member(inst, group, inst->node_id, 0)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication denied group=%llu bytes=%zu", (unsigned long long)group, size); return -1;
}
uint8_t key[32], bytes[GROUP_AVATAR_MAX + 64]; char ch[32]; ga_channel(group, ch);
if (topo_node_sqlite_channel_get_priv(CC(inst)->db, ch, key) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires group key group=%llu", (unsigned long long)group); return -1;
}
struct group_avatar_record r;
if (group_avatar_get(inst, group, &r) < 0 || r.version == INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: cannot allocate version group=%llu", (unsigned long long)group); return -1;
}
uint8_t hash[32]; SHA256(size ? data : r.data, size, hash);
if (r.version && r.size == size && !memcmp(r.hash, hash, 32)) return 0;
int64_t us = ntp_time_get_us(inst); uint64_t now = us > 0 ? (uint64_t)us / 1000 : 0;
r.version = now > r.version ? now : r.version + 1; r.size = (uint16_t)size;
memcpy(r.hash, hash, 32); if (size) memcpy(r.data, data, size);
int n = ga_message(group, &r, bytes); int signed_ok = sc_ed25519_sign(key, bytes, n, r.signature); memset(key, 0, sizeof(key));
if (signed_ok != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: signing failed group=%llu", (unsigned long long)group); return -1; }
return group_avatar_apply(inst, group, &r) > 0 ? 0 : -1;
}
void group_avatar_gui_set(void* arg) {
struct group_avatar_gui_request* q = arg;
int rc = group_avatar_set(q->inst, q->group, q->data, q->size);
uint8_t result[17]; mf_put64(result, q->id); mf_put64(result + 8, q->group); result[16] = rc ? 1 : 0;
chat_event_post(q->inst, CHAT_EVT_GROUP_AVATAR_RESULT, result, sizeof(result)); u_free(q);
}
static struct ga_peer* ga_find(struct group_avatar* ctx, uint64_t group, uint64_t peer, int create) {
for (struct ga_peer* p = ctx->peers; p; p = p->next) if (p->group == group && p->peer == peer) return p;
if (!create) return NULL;
struct ga_peer* p = u_calloc(1, sizeof(*p));
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: peer allocation failed"); return NULL; }
p->owner = ctx; p->group = group; p->peer = peer; p->state_pending = 1; p->next = ctx->peers; ctx->peers = p; return p;
}
static int ga_allowed(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer) {
if (!member_files_member(inst, group, inst->node_id, 0) || !member_files_member(inst, group, peer, 0)) return 0;
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, group);
for (struct ll_entry* e = g && g->senders_list ? g->senders_list->head : NULL; e; e = e->next) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data;
if (item && item->node_id == peer && item->conn && item->ready_notified && !item->transport_down) return 1;
}
return 0;
}
static void ga_release(struct ga_peer* p, int sending) {
struct group_avatar_record** record = sending ? &p->tx : &p->rx;
if (!*record) return;
u_free(*record); *record = NULL;
if (sending) p->owner->tx_count--; else p->owner->rx_count--;
if (!p->owner->closing) for (struct ga_peer* other = p->owner->peers; other; other = other->next) {
ga_schedule(other);
if (!sending) etcp_router_resume_receive(p->owner->inst, other->group, other->peer, ETCP_RT_ID_GROUP_AVATAR);
}
}
static void ga_free(struct ga_peer* p) {
if (p->pump) uasync_call_soon_cancel(p->owner->inst->ua, p->pump);
etcp_router_cancel_send_ready(p->owner->inst, p->group, p->peer, ETCP_RT_ID_GROUP_AVATAR, &p->waiter);
ga_release(p, 1); ga_release(p, 0); u_free(p);
}
static void ga_ready(struct ll_queue* queue, void* arg) { (void)queue; ga_schedule(arg); }
static void ga_schedule(struct ga_peer* p) {
if (p->owner->closing || p->pump || p->waiter.internal || p->waiter.call_soon_id) return;
if (!p->state_pending && !p->get_pending && !p->reply_pending && !p->tx) return;
p->pump = uasync_call_soon(p->owner->inst->ua, p, ga_pump);
if (!p->pump) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: send scheduling failed");
}
static void ga_state(uint8_t* bytes, int kind, uint64_t version, uint16_t size, const uint8_t hash[32]) {
bytes[0] = GA_WIRE_VERSION; bytes[1] = (uint8_t)kind; mf_put64(bytes + 2, version);
bytes[10] = (uint8_t)size; bytes[11] = (uint8_t)(size >> 8); memcpy(bytes + 12, hash, 32);
}
static void ga_pump(void* arg) {
struct ga_peer* p = arg; p->pump = NULL; struct group_avatar* ctx = p->owner;
if (!ga_allowed(ctx->inst, p->group, p->peer)) return;
struct ETCP_ROUTER_CONN* router = etcp_router_conn_get(ctx->inst, p->group, p->peer, ETCP_RT_ID_GROUP_AVATAR);
if (!router) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: router allocation failed"); return; }
queue_set_threshold(router->send_q, 0, 0);
for (unsigned step = 0; step < 4; step++) {
if (!p->state_pending && !p->get_pending && !p->reply_pending && !p->tx) break;
if (!etcp_router_send_q_has_room(ctx->inst, p->group, p->peer, ETCP_RT_ID_GROUP_AVATAR)) {
etcp_router_on_send_ready(ctx->inst, p->group, p->peer, ETCP_RT_ID_GROUP_AVATAR, &p->waiter, ga_ready, p); return;
}
uint8_t packet[GA_DATA_HEADER + GROUP_AVATAR_BLOCK]; size_t length = GA_STATE_BYTES; int kind; uint16_t block = 0;
struct group_avatar_record current;
if (p->state_pending) {
if (group_avatar_get(ctx->inst, p->group, &current) < 0) return;
kind = GA_STATE; ga_state(packet, kind, current.version, current.size, current.hash);
} else if (p->get_pending) { kind = GA_GET; ga_state(packet, kind, p->remote_version, 0, p->remote_hash); }
else {
if (!p->tx) {
if (ctx->tx_count >= GA_STREAMS) return;
p->tx = u_malloc(sizeof(*p->tx));
if (!p->tx) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: send snapshot allocation failed"); return; }
ctx->tx_count++; p->tx_offset = 0; p->tx_at = get_time_tb(); p->reply_pending = 0;
if (group_avatar_get(ctx->inst, p->group, p->tx) < 0) { ga_release(p, 1); return; }
if (!p->tx->version) { ga_release(p, 1); p->state_pending = 1; continue; }
}
kind = GA_DATA; block = p->tx->size - p->tx_offset;
if (block > GROUP_AVATAR_BLOCK) block = GROUP_AVATAR_BLOCK;
ga_state(packet, kind, p->tx->version, p->tx->size, p->tx->hash);
memcpy(packet + 44, p->tx->signature, 64);
packet[108] = (uint8_t)p->tx_offset; packet[109] = (uint8_t)(p->tx_offset >> 8);
packet[110] = (uint8_t)block; packet[111] = (uint8_t)(block >> 8);
memcpy(packet + GA_DATA_HEADER, p->tx->data + p->tx_offset, block); length = GA_DATA_HEADER + block;
}
if (etcp_router_conn_send(router, packet, length, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: send failed group=%llu peer=%016llx kind=%d",
(unsigned long long)p->group, (unsigned long long)p->peer, kind); return;
}
if (kind == GA_STATE) { p->state_pending = 0; p->advertised_at = get_time_tb(); }
else if (kind == GA_GET) { p->get_pending = 0; p->requested_at = get_time_tb(); }
else {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: block sent group=%llu peer=%016llx version=%llu offset=%u bytes=%u total=%u",
(unsigned long long)p->group, (unsigned long long)p->peer, (unsigned long long)p->tx->version,
p->tx_offset, block, p->tx->size);
p->tx_offset += block; p->tx_at = get_time_tb();
if (p->tx_offset == p->tx->size) ga_release(p, 1);
}
}
ga_schedule(p);
}
static int ga_receive_ready(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, const uint8_t* data, size_t size) {
struct group_avatar* ctx = ga_context(inst);
if (!ctx || size < GA_DATA_HEADER || data[0] != GA_WIRE_VERSION || data[1] != GA_DATA || !ga_allowed(inst, group, peer)) return 1;
struct ga_peer* p = ga_find(ctx, group, peer, 0);
return (p && p->rx) || ctx->rx_count < GA_STREAMS;
}
static void ga_receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
struct group_avatar* ctx = conn ? ga_context(conn->instance) : NULL;
if (!ctx || ctx->closing || !entry || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF) goto out;
uint64_t group, peer; memcpy(&group, entry->dgram + ROUTER_SVC_GROUP_OFF, 8); memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
struct ga_peer* p = ga_find(ctx, group, peer, 0);
if (entry->len == ROUTER_SVC_PAYLOAD_OFF) {
if (p) { ga_release(p, 1); ga_release(p, 0); p->requested_at = 0; p->state_pending = 1; ga_schedule(p); }
goto out;
}
if ((entry->dgram[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) !=
(ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED) || !ga_allowed(ctx->inst, group, peer)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: unauthorized packet group=%llu peer=%016llx",
(unsigned long long)group, (unsigned long long)peer); goto out;
}
const uint8_t* bytes = entry->dgram + ROUTER_SVC_PAYLOAD_OFF; size_t length = entry->len - ROUTER_SVC_PAYLOAD_OFF;
if (length < GA_STATE_BYTES || bytes[0] != GA_WIRE_VERSION || bytes[1] < GA_STATE || bytes[1] > GA_DATA) goto malformed;
uint64_t version = mf_get64(bytes + 2); uint16_t size = bytes[10] | ((uint16_t)bytes[11] << 8);
if (version > INT64_MAX || size > GROUP_AVATAR_MAX) goto malformed;
if (!p) p = ga_find(ctx, group, peer, 1);
if (!p) goto out;
if (bytes[1] != GA_DATA) {
if (length != GA_STATE_BYTES) goto malformed;
struct group_avatar_record local; if (group_avatar_get(ctx->inst, group, &local) < 0) goto out;
if (bytes[1] == GA_GET) {
if (!p->tx) p->reply_pending = 1;
} else {
p->remote_version = version; memcpy(p->remote_hash, bytes + 12, 32);
int newer = ga_compare(version, p->remote_hash, local.version, local.hash);
if (newer > 0 && (!p->requested_at || get_time_tb() - p->requested_at >= GA_RETRY_TB)) p->get_pending = 1;
if (newer < 0) p->state_pending = 1;
if (!newer) p->requested_at = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: state group=%llu peer=%016llx remote=%llu local=%llu request=%d",
(unsigned long long)group, (unsigned long long)peer, (unsigned long long)version,
(unsigned long long)local.version, p->get_pending);
}
ga_schedule(p); goto out;
}
if (length < GA_DATA_HEADER || !version) goto malformed;
uint16_t offset = bytes[108] | ((uint16_t)bytes[109] << 8), block = bytes[110] | ((uint16_t)bytes[111] << 8);
if (offset > size || block > GROUP_AVATAR_BLOCK || block > size - offset || length != GA_DATA_HEADER + block ||
(size && !block) || (offset + block < size && block != GROUP_AVATAR_BLOCK)) goto malformed;
if (!offset) {
ga_release(p, 0);
if (ctx->rx_count >= GA_STREAMS) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: receive limit reached"); goto out; }
p->rx = u_calloc(1, sizeof(*p->rx));
if (!p->rx) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: receive allocation failed"); goto out; }
ctx->rx_count++; p->rx_offset = 0; p->rx->version = version; p->rx->size = size;
memcpy(p->rx->hash, bytes + 12, 32); memcpy(p->rx->signature, bytes + 44, 64);
}
if (!p->rx || p->rx_offset != offset || p->rx->version != version || p->rx->size != size ||
memcmp(p->rx->hash, bytes + 12, 32) || memcmp(p->rx->signature, bytes + 44, 64)) goto malformed;
memcpy(p->rx->data + offset, bytes + GA_DATA_HEADER, block); p->rx_offset += block; p->rx_at = get_time_tb();
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: block received group=%llu peer=%016llx version=%llu offset=%u bytes=%u total=%u",
(unsigned long long)group, (unsigned long long)peer, (unsigned long long)version, offset, block, size);
if (p->rx_offset == size) {
int applied = group_avatar_apply(ctx->inst, group, p->rx); ga_release(p, 0);
p->requested_at = applied < 0 ? get_time_tb() : 0;
p->state_pending = 1; ga_schedule(p);
}
goto out;
malformed:
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: malformed/mixed blocks group=%llu peer=%016llx bytes=%zu",
(unsigned long long)group, (unsigned long long)peer, length);
if (p) { ga_release(p, 0); p->requested_at = get_time_tb(); }
out:
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
}
static void ga_tick(void* arg) {
struct group_avatar* ctx = arg; ctx->timer = NULL; uint64_t now = get_time_tb();
for (struct ga_peer* p = ctx->peers; p; p = p->next) p->seen = 0;
for (struct ll_entry* ge = ctx->inst->topo_groups->group_list->head; ge; ge = ge->next) {
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge;
if (group->group_type != TOPO_GROUP_TYPE_CHAT || !group->senders_list) continue;
for (struct ll_entry* e = group->senders_list->head; e; e = e->next) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data;
if (!item || !item->conn || !item->ready_notified || item->transport_down || !ga_allowed(ctx->inst, group->group_id, item->node_id)) continue;
struct ga_peer* p = ga_find(ctx, group->group_id, item->node_id, 1); if (!p) continue;
p->seen = 1;
if (p->epoch != item->local_epoch) {
ga_release(p, 1); ga_release(p, 0); p->epoch = item->local_epoch; p->requested_at = 0; p->state_pending = 1;
}
if (now - p->advertised_at >= GA_EXCHANGE_TB) p->state_pending = 1;
if (p->requested_at && now - p->requested_at >= GA_RETRY_TB) { p->get_pending = 1; p->requested_at = 0; }
if ((p->rx && now - p->rx_at >= GA_RETRY_TB) || (p->tx && now - p->tx_at >= GA_RETRY_TB)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: incomplete transfer expired group=%llu peer=%016llx",
(unsigned long long)p->group, (unsigned long long)p->peer); ga_release(p, 1); ga_release(p, 0); p->state_pending = 1;
}
ga_schedule(p);
}
}
struct ga_peer** link = &ctx->peers;
while (*link) { struct ga_peer* p = *link; if (p->seen) link = &p->next; else { *link = p->next; ga_free(p); } }
ctx->timer = uasync_set_timeout(ctx->inst->ua, 10000, ctx, ga_tick, "group_avatar");
if (!ctx->timer) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: timer allocation failed");
}
static int ga_schema(sqlite3* db) {
const char* names[] = {"avatar_version", "avatar_data", "avatar_hash", "avatar_signature"}; int present[4] = {0};
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, "PRAGMA table_info(channels)", -1, &st, NULL) != SQLITE_OK) goto error;
while (sqlite3_step(st) == SQLITE_ROW) for (unsigned i = 0; i < 4; i++)
if (!strcmp((const char*)sqlite3_column_text(st, 1), names[i])) present[i] = 1;
sqlite3_finalize(st);
if (sqlite3_exec(db, "SAVEPOINT group_avatar_schema", NULL, NULL, NULL) != SQLITE_OK) goto error;
for (unsigned i = 0; i < 4; i++) if (!present[i]) {
char sql[128]; snprintf(sql, sizeof(sql), "ALTER TABLE channels ADD COLUMN %s %s", names[i], i ? "BLOB" : "INTEGER NOT NULL DEFAULT 0");
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK TO group_avatar_schema; RELEASE group_avatar_schema", NULL, NULL, NULL); goto error;
}
}
if (sqlite3_exec(db, "RELEASE group_avatar_schema", NULL, NULL, NULL) == SQLITE_OK) return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: schema initialization failed: %s", sqlite3_errmsg(db)); return -1;
}
int group_avatar_init(struct UTUN_INSTANCE* inst) {
if (!inst || !CC(inst) || !CC(inst)->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: core unavailable"); return -1; }
if (ga_context(inst)) return 0;
if (ga_schema(CC(inst)->db) < 0) return -1;
struct group_avatar* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: context allocation failed"); return -1; }
ctx->inst = inst; CC(inst)->group_avatar = ctx;
if (etcp_router_bind(inst, ETCP_RT_ID_GROUP_AVATAR, ga_receive)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: router registration failed"); CC(inst)->group_avatar = NULL; u_free(ctx); return -1;
}
etcp_router_set_receive_ready(inst, ETCP_RT_ID_GROUP_AVATAR, ga_receive_ready);
ctx->timer = uasync_set_timeout(inst->ua, 10000, ctx, ga_tick, "group_avatar");
if (!ctx->timer) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: initial timer failed"); group_avatar_destroy(inst); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: initialized max_bytes=%d block_bytes=%d", GROUP_AVATAR_MAX, GROUP_AVATAR_BLOCK); return 0;
}
void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group) {
struct group_avatar* ctx = ga_context(inst); if (!ctx) return;
struct ga_peer** link = &ctx->peers;
while (*link) { struct ga_peer* p = *link; if (p->group != group) link = &p->next; else { *link = p->next; ga_free(p); } }
}
void group_avatar_destroy(struct UTUN_INSTANCE* inst) {
struct group_avatar* ctx = ga_context(inst); if (!ctx) return;
ctx->closing = 1; if (ctx->timer) uasync_cancel_timeout(inst->ua, ctx->timer);
etcp_router_set_receive_ready(inst, ETCP_RT_ID_GROUP_AVATAR, NULL);
etcp_router_unbind(inst, ETCP_RT_ID_GROUP_AVATAR);
while (ctx->peers) { struct ga_peer* p = ctx->peers; ctx->peers = p->next; ga_free(p); }
CC(inst)->group_avatar = NULL; u_free(ctx); DEBUG_INFO(DEBUG_CATEGORY_CHAT, "group avatar: stopped");
}