diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 09e8d54a..c258b76c 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -69,6 +69,7 @@ extern "C" { #define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], 0=start 1=ready 2=failed */ #define CHAT_EVT_DM_CALL_ROUTE 47 /* [conv_id_len:1][conv_id:var][peer_node_id:8][group_id:8], 0 = нет BGP-пути */ +#define CHAT_EVT_DM_INVITE_RESULT 48 /* [request_id:8][result:4][peer_node_id:8][conv_id:8], dm_invite_result */ typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len); diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c index 057ff5fc..8b61cff7 100644 --- a/src/dm/dm_core.c +++ b/src/dm/dm_core.c @@ -14,6 +14,8 @@ #include "dm_mailbox.h" #include "../chat/chat_core.h" #include "../chat/chat_event.h" +#include "../chat/invite_build.h" +#include "../chat/chat_join.h" #include "../utun_instance.h" #include "../ntp_time.h" #include "../routing_layer/etcp_router.h" @@ -45,6 +47,7 @@ struct dm_state { sqlite3* db; int initialized; void* timer; + struct dm_invite_req* invite; }; /* ── вспомогательные ── */ @@ -833,6 +836,7 @@ fail: void dm_core_destroy(struct UTUN_INSTANCE* inst) { struct dm_state* dm = dm_of(inst); if (!dm) return; + if (dm->invite) chat_invite_build_cancel(dm->invite->pending); if (dm->timer) uasync_cancel_timeout(inst->ua, dm->timer); dm->timer = NULL; dm->initialized = 0; @@ -1032,6 +1036,93 @@ void dm_send_trampoline(void* arg) { DM_ID, conv); } +/* Завершить операцию и вернуть результат с неизменяемым адресатом. */ +static void dm_invite_done(struct dm_invite_req* req, int result) { + struct dm_state* dm = dm_of(req->inst); + if (dm && dm->invite == req) dm->invite = NULL; + uint64_t conv = dm_derive_conv_id(req->inst->node_id, req->peer_node_id); + int32_t status = result; + uint8_t event[28]; + memcpy(event, &req->request_id, 8); memcpy(event + 8, &status, 4); + memcpy(event + 12, &req->peer_node_id, 8); memcpy(event + 20, &conv, 8); + DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: finished request=%llu peer=%llu ch=%llu result=%d", + (unsigned long long)req->request_id, (unsigned long long)req->peer_node_id, + (unsigned long long)req->channel_id, result); + chat_event_post(req->inst, CHAT_EVT_DM_INVITE_RESULT, event, sizeof(event)); + u_free(req); +} + +/* ACK регистрации разрешает отправку ссылки; ошибка/отмена не создаёт сообщение. */ +static void dm_invite_ready(void* arg, int result, const char* link) { + struct dm_invite_req* req = arg; + req->pending = NULL; + if (result == CHAT_JOIN_CANCELLED) { dm_invite_done(req, DM_INVITE_CANCELLED); return; } + if (result != CHAT_JOIN_OK || !link || !link[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: registration failed request=%llu ch=%llu result=%d", + (unsigned long long)req->request_id, (unsigned long long)req->channel_id, result); + dm_invite_done(req, DM_INVITE_BUILD_ERROR); return; + } + char conv[64]; + snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(req->inst->node_id, req->peer_node_id)); + size_t bytes = strlen(link); + DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: registered request=%llu peer=%llu ch=%llu bytes=%zu ttl=%ds", + (unsigned long long)req->request_id, (unsigned long long)req->peer_node_id, + (unsigned long long)req->channel_id, bytes, JOIN_KEY_TTL_SECONDS); + int rc = dm_send(req->inst, conv, "text", (const uint8_t*)link, (uint32_t)bytes); + if (rc) DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: send failed request=%llu conv=%s rc=%d", + (unsigned long long)req->request_id, conv, rc); + dm_invite_done(req, rc ? DM_INVITE_SEND_ERROR : DM_INVITE_OK); +} + +/* Создать/найти PM, затем зарегистрировать свежий invite; все данные принадлежат запросу. */ +void dm_invite_trampoline(void* arg) { + struct dm_invite_req* req = arg; + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: missing request"); return; } + struct dm_state* dm = dm_of(req->inst); + if (req->cancel) { + if (dm && dm->invite && dm->invite->request_id == req->request_id) { + DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: cancelling request=%llu", (unsigned long long)req->request_id); + chat_invite_build_cancel(dm->invite->pending); + } else DEBUG_DEBUG(DEBUG_CATEGORY_DM, "invite PM: cancel already completed request=%llu", + (unsigned long long)req->request_id); + u_free(req); return; + } + if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: missing instance"); u_free(req); return; } + if (!dm || !dm->initialized || !req->request_id || !req->peer_node_id || req->peer_node_id == req->inst->node_id) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: invalid state/recipient request=%llu peer=%llu", + (unsigned long long)req->request_id, (unsigned long long)req->peer_node_id); + dm_invite_done(req, DM_INVITE_START_ERROR); return; + } + if (dm->invite) { + DEBUG_WARN(DEBUG_CATEGORY_DM, "invite PM: busy request=%llu pending=%llu", + (unsigned long long)req->request_id, (unsigned long long)dm->invite->request_id); + dm_invite_done(req, DM_INVITE_BUSY); return; + } + char channel[64], conv[64]; + snprintf(channel, sizeof(channel), "%llu", (unsigned long long)req->channel_id); + struct TOPO_GROUP* group = topo_groups_find(req->inst->topo_groups, req->channel_id); + if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || + !topo_node_sqlite_member_in_channel(dm->db, channel, req->inst->node_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: no active membership ch=%s", channel); + dm_invite_done(req, DM_INVITE_BUILD_ERROR); return; + } + snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(req->inst->node_id, req->peer_node_id)); + struct dm_conv c; + if (dm_conv_load(dm, conv, &c) != 0) { + uint8_t x25519[32], ed25519[32]; + if (dm_node_pubkeys(dm, req->peer_node_id, x25519, ed25519) != 0 || + dm_start(req->inst, req->peer_node_id, x25519, ed25519, req->peer_name, req->source_ch_id) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite PM: conversation failed peer=%llu", (unsigned long long)req->peer_node_id); + dm_invite_done(req, DM_INVITE_START_ERROR); return; + } + } + DEBUG_INFO(DEBUG_CATEGORY_DM, "invite PM: building request=%llu peer=%llu source=%s ch=%s conv=%s", + (unsigned long long)req->request_id, (unsigned long long)req->peer_node_id, req->source_ch_id, channel, conv); + dm->invite = req; + req->pending = chat_invite_build_link(req->inst, req->channel_id, 0, NULL, dm_invite_ready, req); + if (!req->pending) dm_invite_ready(req, CHAT_JOIN_ERROR, NULL); +} + /* Единая BGP-проверка для кнопки PM и запуска звонка; только uasync-поток. */ int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id) { struct dm_state* dm = dm_of(inst); diff --git a/src/dm/dm_core.h b/src/dm/dm_core.h index 61fc2398..33108e79 100644 --- a/src/dm/dm_core.h +++ b/src/dm/dm_core.h @@ -114,6 +114,25 @@ struct dm_send_req { }; void dm_send_trampoline(void* arg); +/* Приглашение в выбранный канал через PM: свежая ссылка после ACK регистрации, + * затем обычный dm_send("text"). Получатель фиксирован независимо от текущего UI. + * Один ожидающий запрос на instance; результат CHAT_EVT_DM_INVITE_RESULT. + * cancel=1 отменяет только ожидающий request_id; отправленное PM не отзывается. + * arg=u_calloc(dm_invite_req); обработчик/финальный callback освобождает arg. */ +enum dm_invite_result { + DM_INVITE_OK = 0, DM_INVITE_START_ERROR = -1, DM_INVITE_BUILD_ERROR = -2, + DM_INVITE_SEND_ERROR = -3, DM_INVITE_CANCELLED = -4, DM_INVITE_BUSY = -5 +}; +struct dm_invite_req { + struct UTUN_INSTANCE* inst; + uint64_t request_id, peer_node_id, channel_id; + char source_ch_id[64]; /* общая группа для новой PM; из шапки PM может быть пустой */ + char peer_name[128]; + int cancel; + struct chat_invite_build* pending; /* только uasync */ +}; +void dm_invite_trampoline(void* arg); + /* Запросить последние 200 сообщений со статусами. CHAT_EVT_DM_MESSAGES: * [conv_id_len:1][conv_id:var][JSON:utf8]. Все чтения в uasync. */ struct dm_messages_req {