tuwunel_api_client/
utils.rs1use std::{collections::BTreeMap, time::Duration};
2
3use ruma::{
4 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, TransactionId, UserId,
5 api::client::filter::FilterDefinition, profile::ProfileFieldName, thirdparty::Medium,
6};
7use serde_json::Value as JsonValue;
8use tokio::time::Instant;
9use tuwunel_core::{
10 Err, Result, err,
11 utils::{string::str_from_bytes, time::millis},
12 warn,
13};
14use tuwunel_matrix::Event;
15use tuwunel_service::Services;
16
17use crate::Ruma;
18
19pub(crate) fn normalize_profile_fields(mut filter: FilterDefinition) -> FilterDefinition {
20 filter.profile_fields.ids.sort_unstable();
21 filter.profile_fields.ids.dedup();
22 filter
23}
24
25pub(crate) async fn invite_check(
26 services: &Services,
27 sender_user: &UserId,
28 room_id: &RoomId,
29) -> Result {
30 if services.config.rooms.invites.block_non_admin
31 && !services.admin.user_is_admin(sender_user).await
32 {
33 warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
34 return Err!(Request(Forbidden("Invites are not allowed on this server.")));
35 }
36
37 Ok(())
38}
39
40pub(crate) async fn is_self_redaction(
41 services: &Services,
42 user_id: &UserId,
43 event_id: &EventId,
44) -> bool {
45 services
46 .timeline
47 .get_pdu(event_id)
48 .await
49 .is_ok_and(|target| target.sender() == user_id)
50}
51
52pub(crate) fn txnid_unsigned(txn_id: &TransactionId) -> BTreeMap<String, JsonValue> {
56 BTreeMap::from([("transaction_id".to_owned(), txn_id.as_str().into())])
57}
58
59pub(crate) fn txnid_event_id(response: &[u8]) -> Result<OwnedEventId> {
64 str_from_bytes(response)
65 .ok()
66 .and_then(|event_id| event_id.try_into().ok())
67 .ok_or_else(|| err!(Database("Invalid event_id in txn_id data: {response:?}.")))
68}
69
70pub(crate) async fn may_set_profile_field<T>(
77 services: &Services,
78 body: &Ruma<T>,
79 field: &ProfileFieldName,
80 is_admin: impl AsyncFnOnce() -> bool,
81) -> bool
82where
83 T: Sync,
84{
85 let enabled = match field {
86 | ProfileFieldName::DisplayName => services.config.profile.enable_set_displayname,
87 | ProfileFieldName::AvatarUrl => services.config.profile.enable_set_avatar_url,
88 | _ => true,
89 };
90
91 enabled || body.appservice_info.is_some() || is_admin().await
92}
93
94pub(crate) async fn sender_is_guest<T>(services: &Services, body: &Ruma<T>) -> bool
99where
100 T: Sync,
101{
102 body.appservice_info.is_none() && services.users.is_guest(body.sender_user()).await
103}
104
105pub(crate) fn sync_deadline(services: &Services, timeout: Option<Duration>) -> Instant {
110 let config = &services.config;
111 let timeout = timeout
112 .and_then(millis)
113 .unwrap_or(config.client.sync.timeout_default)
114 .max(config.client.sync.timeout_min)
115 .min(config.client.sync.timeout_max);
116
117 poll_deadline(timeout)
118}
119
120pub(crate) fn poll_deadline(timeout: u64) -> Instant {
124 Instant::now()
125 .checked_add(Duration::from_millis(timeout))
126 .expect("configuration must limit maximum timeout")
127}
128
129pub(crate) async fn bind_new_account_email(
135 services: &Services,
136 user_id: &UserId,
137 email_canon: &str,
138) -> Result {
139 let now = MilliSecondsSinceUnixEpoch::now();
140 let bound = services
141 .threepid
142 .bind_unclaimed(user_id, email_canon, Medium::Email, now)
143 .await?;
144
145 if !bound {
146 warn!(%user_id, "Skipping registration email binding: address bound to another user");
147 }
148
149 Ok(())
150}
151
152pub(crate) fn check_email_domain(services: &Services, email_canon: &str) -> Result {
157 services.sendmail.check_address(email_canon)?;
158
159 let (_, domain) = email_canon
160 .rsplit_once('@')
161 .ok_or_else(|| err!(Request(InvalidParam("Email address must contain a domain"))))?;
162
163 if services
164 .config
165 .smtp
166 .forbidden_email_domains
167 .is_match(domain)
168 {
169 return Err!(Request(ThreepidDenied("Email domain is not allowed")));
170 }
171
172 Ok(())
173}