Skip to content

Commit ac662b3

Browse files
authored
feat(xmtp_mls,ffi): add membership_capabilities read surface for proposal migration (#3803)
## What Adds `MlsGroup::membership_capabilities()` plus an FFI mirror and iOS/Android SDK wrappers. It reports **generic per-installation MLS capability facts** so apps can answer migration questions themselves: - `context_extensions` — the extension types present in the group's context. - `members[].installations[].supported_extensions` — the extension types each member installation advertises. - `capabilities_known` — whether those facts could be determined (vs. "advertises nothing"). ## Why generic Rather than bake in a proposals-specific answer, this surfaces raw facts the app filters. For the proposal / app-data-dictionary migration specifically: - **already migrated?** → `context_extensions` contains `AppDataDictionary` - **eligible to migrate?** → every installation's `supported_extensions` contains it - **who's blocking?** → inboxes with an installation that doesn't ## Notes - Remote installations' capabilities come from their **latest published key package** (so a stale in-group leaf can't mask an upgraded client); the local installation's come from its in-group leaf node. - An installation whose key package can't be fetched/verified is reported with `capabilities_known == false` rather than failing the whole call. This matters: old clients — exactly what this surface exists to flag — are the ones likely to lack a published key package, and `fetch_key_packages` errors wholesale on any gap. So we try one batch fetch, then fall back to **bounded** per-installation fetches that tolerate gaps. - The regenerated iOS `xmtpv3.swift` also drops the already-removed `pendingSelfRemove` worker variant that the committed binding still carried (source-faithful regen — the release workflow would do the same). ## Mobile SDKs Adds `Group.membershipCapabilities()` to the Swift (`XMTPiOS`) and Kotlin (`org.xmtp.android`) SDKs, with `MlsExtensionType` / `InstallationCapabilities` / `InboxCapabilities` / `GroupMembershipCapabilities` mirror types. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 58a583e commit ac662b3

8 files changed

Lines changed: 1192 additions & 8 deletions

File tree

bindings/mobile/src/mls.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ use xmtp_id::{
6464
use xmtp_mls::client::inbox_addresses_with_verifier;
6565
use xmtp_mls::context::XmtpSharedContext;
6666
use xmtp_mls::cursor_store::SqliteCursorStore;
67-
use xmtp_mls::groups::ConversationDebugInfo;
67+
use xmtp_mls::groups::{
68+
ConversationDebugInfo, GroupMembershipCapabilities, InboxCapabilities,
69+
InstallationCapabilities, MlsExtensionType,
70+
};
6871
use xmtp_mls::identity_updates::revoke_installations_with_verifier;
6972
use xmtp_mls::identity_updates::{
7073
apply_signature_request_with_verifier, get_creation_signature_kind,
@@ -2212,6 +2215,106 @@ impl From<ConversationDebugInfo> for FfiConversationDebugInfo {
22122215
}
22132216
}
22142217

2218+
/// An MLS extension type advertised by an installation's key package or
2219+
/// present in a group's context. Mirrors
2220+
/// [`xmtp_mls::groups::MlsExtensionType`].
2221+
#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
2222+
pub enum FfiMlsExtensionType {
2223+
ApplicationId,
2224+
RatchetTree,
2225+
RequiredCapabilities,
2226+
ExternalPub,
2227+
ExternalSenders,
2228+
LastResort,
2229+
ImmutableMetadata,
2230+
AppDataDictionary,
2231+
Unknown { id: u16 },
2232+
Grease { id: u16 },
2233+
}
2234+
2235+
impl From<MlsExtensionType> for FfiMlsExtensionType {
2236+
fn from(value: MlsExtensionType) -> Self {
2237+
match value {
2238+
MlsExtensionType::ApplicationId => FfiMlsExtensionType::ApplicationId,
2239+
MlsExtensionType::RatchetTree => FfiMlsExtensionType::RatchetTree,
2240+
MlsExtensionType::RequiredCapabilities => FfiMlsExtensionType::RequiredCapabilities,
2241+
MlsExtensionType::ExternalPub => FfiMlsExtensionType::ExternalPub,
2242+
MlsExtensionType::ExternalSenders => FfiMlsExtensionType::ExternalSenders,
2243+
MlsExtensionType::LastResort => FfiMlsExtensionType::LastResort,
2244+
MlsExtensionType::ImmutableMetadata => FfiMlsExtensionType::ImmutableMetadata,
2245+
MlsExtensionType::AppDataDictionary => FfiMlsExtensionType::AppDataDictionary,
2246+
MlsExtensionType::Unknown(id) => FfiMlsExtensionType::Unknown { id },
2247+
MlsExtensionType::Grease(id) => FfiMlsExtensionType::Grease { id },
2248+
}
2249+
}
2250+
}
2251+
2252+
/// Capabilities for a single installation (device) in a group. Mirrors
2253+
/// [`xmtp_mls::groups::InstallationCapabilities`].
2254+
#[derive(uniffi::Record, Clone, Debug)]
2255+
pub struct FfiInstallationCapabilities {
2256+
pub installation_id: Vec<u8>,
2257+
pub is_own: bool,
2258+
pub supported_extensions: Vec<FfiMlsExtensionType>,
2259+
pub capabilities_known: bool,
2260+
}
2261+
2262+
/// Per-inbox installation capabilities. Mirrors
2263+
/// [`xmtp_mls::groups::InboxCapabilities`].
2264+
#[derive(uniffi::Record, Clone, Debug)]
2265+
pub struct FfiInboxCapabilities {
2266+
pub inbox_id: String,
2267+
pub installations: Vec<FfiInstallationCapabilities>,
2268+
}
2269+
2270+
/// A generic membership/capability snapshot for a group. Mirrors
2271+
/// [`xmtp_mls::groups::GroupMembershipCapabilities`]. Callers filter it — e.g.
2272+
/// the proposal migration is complete when `context_extensions` contains
2273+
/// `AppDataDictionary`, and an inbox blocks migration when one of its
2274+
/// installations' `supported_extensions` does not.
2275+
#[derive(uniffi::Record, Clone, Debug)]
2276+
pub struct FfiGroupMembershipCapabilities {
2277+
pub context_extensions: Vec<FfiMlsExtensionType>,
2278+
pub members: Vec<FfiInboxCapabilities>,
2279+
}
2280+
2281+
impl From<InstallationCapabilities> for FfiInstallationCapabilities {
2282+
fn from(value: InstallationCapabilities) -> Self {
2283+
FfiInstallationCapabilities {
2284+
installation_id: value.installation_id,
2285+
is_own: value.is_own,
2286+
supported_extensions: value
2287+
.supported_extensions
2288+
.into_iter()
2289+
.map(Into::into)
2290+
.collect(),
2291+
capabilities_known: value.capabilities_known,
2292+
}
2293+
}
2294+
}
2295+
2296+
impl From<InboxCapabilities> for FfiInboxCapabilities {
2297+
fn from(value: InboxCapabilities) -> Self {
2298+
FfiInboxCapabilities {
2299+
inbox_id: value.inbox_id,
2300+
installations: value.installations.into_iter().map(Into::into).collect(),
2301+
}
2302+
}
2303+
}
2304+
2305+
impl From<GroupMembershipCapabilities> for FfiGroupMembershipCapabilities {
2306+
fn from(value: GroupMembershipCapabilities) -> Self {
2307+
FfiGroupMembershipCapabilities {
2308+
context_extensions: value
2309+
.context_extensions
2310+
.into_iter()
2311+
.map(Into::into)
2312+
.collect(),
2313+
members: value.members.into_iter().map(Into::into).collect(),
2314+
}
2315+
}
2316+
}
2317+
22152318
impl From<RustMlsGroup> for FfiConversation {
22162319
fn from(mls_group: RustMlsGroup) -> FfiConversation {
22172320
FfiConversation { inner: mls_group }
@@ -3032,6 +3135,19 @@ impl FfiConversation {
30323135
Ok(debug_info.into())
30333136
}
30343137

3138+
/// Snapshot this group's membership capabilities: the group context's
3139+
/// extension types plus, per member inbox and installation, the extension
3140+
/// types each advertises. Generic facts the caller filters — e.g. to
3141+
/// answer whether the group is migrated to the proposal flow and which
3142+
/// members block it. See
3143+
/// [`xmtp_mls::groups::MlsGroup::membership_capabilities`].
3144+
pub async fn membership_capabilities(
3145+
&self,
3146+
) -> Result<FfiGroupMembershipCapabilities, FfiError> {
3147+
let capabilities = self.inner.membership_capabilities().await?;
3148+
Ok(capabilities.into())
3149+
}
3150+
30353151
#[tracing::instrument(level = "debug", skip_all)]
30363152
pub async fn find_duplicate_dms(&self) -> Result<Vec<Arc<FfiConversation>>, FfiError> {
30373153
let dms = self.inner.find_duplicate_dms()?;

0 commit comments

Comments
 (0)