Skip to main content

xmtp_mls/groups/
metadata_fields.rs

1//! Typed metadata fields and user data.
2//!
3//! These methods are the developer-facing view of a group's AppData
4//! dictionary. Each call reads one committed dictionary snapshot through
5//! [`FieldSnapshot`]; the committed registry decides which fields exist and
6//! their policies, the protocol fixes a well-known field's type, and the backend
7//! catalogue only labels application fields. Writes are checked against the
8//! committed registry's types and policies before they are queued as
9//! [`AppDataUpdateIntentData::Fields`], and again when the commit is built,
10//! so a write every receiver would reject is never published.
11
12use std::collections::BTreeMap;
13
14use openmls::group::MlsGroup as OpenMlsGroup;
15use xmtp_db::{
16    Fetch,
17    group_intent::{ID, IntentState, StoredGroupIntent},
18};
19use xmtp_mls_common::{
20    app_data::fields::{
21        ComponentMutation, FieldError, FieldKey, FieldSnapshot, FieldValue, FieldWrite,
22        MetadataFieldDescriptor, MetadataFieldRef, MetadataFieldValue, MetadataValue,
23        UserFieldUpdate, UserFieldValue,
24    },
25    inbox_id::InboxId,
26};
27
28use super::{
29    GroupError, MlsGroup,
30    app_data::sender_intents::field_writes_commit,
31    intents::{AppDataUpdateIntentData, QueueIntent},
32};
33use crate::context::XmtpSharedContext;
34
35impl<Context> MlsGroup<Context>
36where
37    Context: XmtpSharedContext,
38{
39    /// The group's metadata fields in component ID order.
40    // implements: META-069
41    pub fn metadata_fields(&self) -> Result<Vec<MetadataFieldDescriptor>, GroupError> {
42        self.read_fields(|fields| Ok(fields.fields().to_vec()))
43    }
44
45    /// The field named `name`, preferring a well-known field to an
46    /// application field of the same name.
47    // implements: META-069
48    pub fn metadata_field(
49        &self,
50        name: &str,
51    ) -> Result<Option<MetadataFieldDescriptor>, GroupError> {
52        self.read_fields(|fields| Ok(fields.field(name).cloned()))
53    }
54
55    /// The value of `field`, `None` when absent.
56    // implements: META-070
57    pub fn metadata_value(
58        &self,
59        field: &MetadataFieldRef,
60    ) -> Result<Option<MetadataValue>, GroupError> {
61        self.read_fields(|fields| fields.value(field))
62    }
63
64    /// The values of `fields` in request order, read from one snapshot.
65    // implements: META-070
66    pub fn metadata_values(
67        &self,
68        fields: &[MetadataFieldRef],
69    ) -> Result<Vec<MetadataFieldValue>, GroupError> {
70        self.read_fields(|snapshot| snapshot.values(fields))
71    }
72
73    /// The value under `key` of the map `field`.
74    // implements: META-070
75    pub fn map_value(
76        &self,
77        field: &MetadataFieldRef,
78        key: &FieldKey,
79    ) -> Result<Option<FieldValue>, GroupError> {
80        self.read_fields(|fields| fields.map_value(field, key))
81    }
82
83    /// Per-inbox user field values, read from one snapshot. See
84    /// [`FieldSnapshot::user_data`] for the `None` selections.
85    // implements: META-072
86    pub fn user_data(
87        &self,
88        fields: Option<&[MetadataFieldRef]>,
89        inbox_ids: Option<&[InboxId]>,
90    ) -> Result<BTreeMap<InboxId, Vec<UserFieldValue>>, GroupError> {
91        self.read_fields(|snapshot| snapshot.user_data(fields, inbox_ids))
92    }
93
94    /// Apply `mutation` to `field` in one commit, subject to the committed
95    /// registry's type and policies.
96    // implements: META-071
97    pub async fn update_metadata_field(
98        &self,
99        field: &MetadataFieldRef,
100        mutation: &ComponentMutation,
101    ) -> Result<(), GroupError> {
102        self.write_fields(|fields| Ok(vec![fields.field_write(field, mutation)?]))
103            .await
104    }
105
106    /// Set or clear this inbox's own entries of user fields in one commit.
107    /// Nothing is committed when `values` is empty or only clears absent
108    /// entries.
109    // implements: META-073
110    pub async fn update_user_data(&self, values: &[UserFieldUpdate]) -> Result<(), GroupError> {
111        self.write_fields(|fields| fields.user_data_writes(values))
112            .await
113    }
114
115    fn read_fields<T>(
116        &self,
117        read: impl FnOnce(&FieldSnapshot<'_>) -> Result<T, FieldError>,
118    ) -> Result<T, GroupError> {
119        let context = self.load_group_context()?;
120        let dictionary = context
121            .extensions()
122            .app_data_dictionary()
123            .map(|extension| extension.dictionary());
124        let catalogue = &self
125            .context
126            .server_configuration()
127            .configuration()
128            .application_components;
129        Ok(read(&FieldSnapshot::new(dictionary, catalogue)?)?)
130    }
131
132    /// Encode writes with `plan` and commit them. The writes are resolved
133    /// and authorized once here, so a type error, a denied policy, or a
134    /// payload the group would reject is reported to the caller rather than
135    /// as a failed intent.
136    ///
137    /// A write that changes nothing commits nothing. It is judged after a
138    /// sync, against the group's committed state at a point during this
139    /// call, so success is true at that point, and a commit that lands
140    /// later is a later write, as it would be after a commit of ours. The
141    /// publisher cannot make this call instead: it publishes before it
142    /// receives, so it sees the same local state. A write that pending
143    /// proposals carry out but the committed values do not still commits
144    /// them, here and when it is published, so success never rests on an
145    /// uncommitted value.
146    async fn write_fields(
147        &self,
148        plan: impl FnOnce(&FieldSnapshot<'_>) -> Result<Vec<FieldWrite>, FieldError>,
149    ) -> Result<(), GroupError> {
150        self.ensure_not_paused().await?;
151        self.sync().await?;
152        let own = InboxId::from_hex(self.context.inbox_id())
153            .map_err(|e| GroupError::ComponentSource(e.into()))?;
154        let writes = self.with_group_snapshot(|group: &OpenMlsGroup| {
155            let committed = group
156                .extensions()
157                .app_data_dictionary()
158                .map(|extension| extension.dictionary());
159            let writes = plan(&FieldSnapshot::new(committed, &[])?)?;
160            Ok(field_writes_commit(group, own, &writes)?.map(|_| writes))
161        })?;
162        let Some(writes) = writes else {
163            return Ok(());
164        };
165        let intent = QueueIntent::app_data_update()
166            .data(Vec::<u8>::from(AppDataUpdateIntentData::Fields(
167                writes.clone(),
168            )))
169            .queue(self)?;
170        self.publish_field_writes(intent.id, own, &writes).await
171    }
172
173    /// Publish `writes`, which `own` queued as `intent_id`. The publisher
174    /// resolves them again when it builds their commit, so a commit that
175    /// lands first and re-types a field or tightens its policy fails the
176    /// intent before it is published. That failure is stored without
177    /// details, and another sync or another queued write may be the one
178    /// that reports it, so a failed intent is resolved again here and its
179    /// own [`FieldError`] returned in place of the sync error. A write that
180    /// is still queued keeps the sync error: it has not failed yet.
181    pub(super) async fn publish_field_writes(
182        &self,
183        intent_id: ID,
184        own: InboxId,
185        writes: &[FieldWrite],
186    ) -> Result<(), GroupError> {
187        let Err(error) = self.sync_until_intent_resolved(intent_id).await else {
188            return Ok(());
189        };
190        let failed = matches!(
191            self.context.db().fetch(&intent_id),
192            Ok(Some(StoredGroupIntent {
193                state: IntentState::Error,
194                ..
195            }))
196        );
197        if failed
198            && let Err(field @ GroupError::MetadataField(_)) =
199                self.with_group_snapshot(|group| field_writes_commit(group, own, writes))
200        {
201            return Err(field);
202        }
203        Err(error)
204    }
205}