mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-03 21:53:31 +00:00
* feat: expose GroupV2 through the threaded client GroupV2 (de-mls) conversations were reachable only from Core, and every conversation ran the hardcoded millisecond timer profile in group_v2.rs (20-150 ms freeze/consensus windows), which cannot survive real network latency. Make them reachable through ChatClient (as DirectV1 already is), with timing that holds up over a real network. - ChatClient::create_group_conversation(accounts) and add_group_members(convo_id, accounts): resolve each account address to its endorsed signer ids through the client-held directory, the same resolution create_direct_conversation uses, and drive Core::create_group_convo / group_add_member. - GroupV2 timing/policy is injectable: ServiceContext carries a de_mls::ConversationConfig (re-exported as GroupV2Config) defaulting to the de-mls library defaults; Core::set_group_v2_config and the builder's group_v2_config setter override it. The creator's phase durations reach joiners inside the welcome's ConversationSync, so the group runs the creator's phase timing. - GroupV2Convo::add_member validates every member's key package before proposing any add, skips members de-mls would silently not propose (self, already in the group) instead of stranding a pending invite, and flushes opened proposals even on a mid-batch failure, so a failed batch cannot invite members behind the caller's back. - The millisecond test profile moves into the test harnesses (integration_tests_core's TestHarness, crates/client/tests/group_v2.rs). - New client-level tests: three accounts on the in-process transport create a group, a non-creator adds the third member, and messages fan out with directory-verified senders; a batch containing a member with no key package fails without inviting anyone. * fix: class inbound DirectV1 joins as Private, not Group The joiner of a DirectV1 (pairwise) conversation received it classed as Group, because dispatch_to_inbox2 hardcoded ConversationClass::Group for every InboxV2 join. DirectV1 welcomes (InviteType::GroupV1) and GroupV2 welcomes (InviteType::GroupV2) both arrive over InboxV2, so a plain 1:1 invite surfaced to the display layer as a group. ConversationClass is documented as stable across protocol versions of the same conversation shape, and DirectV1 is the pairwise shape, so its joiner must see Private. - InboxV2::handle_frame returns the class alongside the convo: InviteType::GroupV1 (the DirectV1 welcome carrier) yields Private, InviteType::GroupV2 yields Group. - dispatch_to_inbox2 propagates that class instead of hardcoding Group. - direct_v1_by_account_address asserts the joiner sees Private. * feat: expose a group's roster, deduped to one entry per account The display layer needs a group's membership, but nothing exposed it: de-mls holds the authoritative roster (MLS group state) with no public accessor, and members added by other members stay invisible until they send a message. Rebuilding the roster from observed messages would fork state the crypto layer owns and be wrong exactly when a group grows. - GroupConvo::members() returns each member's hex-encoded MLS leaf-credential content, self included. GroupV2Convo delegates to de-mls and guarantees self-inclusion; GroupV1Convo reads its openmls leaves. - Core::group_members(convo_id) mirrors group_add_member's dispatch: a cached group yields its members, a direct conversation is an UnsupportedFunction, otherwise the group is loaded. - ChatClient::group_members returns Vec<GroupMember>, resolving each member's account claim through the directory. A member whose account claim is unconfirmable is listed by device with account None rather than dropped: it is cryptographically in the group, only the account claim is unproven. The credential parsing decode_sender did is factored into parse_credential and shared by both, leaving decode_sender's stricter drop semantics for message senders unchanged. - Because resolve_device_ids fans an account out to every endorsed device, an account whose devices all join surfaced once per device; group_members dedups by account, keeping the first-seen device as the account's representative. Members with no confirmed account stay individual, keyed by their unique device key. - Unit tests cover the tolerant-vs-drop split and the per-account dedup; the three-member group integration test asserts the roster converges after create and after each add, and a solo group lists only its creator. * feat: retry the registry on transient 5xx with backoff and jitter The keypackage/account registry is reliable request-by-request but sheds concurrent bursts with a 5xx, so several instances registering at once each hard-failed on init. HttpRegistry's four calls now retry network errors and 5xx/429 with exponential backoff and full jitter (the jitter decorrelates concurrent publishers so their retries don't re-collide); 4xx and success return immediately. The total retry window is bounded to a few seconds. * fix: mark InboxV2 key package last-resort so members can join multiple groups A key package's init key is one-time-use: openmls deletes it after the first welcome that consumes it. Each installation registers a single key package, so a second group inviting the same member found no matching key package and rejected the welcome with "welcome not addressed to this member", the flaky group add. Mark the InboxV2 key package as last-resort (and advertise the extension in the leaf capabilities, which key-package validation requires) so openmls retains the init key, letting one key package admit an installation to any number of groups. This reuses one init key for every join, trading per-join forward secrecy for membership that just works. A TODO at the publish site tracks the intended one-time key-package pool (the registry pops one per fetch, the client replenishes) with last-resort as the exhaustion fallback (#169). Add regression tests: a member joining two groups (core harness) and two peers invited to several groups over the threaded client. * fix: dedup list_conversations across the store and the in-memory cache A DirectV1 join persists its conversation to the store and also caches it in memory, so list_conversations saw it twice. It deduped with Vec::dedup, which only drops consecutive repeats, over cached_convos' nondeterministic HashMap order, so the duplicate survived whenever another cached conversation fell between the two copies. list_conversations then intermittently returned a conversation twice, and a consumer counting conversations (e.g. checking that a peer joined a group while a direct chat already existed) saw a flaky count. Dedup through a set so a conversation held in both stores is listed once regardless of iteration order. Add a DirectV1-then-GroupV2 regression test, which also covers key-package reuse across conversation types. * fix: dedup the GroupV2 add batch to avoid redundant fetches and duplicate invites Both create_group_convo_v2 and group_add_member funnel through GroupV2Convo::add_member, so a duplicate signer (an account that resolves to the same signer twice, or a repeated account) cost a redundant key-package fetch and a second Add proposal. The existing guard skipped only self and already-committed members, which a within-batch duplicate escapes because add_member opens a proposal the committed roster does not yet reflect, stranding a pending_invite that can later fire a spurious duplicate welcome. Dedup the requested signers before fetching, and guard the add loop with a membership set seeded from the roster and self, hoisting the per-iteration members() call out of the loop. * docs: correct the retry-budget and group-add doc comments The retry-budget comment claimed the ~20s init IPC budget held even at the worst-case sum, but that only holds on the load-shed path where each retry returns fast; a fully unreachable registry costs up to MAX_RETRIES times the reqwest timeout, which no retry budget can rescue. State both. Reword add_group_members to name the proposal, commit, and welcome flow rather than the unexplained "once the add commits". * chore: allow clippy::question_mark in LocalBroadcaster::poll (Rust 1.97 FP) Stable rolled to 1.97, whose clippy question_mark flags poll()'s match on `self.shared.borrow().read(next)`. Its suggested `read(next)?` would drop the RefCell Ref guard and dangle the returned reference, so the lint is a false positive here. CI tracks floating stable (`rustup update stable`), so this is pre-existing code newly flagged; suppress it to keep the branch green.