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, MetadataPolicy, MlsConfiguration, RetentionConfiguration,
16    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            max_installations_per_inbox: or_default(
133                mls.max_installations_per_inbox,
134                default.max_installations_per_inbox,
135            ),
136            // Absent is not the same as false: an operator may switch the
137            // commit log off explicitly.
138            commit_log_enabled: mls.commit_log_enabled,
139        }
140    }
141}
142
143impl From<backend_v1::HandlesConfiguration> for HandlesConfiguration {
144    fn from(handles: backend_v1::HandlesConfiguration) -> Self {
145        Self {
146            min_name_length: or_default(handles.min_name_length, Self::default().min_name_length),
147        }
148    }
149}
150
151impl From<mls::MetadataPolicy> for MetadataPolicy {
152    fn from(policy: mls::MetadataPolicy) -> Self {
153        let all =
154            |policies: Vec<mls::MetadataPolicy>| policies.into_iter().map(Self::from).collect();
155        match policy.kind {
156            Some(Kind::Base(base)) => Self::Base(base),
157            Some(Kind::AndCondition(and)) => Self::And(all(and.policies)),
158            Some(Kind::AnyCondition(any)) => Self::Any(all(any.policies)),
159            // A policy with no kind evaluates as the unspecified base.
160            None => Self::Base(0),
161        }
162    }
163}
164
165impl From<MetadataPolicy> for mls::MetadataPolicy {
166    fn from(policy: MetadataPolicy) -> Self {
167        let all = |policies: Vec<MetadataPolicy>| policies.into_iter().map(Self::from).collect();
168        let kind = match policy {
169            MetadataPolicy::Base(base) => Kind::Base(base),
170            MetadataPolicy::And(policies) => Kind::AndCondition(AndCondition {
171                policies: all(policies),
172            }),
173            MetadataPolicy::Any(policies) => Kind::AnyCondition(AnyCondition {
174                policies: all(policies),
175            }),
176        };
177        Self { kind: Some(kind) }
178    }
179}
180
181impl From<mls::ComponentPermissions> for ComponentPermissions {
182    fn from(permissions: mls::ComponentPermissions) -> Self {
183        Self {
184            insert: permissions.insert_policy.map(Into::into),
185            update: permissions.update_policy.map(Into::into),
186            delete: permissions.delete_policy.map(Into::into),
187        }
188    }
189}
190
191impl From<ComponentPermissions> for mls::ComponentPermissions {
192    fn from(permissions: ComponentPermissions) -> Self {
193        Self {
194            insert_policy: permissions.insert.map(Into::into),
195            update_policy: permissions.update.map(Into::into),
196            delete_policy: permissions.delete.map(Into::into),
197        }
198    }
199}
200
201impl From<backend_v1::ApplicationComponentDefinition> for ApplicationComponentDefinition {
202    fn from(definition: backend_v1::ApplicationComponentDefinition) -> Self {
203        Self {
204            // Saturating keeps an oversized ID outside the application range,
205            // so validation refuses it rather than reading a truncated ID.
206            component_id: u16::try_from(definition.component_id).unwrap_or(u16::MAX),
207            name: definition.name,
208            component_type: definition.component_type,
209            permissions: definition.permissions.unwrap_or_default().into(),
210            in_groups: definition.in_groups,
211            in_dms: definition.in_dms,
212        }
213    }
214}
215
216impl From<ApplicationComponentDefinition> for backend_v1::ApplicationComponentDefinition {
217    fn from(definition: ApplicationComponentDefinition) -> Self {
218        Self {
219            component_id: definition.component_id.into(),
220            name: definition.name,
221            component_type: definition.component_type,
222            permissions: Some(definition.permissions.into()),
223            in_groups: definition.in_groups,
224            in_dms: definition.in_dms,
225        }
226    }
227}
228
229// implements: ATCH-008, CONF-025
230impl TryFrom<backend_v1::AttachmentsConfiguration> for AttachmentsConfiguration {
231    type Error = AttachmentConfigurationError;
232
233    fn try_from(attachments: backend_v1::AttachmentsConfiguration) -> Result<Self, Self::Error> {
234        let max_upload_bytes = or_default(
235            attachments.max_upload_bytes,
236            BACKEND_DEFAULT_MAX_UPLOAD_BYTES,
237        );
238        check_base_url(&attachments.base_url)?;
239        check_max_upload_bytes(max_upload_bytes)?;
240        check_retention_seconds(attachments.retention_seconds)?;
241        Ok(Self {
242            base_url: attachments.base_url,
243            max_upload_bytes,
244            retention_seconds: attachments.retention_seconds,
245        })
246    }
247}
248
249impl From<backend_v1::GetConfigurationResponse> for ServerConfiguration {
250    fn from(response: backend_v1::GetConfigurationResponse) -> Self {
251        let attachments = response.attachments.and_then(|message| {
252            match AttachmentsConfiguration::try_from(message) {
253                Ok(attachments) => Some(attachments),
254                Err(reason) => {
255                    tracing::warn!(
256                        field = reason.field(),
257                        reason = reason.reason(),
258                        "ignoring unusable attachment storage offer"
259                    );
260                    None
261                }
262            }
263        });
264        Self {
265            identifier: response.identifier,
266            server_version: response.server_version,
267            min_libxmtp_version: response.min_libxmtp_version,
268            auth: response.auth.unwrap_or_default().into(),
269            retention: response.retention.unwrap_or_default().into(),
270            limits: response.limits.unwrap_or_default().into(),
271            mls: response.mls.unwrap_or_default().into(),
272            handles: response.handles.unwrap_or_default().into(),
273            attachments,
274            smart_contract_wallet_chains: response.smart_contract_wallet_chains,
275            application_components: response
276                .application_components
277                .into_iter()
278                .map(Into::into)
279                .collect(),
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests;