1use rusqlite::Connection;
5
6use crate::error::MatrixError;
7use crate::store::Membership;
8
9pub fn whoami_response(mxid: &str, device_id: &str) -> serde_json::Value {
19 serde_json::json!({ "user_id": mxid, "device_id": device_id, "is_guest": false })
20}
21
22
23pub fn require_member(membership: Option<Membership>) -> Result<(), MatrixError> {
30 match membership {
31 Some(Membership::Join) => Ok(()),
32 _ => Err(MatrixError::forbidden("not a member of this room")),
33 }
34}
35
36
37pub fn require_member_or_invited(membership: Option<Membership>) -> Result<(), MatrixError> {
40 match membership {
41 Some(Membership::Join) | Some(Membership::Invite) => Ok(()),
42 _ => Err(MatrixError::forbidden("not a member or invitee of this room")),
43 }
44}
45
46
47pub fn check_caller_owns_user_id(path_user_id: &str, caller_mxid: &str) -> Result<(), MatrixError> {
51 if path_user_id == caller_mxid {
52 Ok(())
53 } else {
54 Err(MatrixError::forbidden("userId must be the caller's own mxid"))
55 }
56}
57
58
59pub fn check_account_data_type_allowed(event_type: &str) -> Result<(), MatrixError> {
70 if event_type == "m.fully_read" {
71 Err(MatrixError::managed_account_data_type(
72 "m.fully_read is managed through POST .../read_markers, not raw account data",
73 ))
74 } else {
75 Ok(())
76 }
77}
78
79
80pub fn validate_account_data_content(value: &serde_json::Value) -> Result<String, MatrixError> {
85 if !value.is_object() {
86 return Err(MatrixError::bad_json("account data content must be a JSON object"));
87 }
88 let serialized = value.to_string();
89 if serialized.len() > crate::store::MATRIX_EVENT_CONTENT_MAX_BYTES {
90 return Err(MatrixError::invalid_param("account data content too large"));
91 }
92 Ok(serialized)
93}
94
95
96pub const MAX_TAG_NAME_BYTES: usize = 255;
101
102pub const MAX_TAGS_PER_ROOM: usize = 100;
103
104
105#[derive(serde::Deserialize, Default)]
106pub struct TagBody {
107 #[serde(default)]
108 pub order: Option<f64>,
109}
110
111
112pub fn validate_tag_name(tag: &str) -> Result<(), MatrixError> {
113 if tag.is_empty() || tag.len() > MAX_TAG_NAME_BYTES {
114 Err(MatrixError::invalid_param("tag name must be 1-255 bytes"))
115 } else {
116 Ok(())
117 }
118}
119
120
121pub fn validate_tag_order(order: Option<f64>) -> Result<(), MatrixError> {
123 match order {
124 Some(v) if !(0.0..=1.0).contains(&v) => Err(MatrixError::invalid_param("tag order must be between 0 and 1")),
125 _ => Ok(()),
126 }
127}
128
129
130pub fn read_tags(conn: &Connection, user_id: i64, room_id: &str) -> Result<serde_json::Map<String, serde_json::Value>, MatrixError> {
133 match crate::store::get_account_data(conn, user_id, room_id, "m.tag")? {
134 Some(row) => {
135 let content: serde_json::Value = serde_json::from_str(&row.content)?;
136 Ok(content.get("tags").and_then(|v| v.as_object()).cloned().unwrap_or_default())
137 }
138 None => Ok(serde_json::Map::new()),
139 }
140}
141
142
143pub fn write_tags(conn: &mut Connection, user_id: i64, room_id: &str, tags: serde_json::Map<String, serde_json::Value>) -> Result<(), MatrixError> {
144 let content = serde_json::json!({ "tags": tags }).to_string();
145 crate::store::upsert_account_data(conn, user_id, room_id, "m.tag", &content)?;
146 Ok(())
147}
148
149
150pub fn apply_tag_put(conn: &mut Connection, user_id: i64, room_id: &str, tag: &str, order: Option<f64>) -> Result<(), MatrixError> {
157 validate_tag_name(tag)?;
158 validate_tag_order(order)?;
159
160 let mut tags = read_tags(conn, user_id, room_id)?;
161 if !tags.contains_key(tag) && tags.len() >= MAX_TAGS_PER_ROOM {
162 return Err(MatrixError::invalid_param("too many tags on this room"));
163 }
164 let mut entry = serde_json::Map::new();
165 if let Some(order) = order {
166 entry.insert("order".to_string(), serde_json::json!(order));
167 }
168 tags.insert(tag.to_string(), serde_json::Value::Object(entry));
169 write_tags(conn, user_id, room_id, tags)
170}
171
172
173pub fn apply_tag_delete(conn: &mut Connection, user_id: i64, room_id: &str, tag: &str) -> Result<(), MatrixError> {
176 let mut tags = read_tags(conn, user_id, room_id)?;
177 tags.remove(tag);
178 write_tags(conn, user_id, room_id, tags)
179}
180
181
182pub const FILTER_MAX_BYTES: usize = 64 * 1024;
188
189
190pub fn validate_filter_definition(value: &serde_json::Value) -> Result<String, MatrixError> {
194 if !value.is_object() {
195 return Err(MatrixError::bad_json("filter must be a JSON object"));
196 }
197 let serialized = value.to_string();
198 if serialized.len() > FILTER_MAX_BYTES {
199 return Err(MatrixError::invalid_param("filter definition too large"));
200 }
201 Ok(serialized)
202}
203
204
205pub const MAX_SEARCH_TERM_LEN: usize = 64;
213
214
215pub const DEFAULT_DIRECTORY_RESULTS: usize = 10;
218
219
220pub const MAX_DIRECTORY_RESULTS: usize = 50;
224
225
226#[derive(serde::Deserialize)]
227pub struct UserDirectorySearchRequest {
228 pub search_term: String,
229 #[serde(default)]
230 pub limit: Option<i64>,
231}
232
233
234#[derive(serde::Serialize)]
235pub struct UserDirectoryResult {
236 pub user_id: String,
237 pub display_name: String,
238}
239
240
241#[derive(serde::Serialize)]
242pub struct UserDirectorySearchResponse {
243 pub results: Vec<UserDirectoryResult>,
244 pub limited: bool,
245}
246
247
248pub struct DirectoryMatch {
253 pub user_id: i64,
254 pub public_id: String,
255 pub label: String,
256}
257
258
259pub const DEFAULT_PUBLIC_ROOMS_LIMIT: i64 = 20;
264
265pub const MAX_PUBLIC_ROOMS_LIMIT: i64 = 100;
266
267
268pub fn clamp_public_rooms_limit(limit: Option<i64>) -> usize {
269 limit.unwrap_or(DEFAULT_PUBLIC_ROOMS_LIMIT).clamp(1, MAX_PUBLIC_ROOMS_LIMIT) as usize
270}
271
272
273#[derive(serde::Deserialize, Default)]
274pub struct PublicRoomsQuery {
275 pub limit: Option<i64>,
276 pub since: Option<String>,
277}
278
279
280#[derive(serde::Deserialize, Default)]
281pub struct PublicRoomsFilter {
282 #[serde(default)]
283 pub generic_search_term: Option<String>,
284}
285
286
287#[derive(serde::Deserialize, Default)]
288pub struct PublicRoomsRequestBody {
289 #[serde(default)]
290 pub limit: Option<i64>,
291 #[serde(default)]
292 pub since: Option<String>,
293 #[serde(default)]
294 pub filter: PublicRoomsFilter,
295}
296
297
298pub fn public_room_json(room: &crate::store::PublicRoomSummary) -> serde_json::Value {
303 let mut value = serde_json::json!({
304 "room_id": room.room_id,
305 "num_joined_members": room.num_joined_members,
306 "world_readable": room.world_readable,
307 "guest_can_join": false,
308 "join_rule": "public",
309 });
310 if let Some(name) = &room.name {
311 value["name"] = serde_json::Value::String(name.clone());
312 }
313 if let Some(topic) = &room.topic {
314 value["topic"] = serde_json::Value::String(topic.clone());
315 }
316 value
317}
318
319
320pub fn list_public_rooms(
322 conn: &Connection,
323 since: Option<&str>,
324 limit: Option<i64>,
325 search_term: Option<&str>,
326) -> Result<serde_json::Value, MatrixError> {
327 let limit = clamp_public_rooms_limit(limit);
328 let (page, has_more, total) = crate::store::public_rooms_page(conn, since, limit, search_term)?;
329 let chunk: Vec<serde_json::Value> = page.iter().map(public_room_json).collect();
330 let mut response = serde_json::json!({ "chunk": chunk, "total_room_count_estimate": total });
331 if has_more {
332 if let Some(last) = page.last() {
333 response["next_batch"] = serde_json::Value::String(last.room_id.clone());
334 }
335 }
336 Ok(response)
337}
338
339pub fn search_users_by_localpart(conn: &Connection, query: &str, limit: usize) -> rusqlite::Result<Vec<String>> {
342 let limit = limit.clamp(1, 50) as i64;
343 let pattern = format!("%{query}%");
344 let mut stmt = conn.prepare(
345 "SELECT mxid FROM matrix_users WHERE mxid LIKE ?1 ORDER BY mxid LIMIT ?2",
346 )?;
347 let rows = stmt.query_map(rusqlite::params![pattern, limit], |row| row.get(0))?;
348 rows.collect()
349}