Skip to main content

tuwunel_api_client/
utils.rs

1use 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
52/// The unsigned fields carrying a client's transaction ID on the event it sent.
53///
54/// Every client read removes the ID for anyone but the sender.
55pub(crate) fn txnid_unsigned(txn_id: &TransactionId) -> BTreeMap<String, JsonValue> {
56	BTreeMap::from([("transaction_id".to_owned(), txn_id.as_str().into())])
57}
58
59/// Decodes the event ID a room transaction stored as its response.
60///
61/// Stored bytes that are not an event ID are corrupt data, reported as a
62/// database error.
63pub(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
70/// Whether the caller may change a profile field under
71/// `profile.enable_set_displayname` and `profile.enable_set_avatar_url`.
72///
73/// Fields without an option are always allowed. Appservices and server admins
74/// are exempt, matching Synapse's exemption for admins. `is_admin` is awaited
75/// only when the field's option is off and the caller is not an appservice.
76pub(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
94/// Whether the sender of a request is a guest.
95///
96/// A request on an appservice's own token is never a guest's, whichever user
97/// it acts as.
98pub(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
105/// When the long poll of a sync-like request gives up.
106///
107/// A request without a timeout takes the configured default, and every
108/// timeout is held between the configured minimum and maximum.
109pub(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
120/// When a long poll of `timeout` milliseconds starting now gives up.
121///
122/// The configured maximum keeps every timeout small enough to add to now.
123pub(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
129/// Bind a new account's canonical email address unless another account holds
130/// it.
131///
132/// An address held elsewhere is skipped with a warning rather than failing the
133/// registration that brought it.
134pub(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
152/// Rejects a canonical email address whose domain is forbidden for new bindings.
153///
154/// Callers canonicalize the address before applying this add-time policy;
155/// password reset and existing bindings do not use it.
156pub(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}