352 lines
12 KiB
Rust
Raw Normal View History

/// GroupV1 is a conversationType which provides effecient handling of multiple participants
/// Properties:
/// - Harvest Now Decrypt Later (HNDL) protection provided by XWING
/// - Multiple
use blake2::{Blake2b, Digest, digest::consts::U6};
use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload};
use chat_proto::logoschat::reliability::ReliablePayload;
use openmls::prelude::tls_codec::Deserialize;
use openmls::prelude::*;
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
use prost::Message as _;
use shared_traits::IdentIdRef;
use crate::account_directory::{AccountDirectory, resolve_device_ids};
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
use crate::inbox_v2::MlsProvider;
use crate::service_context::{ExternalServices, ServiceContext};
use crate::{
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
DeliveryService, IdentityProvider,
conversation::{ChatError, Convo, GroupConvo},
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
outcomes::{Content, ConvoOutcome},
service_traits::KeyPackageProvider,
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
types::AddressedEncryptedPayload,
};
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
pub struct GroupV1Convo {
mls_group: MlsGroup,
convo_id: String,
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
impl std::fmt::Debug for GroupV1Convo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GroupV1Convo")
.field("convo_id", &self.convo_id)
.field("mls_epoch", &self.mls_group.epoch())
.finish_non_exhaustive()
}
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
impl GroupV1Convo {
// Create a new conversation with the creator as the only participant.
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
pub fn new<S: ExternalServices>(cx: &mut ServiceContext<S>) -> Result<Self, ChatError> {
let config = Self::mls_create_config(cx);
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
let mls_group = MlsGroup::new(
&cx.mls_provider,
&cx.mls_identity,
&config,
cx.mls_identity.get_credential(),
)
.unwrap();
let convo_id = hex::encode(mls_group.group_id().as_slice());
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
Self::subscribe(&mut cx.ds, &convo_id)?;
Ok(Self {
mls_group,
convo_id,
})
}
// Constructs a new conversation upon receiving a MlsWelcome message.
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
pub fn new_from_welcome<S: ExternalServices>(
cx: &mut ServiceContext<S>,
welcome: Welcome,
) -> Result<Self, ChatError> {
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
let mls_group =
StagedWelcome::build_from_welcome(&cx.mls_provider, &Self::mls_join_config(), welcome)
.unwrap()
.build()
.unwrap()
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
.into_group(&cx.mls_provider)
.unwrap();
let convo_id = hex::encode(mls_group.group_id().as_slice());
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
Self::subscribe(&mut cx.ds, &convo_id)?;
Ok(Self {
mls_group,
convo_id,
})
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
pub fn load<S: ExternalServices>(
cx: &mut ServiceContext<S>,
convo_id: String,
group_id: GroupId,
) -> Result<Self, ChatError> {
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
let mls_group = MlsGroup::load(cx.mls_provider.storage(), &group_id)
.map_err(ChatError::generic)?
.ok_or_else(|| ChatError::NoConvo("mls group not found".into()))?;
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
Self::subscribe(&mut cx.ds, &convo_id)?;
Ok(GroupV1Convo {
mls_group,
convo_id,
})
}
// Configure the delivery service to listen for the required delivery addresses.
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
fn subscribe(ds: &mut impl DeliveryService, convo_id: &str) -> Result<(), ChatError> {
ds.subscribe(&Self::delivery_address_from_id(convo_id))
.map_err(ChatError::generic)?;
ds.subscribe(&Self::ctrl_delivery_address_from_id(convo_id))
.map_err(ChatError::generic)?;
Ok(())
}
fn mls_create_config<S: ExternalServices>(cx: &mut ServiceContext<S>) -> MlsGroupCreateConfig {
MlsGroupCreateConfig::builder()
.ciphersuite(cx.mls_provider.crypto().supported_ciphersuites()[0])
.use_ratchet_tree_extension(true) // This is handy for now, until there is central store for this data
.build()
}
fn mls_join_config() -> MlsGroupJoinConfig {
MlsGroupJoinConfig::builder().build()
}
fn delivery_address_from_id(convo_id: &str) -> String {
let hash = Blake2b::<U6>::new()
.chain_update("delivery_addr|")
.chain_update(convo_id)
.finalize();
hex::encode(hash)
}
fn delivery_address(&self) -> String {
Self::delivery_address_from_id(&self.convo_id)
}
fn ctrl_delivery_address_from_id(convo_id: &str) -> String {
let hash = Blake2b::<U6>::new()
.chain_update("ctrl_delivery_addr|")
.chain_update(convo_id)
.finalize();
hex::encode(hash)
}
fn ctrl_delivery_address(&self) -> String {
Self::ctrl_delivery_address_from_id(&self.convo_id)
}
/// Resolve an account to a KeyPackage for *every* device it authorizes.
///
/// First resolves the account to its device ids through the account
/// directory ([`resolve_device_ids`]), then fetches each device's
/// KeyPackage. When the account never published a bundle, resolution falls
/// back to a single device id equal to the account id — the pre-directory
/// behaviour — so single-device accounts are unaffected.
fn key_packages_for_account(
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
&self,
ident: IdentIdRef,
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
provider: &impl MlsProvider,
registry: &(impl KeyPackageProvider + AccountDirectory),
) -> Result<Vec<KeyPackage>, ChatError> {
let device_ids =
resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?;
let mut keypackages = Vec::with_capacity(device_ids.len());
for device_id in &device_ids {
let retrieved = registry
.retrieve(device_id)
.map_err(|e| ChatError::Generic(e.to_string()))?;
let Some(keypkg_bytes) = retrieved else {
return Err(ChatError::Protocol(format!(
"no keypackage for device {device_id} of account {ident}"
)));
};
let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?;
let keypkg = key_package_in.validate(provider.crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version
keypackages.push(keypkg);
}
Ok(keypackages)
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
pub fn id(&self) -> &str {
&self.convo_id
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
fn send_message<S: ExternalServices>(
&mut self,
content: &[u8],
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
cx: &ServiceContext<S>,
) -> Result<Vec<AddressedEncryptedPayload>, ChatError> {
let sender_id = cx.mls_identity.id().as_str();
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
let reliable = cx.causal.on_send(&self.convo_id, sender_id, content);
let wire = reliable.encode_to_vec();
let mls_message_out = self
.mls_group
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
.create_message(&cx.mls_provider, &cx.mls_identity, &wire)
.unwrap();
let a = AddressedEncryptedPayload {
delivery_address: self.delivery_address(),
data: EncryptedPayload {
encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext {
payload: mls_message_out.to_bytes().unwrap().into(),
})),
},
};
Ok(vec![a])
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
}
impl<S: ExternalServices> Convo<S> for GroupV1Convo {
fn send_content(
&mut self,
cx: &mut ServiceContext<S>,
content: &[u8],
) -> Result<(), ChatError> {
let payloads = self.send_message(content, cx)?;
for payload in payloads {
cx.ds
.publish(payload.into_envelope(self.id().into()))
.map_err(|e| ChatError::Delivery(e.to_string()))?;
}
Ok(())
}
fn handle_frame(
&mut self,
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
cx: &mut ServiceContext<S>,
encoded_payload: EncryptedPayload,
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
) -> Result<ConvoOutcome, ChatError> {
let bytes = match encoded_payload.encryption {
Some(encrypted_payload::Encryption::Plaintext(pt)) => pt.payload,
_ => {
return Err(ChatError::ProtocolExpectation(
"None",
"Some(Encryption::Plaintext)".into(),
));
}
};
let mls_message =
MlsMessageIn::tls_deserialize_exact_bytes(&bytes).map_err(ChatError::generic)?;
let protocol_message: ProtocolMessage = mls_message
.try_into_protocol_message()
.map_err(ChatError::generic)?;
if protocol_message.epoch() < self.mls_group.epoch() {
// TODO: (P1) Add logging for messages arriving from past epoch.
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
return Ok(ConvoOutcome::empty(self.id().to_string()));
}
let processed = self
.mls_group
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
.process_message(&cx.mls_provider, protocol_message)
.map_err(ChatError::generic)?;
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
let content = match processed.into_content() {
ProcessedMessageContent::ApplicationMessage(msg) => {
let reliable = ReliablePayload::decode(msg.into_bytes().as_slice())?;
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
cx.causal.on_receive(&self.convo_id, &reliable);
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
Some(Content {
bytes: reliable.content.to_vec(),
})
}
ProcessedMessageContent::StagedCommitMessage(commit) => {
self.mls_group
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
.merge_staged_commit(&cx.mls_provider, *commit)
.map_err(ChatError::generic)?;
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
None
}
_ => {
// TODO: (P2) Log unknown message type
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
None
}
feat: introduce client event system (#106) * chore(flake): accept extra system attr; add perl for openssl-sys build forAllSystems calls the lambda with {system, pkgs}; strict destructuring requires `..` to ignore the system attribute. `pkgs.perl` is needed because openssl-sys is pulled vendored via libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure` step needs FindBin.pm, which Fedora's system perl doesn't ship. * feat: introduce client event system - Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or `Inbox`. `ConvoOutcome` carries a conversation id and an optional decrypted `Content`; `InboxOutcome` adds a `NewConversation` (id + `ConversationClass`) for a peer-initiated conversation. - Client translates `PayloadOutcome` into app-facing `Vec<Event>` (`ConversationStarted`, `MessageReceived`) at the boundary, so the application loop sees discrete events rather than core types. - MLS group welcomes produce a `ConversationStarted` event with no initial content, fixing the silent-group-join case where the inbox layer dropped the observation. - C FFI exposes an `EventList` opaque type with indexed accessors and an `Invalid` sentinel for out-of-bounds / non-applicable reads. - Symmetric `Inbox` / `InboxV2` handlers: both return `Result<InboxOutcome, _>` and own the persistence + ephemeral-key cleanup for the conversations they create. - Updated and simplified `docs/adr/0001-client-event-system.md`. * chore(flake): bump nixpkgs to nixos-unstable-small Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for importCargoLock's curl UA) haven't propagated to nixos-unstable yet. Switch to nixos-unstable-small and force logos-delivery to follow so the smoketest gets the same fix. Revert once nixos-unstable catches up. Refs: - https://github.com/rust-lang/crates.io/issues/13482 - https://github.com/rust-lang/crates.io/issues/13783 - https://crates.io/data-access
2026-05-28 23:51:15 +02:00
};
Ok(ConvoOutcome {
convo_id: self.id().to_string(),
content,
})
}
fn wakeup(&mut self, _: &mut ServiceContext<S>) -> Result<(), ChatError> {
Ok(())
}
}
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
impl<S: ExternalServices> GroupConvo<S> for GroupV1Convo {
// add_members returns:
// commit — the Commit message Alice broadcasts to all members
// welcome — the Welcome message sent privately to each new joiner
// _group_info — used for external joins; ignore for now
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
fn add_member(
&mut self,
cx: &mut ServiceContext<S>,
members: &[IdentIdRef],
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
) -> Result<(), ChatError> {
if members.len() > 50 {
// This is a temporary limit that originates from the the De-MLS epoch time.
return Err(ChatError::Protocol(
"Cannot add more than 50 Members at a time".into(),
));
}
// Resolve each account to a KeyPackage per authorized device and flatten
// them into one list — every device of every invitee becomes an MLS
// leaf, so all of a user's installations join the group.
let mut keypkgs = Vec::with_capacity(members.len());
for ident in members {
keypkgs.extend(self.key_packages_for_account(ident, &cx.mls_provider, &cx.registry)?);
}
let (commit, welcome, _group_info) = self
.mls_group
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
.add_members(
&cx.mls_provider,
&cx.mls_identity,
keypkgs.iter().as_slice(),
)
.unwrap();
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
self.mls_group
.merge_pending_commit(&cx.mls_provider)
.unwrap();
// TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users
for account_id in members {
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
cx.mls_provider
.invite_user(&mut cx.ds, account_id, &welcome)?;
}
let encrypted_payload = EncryptedPayload {
encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext {
payload: commit.to_bytes()?.into(),
})),
};
let addr_enc_payload = AddressedEncryptedPayload {
delivery_address: self.ctrl_delivery_address(),
data: encrypted_payload,
};
// Prepare commit message
// TODO: (P1) Make GroupConvos agnostic to framing so its less error prone and more
let env = addr_enc_payload.into_envelope(self.convo_id.clone());
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123) Make the conversations core Send so the threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd and renamed to Core, owning its services outright and driving the inbox and conversation primitives with plain &mut self. - Services (identity, delivery, store, registry, MLS context, causal history) are bundled into a ServiceContext<S> behind an ExternalServices trait, with S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot be inferred backwards through S::DS. - Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and receive the ServiceContext bundle as a &mut/& parameter; no Rc or RefCell-as-shared-state remains, so Core is Send whenever its injected services are. - Dispatch branches on ConversationKind in one place: Core rebuilds the target as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so conversations never escape the orchestrator. - CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
cx.ds
.publish(env)
.map_err(|e| ChatError::Generic(format!("Publish: {e}")))
}
fn id(&self) -> super::ConversationIdRef<'_> {
&self.convo_id
}
}