2026-06-02 15:21:21 -07:00
|
|
|
mod identity;
|
|
|
|
|
mod mls_provider;
|
|
|
|
|
|
2026-05-19 11:54:54 -07:00
|
|
|
use chat_proto::logoschat::envelope::EnvelopeV1;
|
2026-06-15 13:15:18 -07:00
|
|
|
use de_mls::protos::de_mls::messages::v1::MemberWelcome;
|
2026-05-19 11:54:54 -07:00
|
|
|
use openmls::prelude::tls_codec::Serialize;
|
|
|
|
|
use openmls::prelude::*;
|
|
|
|
|
use prost::{Message, Oneof};
|
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 storage::{ConversationKind, ConversationMeta, ConversationStore};
|
2026-06-15 13:15:18 -07:00
|
|
|
use tracing::info;
|
|
|
|
|
use tracing::instrument;
|
|
|
|
|
|
|
|
|
|
pub use identity::MlsIdentityProvider;
|
|
|
|
|
pub(crate) use mls_provider::MlsEphemeralPqProvider;
|
2026-05-19 11:54:54 -07:00
|
|
|
|
|
|
|
|
use crate::ChatError;
|
|
|
|
|
use crate::DeliveryService;
|
|
|
|
|
use crate::RegistrationService;
|
2026-06-15 13:15:18 -07:00
|
|
|
use crate::conversation::GroupConvo;
|
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::conversation::GroupV1Convo;
|
2026-06-15 13:15:18 -07:00
|
|
|
use crate::conversation::GroupV2Convo;
|
2026-06-19 08:43:55 -07:00
|
|
|
use crate::conversation::Identified as _;
|
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::service_context::{ExternalServices, ServiceContext};
|
2026-05-19 11:54:54 -07:00
|
|
|
use crate::utils::{blake2b_hex, hash_size};
|
2026-07-03 23:18:10 +02:00
|
|
|
use crate::{AddressedEnvelope, IdentId, IdentIdRef, IdentityProvider};
|
2026-06-15 13:15:18 -07:00
|
|
|
|
|
|
|
|
// Downgraded from MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 until demls accepts an external provider
|
2026-06-23 18:08:25 +03:00
|
|
|
pub(crate) const CIPHER_SUITE: Ciphersuite =
|
|
|
|
|
Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
|
2026-05-19 11:54:54 -07:00
|
|
|
|
|
|
|
|
// Define unique Identifiers derivations used in InboxV2
|
2026-06-10 06:59:04 -07:00
|
|
|
fn delivery_address_for(ident_id: IdentIdRef) -> String {
|
|
|
|
|
blake2b_hex::<hash_size::DeliveryAddr>(&["InboxV2|", "delivery_address|", ident_id.as_str()])
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-10 06:59:04 -07:00
|
|
|
fn conversation_id_for(ident_id: IdentIdRef) -> String {
|
|
|
|
|
blake2b_hex::<hash_size::ConvoId>(&["InboxV2|", "conversation_id|", ident_id.as_str()])
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 15:21:21 -07:00
|
|
|
/// An Extension trait which extends OpenMlsProvider to add required functionality
|
|
|
|
|
/// All MLS based Conversation should use this trait for defining requirements.
|
|
|
|
|
pub trait MlsProvider: OpenMlsProvider {
|
|
|
|
|
fn invite_user<DS: DeliveryService>(
|
|
|
|
|
&self,
|
|
|
|
|
ds: &mut DS,
|
2026-06-10 06:59:04 -07:00
|
|
|
ident_id: IdentIdRef,
|
2026-06-02 15:21:21 -07:00
|
|
|
welcome: &MlsMessageOut,
|
|
|
|
|
) -> Result<(), ChatError>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 23:18:10 +02:00
|
|
|
/// Deliver a de-mls welcome to `signer_id` over its InboxV2 1-1 channel.
|
2026-06-15 13:15:18 -07:00
|
|
|
/// Function mirroring the GroupV1 `invite_user` path, but carrying a de-mls `MemberWelcome`.
|
|
|
|
|
pub fn invite_user_v2<DS: DeliveryService>(
|
|
|
|
|
ds: &mut DS,
|
2026-07-03 23:18:10 +02:00
|
|
|
signer_id: IdentIdRef,
|
2026-06-15 13:15:18 -07:00
|
|
|
welcome: &MemberWelcome,
|
|
|
|
|
) -> Result<(), ChatError> {
|
|
|
|
|
let frame = InboxV2Frame {
|
|
|
|
|
payload: Some(InviteType::GroupV2(welcome.encode_to_vec())),
|
|
|
|
|
};
|
|
|
|
|
let envelope = EnvelopeV1 {
|
2026-07-03 23:18:10 +02:00
|
|
|
conversation_hint: conversation_id_for(signer_id),
|
2026-06-15 13:15:18 -07:00
|
|
|
salt: 0,
|
|
|
|
|
payload: frame.encode_to_vec().into(),
|
|
|
|
|
};
|
|
|
|
|
ds.publish(AddressedEnvelope {
|
2026-07-03 23:18:10 +02:00
|
|
|
delivery_address: delivery_address_for(signer_id),
|
2026-06-15 13:15:18 -07:00
|
|
|
data: envelope.encode_to_vec(),
|
|
|
|
|
})
|
|
|
|
|
.map_err(ChatError::generic)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 23:18:10 +02:00
|
|
|
/// A PQ focused Conversation initializer.
|
|
|
|
|
/// InboxV2 is signer-scoped: it receives invites under this installation's
|
|
|
|
|
/// signer id (the hex of the signer's verifying key), supporting PQ based
|
|
|
|
|
/// conversation protocols such as MLS.
|
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 InboxV2 {
|
2026-07-03 23:18:10 +02:00
|
|
|
// Owned so it can be returned via reference.
|
2026-06-10 06:59:04 -07:00
|
|
|
ident_id: IdentId,
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
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 InboxV2 {
|
2026-06-10 06:59:04 -07:00
|
|
|
pub fn new(ident_id: IdentId) -> Self {
|
2026-06-24 07:34:52 -07:00
|
|
|
Self { ident_id }
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-10 06:59:04 -07:00
|
|
|
pub fn ident_id(&self) -> IdentIdRef<'_> {
|
|
|
|
|
&self.ident_id
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Submit MlsKeypackage to registration service
|
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 register<S: ExternalServices>(
|
2026-06-15 13:15:18 -07:00
|
|
|
&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>,
|
|
|
|
|
) -> Result<(), ChatError> {
|
|
|
|
|
let keypackage_bytes = Self::create_keypackage(cx)?.tls_serialize_detached()?;
|
2026-05-19 11:54:54 -07:00
|
|
|
|
|
|
|
|
// TODO: (P3) Each keypackage can only be used once either enable...
|
|
|
|
|
// "LastResort" package or publish multiple
|
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.registry
|
|
|
|
|
.register(&cx.mls_identity, keypackage_bytes)
|
2026-06-15 13:15:18 -07:00
|
|
|
.map_err(ChatError::generic)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn delivery_address(&self) -> String {
|
2026-06-10 06:59:04 -07:00
|
|
|
delivery_address_for(&self.ident_id)
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn id(&self) -> String {
|
2026-06-10 06:59:04 -07:00
|
|
|
conversation_id_for(&self.ident_id)
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 13:15:18 -07:00
|
|
|
#[instrument(name = "inboxV2.handle_frame", skip_all, fields(user_id = %service_ctx.mls_identity.display_name()))]
|
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 handle_frame<S: ExternalServices>(
|
2026-06-02 15:21:21 -07:00
|
|
|
&self,
|
2026-06-15 13:15:18 -07:00
|
|
|
service_ctx: &mut ServiceContext<S>,
|
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
|
|
|
payload_bytes: &[u8],
|
2026-06-15 13:15:18 -07:00
|
|
|
) -> Result<Option<Box<dyn GroupConvo<S>>>, ChatError> {
|
|
|
|
|
// On a broadcast transport the inbox address also receives traffic
|
|
|
|
|
// that isn't an invite (or that prost decodes into an empty frame).
|
|
|
|
|
// Treat anything we can't interpret as "not for us" and skip it,
|
|
|
|
|
// rather than failing the whole poll cycle.
|
|
|
|
|
let Ok(inbox_frame) = InboxV2Frame::decode(payload_bytes) else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
2026-05-19 11:54:54 -07:00
|
|
|
let Some(payload) = inbox_frame.payload else {
|
2026-06-15 13:15:18 -07:00
|
|
|
return Ok(None);
|
2026-05-19 11:54:54 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match payload {
|
2026-06-15 13:15:18 -07:00
|
|
|
InviteType::GroupV1(inv) => {
|
|
|
|
|
Ok(Some(Box::new(self.handle_heavy_invite(service_ctx, inv)?)))
|
|
|
|
|
}
|
|
|
|
|
InviteType::GroupV2(welcome_bytes) => {
|
|
|
|
|
info!("Process WelcomeMessage");
|
|
|
|
|
let mw =
|
|
|
|
|
MemberWelcome::decode(welcome_bytes.as_slice()).map_err(ChatError::generic)?;
|
2026-06-24 07:34:52 -07:00
|
|
|
let convo = GroupV2Convo::new_from_welcome(service_ctx, &mw)?;
|
2026-06-15 13:15:18 -07:00
|
|
|
Ok(Some(Box::new(convo)))
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 persist_convo<S: ExternalServices>(
|
|
|
|
|
&self,
|
|
|
|
|
convo: &GroupV1Convo,
|
|
|
|
|
cx: &mut ServiceContext<S>,
|
|
|
|
|
) -> Result<(), ChatError> {
|
2026-05-19 11:54:54 -07:00
|
|
|
// TODO: (P2) Remove remote_convo_id this is an implementation detail specific to PrivateV1
|
|
|
|
|
// TODO: (P3) Implement From<Convo> for ConversationMeta
|
|
|
|
|
let meta = ConversationMeta {
|
|
|
|
|
local_convo_id: convo.id().to_string(),
|
|
|
|
|
remote_convo_id: "0".into(),
|
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
|
|
|
kind: ConversationKind::GroupV1,
|
2026-05-19 11:54:54 -07:00
|
|
|
};
|
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.store.save_conversation(&meta)?;
|
2026-05-19 11:54:54 -07:00
|
|
|
// TODO: (P1) Persist state
|
|
|
|
|
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
|
|
|
fn handle_heavy_invite<S: ExternalServices>(
|
|
|
|
|
&self,
|
|
|
|
|
cx: &mut ServiceContext<S>,
|
2026-06-15 13:15:18 -07:00
|
|
|
invite: GroupV1HeavyInvite,
|
|
|
|
|
) -> Result<GroupV1Convo, ChatError> {
|
2026-05-19 11:54:54 -07:00
|
|
|
let (msg_in, _rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?;
|
|
|
|
|
|
|
|
|
|
let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else {
|
|
|
|
|
return Err(ChatError::ProtocolExpectation(
|
|
|
|
|
"something else",
|
|
|
|
|
"Welcome".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
|
|
|
let convo = GroupV1Convo::new_from_welcome(cx, welcome)?;
|
|
|
|
|
self.persist_convo(&convo, cx)?;
|
2026-06-15 13:15:18 -07:00
|
|
|
|
|
|
|
|
Ok(convo)
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
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 create_keypackage<S: ExternalServices>(
|
|
|
|
|
cx: &ServiceContext<S>,
|
|
|
|
|
) -> Result<KeyPackage, ChatError> {
|
2026-05-19 11:54:54 -07:00
|
|
|
let capabilities = Capabilities::builder()
|
2026-06-15 13:15:18 -07:00
|
|
|
.ciphersuites(vec![CIPHER_SUITE])
|
2026-05-19 11:54:54 -07:00
|
|
|
.extensions(vec![ExtensionType::ApplicationId])
|
|
|
|
|
.build();
|
|
|
|
|
let a = KeyPackage::builder()
|
|
|
|
|
.leaf_node_capabilities(capabilities)
|
|
|
|
|
.build(
|
2026-06-15 13:15:18 -07:00
|
|
|
CIPHER_SUITE,
|
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,
|
|
|
|
|
&cx.mls_identity,
|
|
|
|
|
cx.mls_identity.get_credential(),
|
2026-05-19 11:54:54 -07:00
|
|
|
)
|
|
|
|
|
.expect("Failed to build KeyPackage");
|
|
|
|
|
|
|
|
|
|
Ok(a.key_package().clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
|
|
|
pub struct InboxV2Frame {
|
2026-06-15 13:15:18 -07:00
|
|
|
#[prost(oneof = "InviteType", tags = "1, 2")]
|
2026-05-19 11:54:54 -07:00
|
|
|
pub payload: Option<InviteType>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, PartialEq, Oneof)]
|
|
|
|
|
pub enum InviteType {
|
|
|
|
|
#[prost(message, tag = "1")]
|
|
|
|
|
GroupV1(GroupV1HeavyInvite),
|
2026-06-15 13:15:18 -07:00
|
|
|
#[prost(bytes, tag = "2")]
|
|
|
|
|
GroupV2(Vec<u8>),
|
2026-05-19 11:54:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
|
|
|
pub struct GroupV1HeavyInvite {
|
|
|
|
|
#[prost(bytes, tag = "1")]
|
|
|
|
|
pub welcome_bytes: Vec<u8>,
|
|
|
|
|
}
|