Skip to main content

xmtp_proto/types/
server_configuration.rs

1//! Turning one `GetConfigurationResponse` into the snapshot the client holds.
2//!
3//! Zero or empty on the wire means "not provided", so every such field falls
4//! back to the compiled `BACKEND_DEFAULT_*` constant. The two
5//! exceptions are the identifier, which is rejected when
6//! empty, and `commit_log_enabled`, which keeps `None` distinct from
7//! `Some(false)`.
8//!
9//! The application catalogue converts both ways, so the backend publishes
10//! and a client registers the same definitions the snapshot holds.
11
12use xmtp_configuration::{
13    ApplicationComponentDefinition, AttachmentsConfiguration, AuthConfiguration,
14    BACKEND_DEFAULT_MAX_UPLOAD_BYTES, ComponentPermissions, HandlesConfiguration,
15    LimitsConfiguration, MAX_IDENTITY_UPDATE_ACTIONS, MetadataPolicy, MlsConfiguration,
16    RetentionConfiguration, ServerConfiguration, SigningKeyDescription,
17    attachments::{
18        AttachmentConfigurationError, check_base_url, check_max_upload_bytes,
19        check_retention_seconds,
20    },
21};
22
23use crate::backend_v1;
24use crate::xmtp::mls::message_contents::{
25    self as mls,
26    metadata_policy::{AndCondition, AnyCondition, Kind},
27};
28
29/// Take the published value, or the compiled default when it is zero.
30// implements: CONF-025
31fn or_default<T, W>(published: W, default: T) -> T
32where
33    T: Copy + PartialEq + Default + TryFrom<W>,
34    W: Copy,
35{
36    T::try_from(published)
37        .ok()
38        .filter(|value| *value != T::default())
39        .unwrap_or(default)
40}
41
42impl From<backend_v1::AuthConfiguration> for AuthConfiguration {
43    fn from(auth: backend_v1::AuthConfiguration) -> Self {
44        Self {
45            enabled: auth.enabled,
46            keys: auth
47                .keys
48                .into_iter()
49                .map(|key| SigningKeyDescription {
50                    kid: key.kid,
51                    alg: key.alg,
52                })
53                .collect(),
54            audiences: auth.audiences,
55            issuers: auth.issuers,
56            required_scopes: auth.required_scopes,
57        }
58    }
59}
60
61impl From<backend_v1::RetentionConfiguration> for RetentionConfiguration {
62    fn from(retention: backend_v1::RetentionConfiguration) -> Self {
63        let default = Self::default();
64        Self {
65            group_message_seconds: or_default(
66                retention.group_message_seconds,
67                default.group_message_seconds,
68            ),
69            welcome_seconds: or_default(retention.welcome_seconds, default.welcome_seconds),
70            key_package_seconds: or_default(
71                retention.key_package_seconds,
72                default.key_package_seconds,
73            ),
74        }
75    }
76}
77
78impl From<backend_v1::LimitsConfiguration> for LimitsConfiguration {
79    fn from(limits: backend_v1::LimitsConfiguration) -> Self {
80        let default = Self::default();
81        Self {
82            max_envelope_bytes: or_default(limits.max_envelope_bytes, default.max_envelope_bytes),
83            max_request_bytes: or_default(limits.max_request_bytes, default.max_request_bytes),
84            max_response_bytes: or_default(limits.max_response_bytes, default.max_response_bytes),
85            max_publish_topics: or_default(limits.max_publish_topics, default.max_publish_topics),
86            max_query_topics: or_default(limits.max_query_topics, default.max_query_topics),
87            max_query_limit: or_default(limits.max_query_limit, default.max_query_limit),
88            default_query_limit: or_default(
89                limits.default_query_limit,
90                default.default_query_limit,
91            ),
92            max_newest_metadata_topics: or_default(
93                limits.max_newest_metadata_topics,
94                default.max_newest_metadata_topics,
95            ),
96            max_newest_full_topics: or_default(
97                limits.max_newest_full_topics,
98                default.max_newest_full_topics,
99            ),
100            max_update_adds: or_default(limits.max_update_adds, default.max_update_adds),
101            max_update_removes: or_default(limits.max_update_removes, default.max_update_removes),
102            max_stream_topics: or_default(limits.max_stream_topics, default.max_stream_topics),
103            max_static_topics: or_default(limits.max_static_topics, default.max_static_topics),
104            max_lookup_identifiers: or_default(
105                limits.max_lookup_identifiers,
106                default.max_lookup_identifiers,
107            ),
108            max_scw_signatures: or_default(limits.max_scw_signatures, default.max_scw_signatures),
109            max_identity_entries: or_default(
110                limits.max_identity_entries,
111                default.max_identity_entries,
112            ),
113            max_update_frames_per_second: or_default(
114                limits.max_update_frames_per_second,
115                default.max_update_frames_per_second,
116            ),
117            max_update_burst: or_default(limits.max_update_burst, default.max_update_burst),
118            max_ping_frames_per_second: or_default(
119                limits.max_ping_frames_per_second,
120                default.max_ping_frames_per_second,
121            ),
122            max_ping_burst: or_default(limits.max_ping_burst, default.max_ping_burst),
123        }
124    }
125}
126
127impl From<backend_v1::MlsConfiguration> for MlsConfiguration {
128    fn from(mls: backend_v1::MlsConfiguration) -> Self {
129        let default = Self::default();
130        Self {
131            max_group_members: or_default(mls.max_group_members, default.max_group_members),
132            // A ceiling above one update's actions would let an inbox hold
133            // installations that no single update can revoke.
134            max_installations_per_inbox: or_default(
135                mls.max_installations_per_inbox,
136                default.max_installations_per_inbox,
137            )
138            .min(MAX_IDENTITY_UPDATE_ACTIONS),
139            // Absent is not the same as false: an operator may switch the
140            // commit log off explicitly.
141            commit_log_enabled: mls.commit_log_enabled,
142        }
143    }
144}
145
146impl From<backend_v1::HandlesConfiguration> for HandlesConfiguration {
147    fn from(handles: backend_v1::HandlesConfiguration) -> Self {
148        Self {
149            min_name_length: or_default(handles.min_name_length, Self::default().min_name_length),
150        }
151    }
152}
153
154impl From<mls::MetadataPolicy> for MetadataPolicy {
155    fn from(policy: mls::MetadataPolicy) -> Self {
156        let all =
157            |policies: Vec<mls::MetadataPolicy>| policies.into_iter().map(Self::from).collect();
158        match policy.kind {
159            Some(Kind::Base(base)) => Self::Base(base),
160            Some(Kind::AndCondition(and)) => Self::And(all(and.policies)),
161            Some(Kind::AnyCondition(any)) => Self::Any(all(any.policies)),
162            // A policy with no kind evaluates as the unspecified base.
163            None => Self::Base(0),
164        }
165    }
166}
167
168impl From<MetadataPolicy> for mls::MetadataPolicy {
169    fn from(policy: MetadataPolicy) -> Self {
170        let all = |policies: Vec<MetadataPolicy>| policies.into_iter().map(Self::from).collect();
171        let kind = match policy {
172            MetadataPolicy::Base(base) => Kind::Base(base),
173            MetadataPolicy::And(policies) => Kind::AndCondition(AndCondition {
174                policies: all(policies),
175            }),
176            MetadataPolicy::Any(policies) => Kind::AnyCondition(AnyCondition {
177                policies: all(policies),
178            }),
179        };
180        Self { kind: Some(kind) }
181    }
182}
183
184impl From<mls::ComponentPermissions> for ComponentPermissions {
185    fn from(permissions: mls::ComponentPermissions) -> Self {
186        Self {
187            insert: permissions.insert_policy.map(Into::into),
188            update: permissions.update_policy.map(Into::into),
189            delete: permissions.delete_policy.map(Into::into),
190        }
191    }
192}
193
194impl From<ComponentPermissions> for mls::ComponentPermissions {
195    fn from(permissions: ComponentPermissions) -> Self {
196        Self {
197            insert_policy: permissions.insert.map(Into::into),
198            update_policy: permissions.update.map(Into::into),
199            delete_policy: permissions.delete.map(Into::into),
200        }
201    }
202}
203
204impl From<backend_v1::ApplicationComponentDefinition> for ApplicationComponentDefinition {
205    fn from(definition: backend_v1::ApplicationComponentDefinition) -> Self {
206        Self {
207            // Saturating keeps an oversized ID outside the application range,
208            // so validation refuses it rather than reading a truncated ID.
209            component_id: u16::try_from(definition.component_id).unwrap_or(u16::MAX),
210            name: definition.name,
211            component_type: definition.component_type,
212            permissions: definition.permissions.unwrap_or_default().into(),
213            in_groups: definition.in_groups,
214            in_dms: definition.in_dms,
215        }
216    }
217}
218
219impl From<ApplicationComponentDefinition> for backend_v1::ApplicationComponentDefinition {
220    fn from(definition: ApplicationComponentDefinition) -> Self {
221        Self {
222            component_id: definition.component_id.into(),
223            name: definition.name,
224            component_type: definition.component_type,
225            permissions: Some(definition.permissions.into()),
226            in_groups: definition.in_groups,
227            in_dms: definition.in_dms,
228        }
229    }
230}
231
232// implements: ATCH-008, CONF-025
233impl TryFrom<backend_v1::AttachmentsConfiguration> for AttachmentsConfiguration {
234    type Error = AttachmentConfigurationError;
235
236    fn try_from(attachments: backend_v1::AttachmentsConfiguration) -> Result<Self, Self::Error> {
237        let max_upload_bytes = or_default(
238            attachments.max_upload_bytes,
239            BACKEND_DEFAULT_MAX_UPLOAD_BYTES,
240        );
241        check_base_url(&attachments.base_url)?;
242        check_max_upload_bytes(max_upload_bytes)?;
243        check_retention_seconds(attachments.retention_seconds)?;
244        Ok(Self {
245            base_url: attachments.base_url,
246            max_upload_bytes,
247            retention_seconds: attachments.retention_seconds,
248        })
249    }
250}
251
252impl From<backend_v1::GetConfigurationResponse> for ServerConfiguration {
253    fn from(response: backend_v1::GetConfigurationResponse) -> Self {
254        let attachments = response.attachments.and_then(|message| {
255            match AttachmentsConfiguration::try_from(message) {
256                Ok(attachments) => Some(attachments),
257                Err(reason) => {
258                    tracing::warn!(
259                        field = reason.field(),
260                        reason = reason.reason(),
261                        "ignoring unusable attachment storage offer"
262                    );
263                    None
264                }
265            }
266        });
267        Self {
268            identifier: response.identifier,
269            server_version: response.server_version,
270            min_libxmtp_version: response.min_libxmtp_version,
271            auth: response.auth.unwrap_or_default().into(),
272            retention: response.retention.unwrap_or_default().into(),
273            limits: response.limits.unwrap_or_default().into(),
274            mls: response.mls.unwrap_or_default().into(),
275            handles: response.handles.unwrap_or_default().into(),
276            attachments,
277            smart_contract_wallet_chains: response.smart_contract_wallet_chains,
278            application_components: response
279                .application_components
280                .into_iter()
281                .map(Into::into)
282                .collect(),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests;