Skip to main content

tuwunel_api_client/account_data/
mod.rs

1mod delete_global_account_data;
2mod delete_room_account_data;
3mod get_global_account_data;
4mod get_room_account_data;
5mod set_global_account_data;
6mod set_room_account_data;
7#[cfg(test)]
8mod tests;
9
10use ruma::{
11	RoomId, UserId,
12	events::{GlobalAccountDataEventType, RoomAccountDataEventType},
13	serde::Raw,
14};
15use serde::Deserialize;
16use serde_json::{Value as JsonValue, json, value::RawValue as RawJsonValue};
17use tuwunel_core::{Err, Result, err};
18use tuwunel_service::{Services, appservice::RegistrationInfo};
19
20pub(crate) use self::{
21	delete_global_account_data::delete_global_account_data_route,
22	delete_room_account_data::delete_room_account_data_route,
23	get_global_account_data::get_global_account_data_route,
24	get_room_account_data::get_room_account_data_route,
25	set_global_account_data::set_global_account_data_route,
26	set_room_account_data::set_room_account_data_route,
27};
28
29fn assert_account_data_owner(
30	sender_user: &UserId,
31	user_id: &UserId,
32	appservice_info: Option<&RegistrationInfo>,
33	message: &str,
34) -> Result {
35	if sender_user != user_id
36		&& !appservice_info.is_some_and(|registration| registration.is_user_match(user_id))
37	{
38		return Err!(Request(Forbidden("{message}")));
39	}
40
41	Ok(())
42}
43
44async fn set_account_data(
45	services: &Services,
46	room_id: Option<&RoomId>,
47	sender_user: &UserId,
48	event_type_s: &str,
49	data: &RawJsonValue,
50) -> Result {
51	validate_event_type(event_type_s)?;
52
53	let data: serde_json::Value = serde_json::from_str(data.get())
54		.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
55
56	services
57		.account_data
58		.update(
59			room_id,
60			sender_user,
61			event_type_s.into(),
62			&json!({
63				"type": event_type_s,
64				"content": data,
65			}),
66		)
67		.await
68}
69
70fn validate_event_type(event_type: &str) -> Result {
71	if event_type == RoomAccountDataEventType::FullyRead.to_cow_str() {
72		return Err!(Request(BadJson(
73			"This endpoint cannot be used for marking a room as fully read (setting \
74			 m.fully_read)"
75		)));
76	}
77
78	if event_type == GlobalAccountDataEventType::PushRules.to_cow_str() {
79		return Err!(Request(BadJson(
80			"This endpoint cannot be used for setting/configuring push rules."
81		)));
82	}
83
84	Ok(())
85}
86
87/// MSC3391: tombstoned account data carries `content: {}`. Sync delta
88/// surfaces the empty event so clients can apply the deletion; everywhere
89/// else (GET, initial sync) treats it as not-present.
90fn is_empty_content<T>(content: &Raw<T>) -> bool { is_empty_object_json(content.json()) }
91
92/// Equivalent test against a stored account-data event (`{type, content}`)
93/// rather than the bare `content` payload. Used by sync filters.
94pub(crate) fn is_empty_account_data_event<T>(event: &Raw<T>) -> bool {
95	#[derive(Deserialize)]
96	struct ContentOnly<'a> {
97		#[serde(borrow)]
98		content: &'a RawJsonValue,
99	}
100
101	serde_json::from_str::<ContentOnly<'_>>(event.json().get())
102		.is_ok_and(|c| is_empty_object_json(c.content))
103}
104
105fn is_empty_object_json(s: &RawJsonValue) -> bool {
106	serde_json::from_str::<JsonValue>(s.get())
107		.ok()
108		.and_then(|v| v.as_object().map(serde_json::Map::is_empty))
109		.unwrap_or(false)
110}