1use rusqlite::{params, Connection, OptionalExtension};
8
9use crate::error::MatrixError;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct MessengerSession {
14 pub session_id: String,
15 pub user_id: i64,
16 pub device_id: String,
17 pub nick: String,
18}
19
20pub fn normalize_nick(nick: &str) -> Result<String, MatrixError> {
21 let nick = nick.trim();
22 if nick.is_empty()
23 || nick.len() > 32
24 || !nick
25 .chars()
26 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
27 {
28 return Err(MatrixError::invalid_param(
29 "nick must be 1..=32 of [A-Za-z0-9_-]",
30 ));
31 }
32 Ok(nick.to_string())
33}
34
35pub(crate) fn legacy_session_id(user_id: i64) -> String {
38 format!("legacy-user-{user_id}")
39}
40
41pub(crate) fn nick_taken(
43 conn: &Connection,
44 nick: &str,
45 except_session_id: Option<&str>,
46) -> Result<bool, MatrixError> {
47 let except = except_session_id.unwrap_or("");
48 let taken = conn
49 .query_row(
50 "SELECT 1 FROM messenger_sessions WHERE LOWER(nick) = LOWER(?1) AND session_id != ?2",
51 params![nick, except],
52 |_| Ok(()),
53 )
54 .optional()?
55 .is_some();
56 Ok(taken)
57}
58
59pub fn set_nick(conn: &Connection, user_id: i64, nick: &str) -> Result<(), MatrixError> {
60 let nick = normalize_nick(nick)?;
61 let user_exists: bool = conn
62 .query_row(
63 "SELECT 1 FROM matrix_users WHERE user_id = ?1",
64 params![user_id],
65 |_| Ok(()),
66 )
67 .optional()?
68 .is_some();
69 if !user_exists {
70 return Err(MatrixError::not_found("unknown user"));
71 }
72 let current: Option<String> = conn
73 .query_row(
74 "SELECT session_id FROM messenger_sessions WHERE user_id = ?1 ORDER BY rowid LIMIT 1",
75 params![user_id],
76 |row| row.get(0),
77 )
78 .optional()?;
79 if nick_taken(conn, &nick, current.as_deref())? {
80 return Err(MatrixError::invalid_param("nick is taken"));
81 }
82 if let Some(session_id) = current {
83 conn.execute(
84 "UPDATE messenger_sessions SET nick = ?1 WHERE session_id = ?2",
85 params![nick, session_id],
86 )?;
87 } else {
88 let session_id = legacy_session_id(user_id);
89 conn.execute(
90 "INSERT INTO messenger_sessions (session_id, user_id, device_id, nick) VALUES (?1, ?2, '', ?3)",
91 params![session_id, user_id, nick],
92 )?;
93 }
94 Ok(())
95}
96
97pub fn user_nick(conn: &Connection, user_id: i64) -> rusqlite::Result<Option<String>> {
98 let nick: Option<String> = conn
99 .query_row(
100 "SELECT nick FROM messenger_sessions
101 WHERE user_id = ?1 AND nick != ''
102 ORDER BY rowid LIMIT 1",
103 params![user_id],
104 |row| row.get(0),
105 )
106 .optional()?;
107 if let Some(n) = nick.filter(|n| !n.is_empty()) {
108 return Ok(Some(n));
109 }
110 crate::identities::nick_of_user(conn, user_id)
111}
112
113pub fn effective_label(conn: &Connection, user_id: i64) -> rusqlite::Result<String> {
115 if let Some(nick) = user_nick(conn, user_id)? {
116 return Ok(nick);
117 }
118 let mxid: String = conn.query_row(
119 "SELECT mxid FROM matrix_users WHERE user_id = ?1",
120 params![user_id],
121 |row| row.get(0),
122 )?;
123 Ok(crate::store::public_id_from_mxid(&mxid)
124 .unwrap_or("")
125 .to_string())
126}
127
128pub fn require_nick(
129 conn: &Connection,
130 user_id: i64,
131 missing: &'static str,
132) -> Result<String, MatrixError> {
133 match user_nick(conn, user_id)? {
134 Some(nick) => Ok(nick),
135 None => Err(MatrixError::forbidden(missing)),
136 }
137}
138
139pub struct NickHit {
140 pub mxid: String,
141 pub nick: String,
142}
143
144pub fn search_nicks(
147 conn: &Connection,
148 term: &str,
149 limit: usize,
150) -> rusqlite::Result<(Vec<NickHit>, bool)> {
151 let escaped = term
152 .replace('\\', "\\\\")
153 .replace('%', "\\%")
154 .replace('_', "\\_");
155 let pattern = format!("%{escaped}%");
156 let mut stmt = conn.prepare(
157 "SELECT mxid, nick FROM (
158 SELECT u.mxid AS mxid, s.nick AS nick
159 FROM messenger_sessions s JOIN matrix_users u ON u.user_id = s.user_id
160 WHERE s.nick != ''
161 UNION
162 SELECT u.mxid, i.nick
163 FROM identities i JOIN matrix_users u ON u.user_id = i.id
164 )
165 WHERE LOWER(nick) LIKE LOWER(?1) ESCAPE '\\'
166 ORDER BY LOWER(nick)
167 LIMIT ?2",
168 )?;
169 let rows = stmt.query_map(params![pattern, (limit as i64) + 1], |row| {
170 Ok(NickHit {
171 mxid: row.get(0)?,
172 nick: row.get(1)?,
173 })
174 })?;
175 let mut hits = Vec::new();
176 for row in rows {
177 hits.push(row?);
178 }
179 let limited = hits.len() > limit;
180 hits.truncate(limit);
181 Ok((hits, limited))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::store::{create_matrix_schema, ensure_matrix_user};
188
189 const T0: &str = "2026-10-05T00:00:00+00:00";
190
191 fn conn() -> Connection {
192 let conn = Connection::open_in_memory().expect("memory");
193 create_matrix_schema(&conn).expect("schema");
194 conn
195 }
196
197 #[test]
198 fn set_nick_is_unique_and_gates_an_empty_user() {
199 let conn = conn();
200 ensure_matrix_user(&conn, 1, "alice000000000000000000000000a1", T0).expect("alice");
201 ensure_matrix_user(&conn, 2, "bob0000000000000000000000000b02", T0).expect("bob");
202 assert!(require_nick(&conn, 1, "choose a nick before creating a room").is_err());
203 set_nick(&conn, 1, "alice_nick").expect("set");
204 assert_eq!(effective_label(&conn, 1).expect("label"), "alice_nick");
205 let on_session: String = conn
206 .query_row(
207 "SELECT nick FROM messenger_sessions WHERE user_id = 1",
208 [],
209 |row| row.get(0),
210 )
211 .expect("session nick");
212 assert_eq!(on_session, "alice_nick");
213 let column: Option<String> = conn
214 .query_row(
215 "SELECT nick FROM matrix_users WHERE user_id = 1",
216 [],
217 |row| row.get(0),
218 )
219 .expect("column");
220 assert!(column.is_none());
221 assert!(set_nick(&conn, 2, "Alice_Nick").is_err());
222 assert!(set_nick(&conn, 1, "bad nick").is_err());
223 }
224}