matrix_sdk/utils/
mod.rs
1#[cfg(feature = "e2e-encryption")]
18use std::sync::{Arc, RwLock};
19
20#[cfg(feature = "e2e-encryption")]
21use futures_core::Stream;
22#[cfg(feature = "e2e-encryption")]
23use futures_util::StreamExt;
24#[cfg(feature = "markdown")]
25use ruma::events::room::message::FormattedBody;
26use ruma::{
27 events::{AnyMessageLikeEventContent, AnyStateEventContent},
28 serde::Raw,
29 RoomAliasId,
30};
31use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
32#[cfg(feature = "e2e-encryption")]
33use tokio::sync::broadcast;
34#[cfg(feature = "e2e-encryption")]
35use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream};
36
37#[cfg(feature = "local-server")]
38pub mod local_server;
39
40#[cfg(doc)]
41use crate::Room;
42
43#[cfg(feature = "e2e-encryption")]
49#[derive(Clone, Debug)]
50pub(crate) struct ChannelObservable<T: Clone + Send> {
51 value: Arc<RwLock<T>>,
52 channel: broadcast::Sender<T>,
53}
54
55#[cfg(feature = "e2e-encryption")]
56impl<T: Default + Clone + Send + 'static> Default for ChannelObservable<T> {
57 fn default() -> Self {
58 let value = Default::default();
59 Self::new(value)
60 }
61}
62
63#[cfg(feature = "e2e-encryption")]
64impl<T: 'static + Send + Clone> ChannelObservable<T> {
65 pub(crate) fn new(value: T) -> Self {
68 let channel = broadcast::Sender::new(100);
69 Self { value: RwLock::new(value).into(), channel }
70 }
71
72 pub(crate) fn subscribe(&self) -> impl Stream<Item = Result<T, BroadcastStreamRecvError>> {
77 let current_value = self.value.read().unwrap().to_owned();
78 let initial_stream = tokio_stream::once(Ok(current_value));
79 let broadcast_stream = BroadcastStream::new(self.channel.subscribe());
80
81 initial_stream.chain(broadcast_stream)
82 }
83
84 pub(crate) fn set(&self, new_value: T) -> T {
86 let old_value = {
87 let mut guard = self.value.write().unwrap();
88 std::mem::replace(&mut (*guard), new_value.clone())
89 };
90
91 let _ = self.channel.send(new_value);
93
94 old_value
95 }
96
97 pub(crate) fn get(&self) -> T {
99 self.value.read().unwrap().to_owned()
100 }
101}
102
103pub trait IntoRawMessageLikeEventContent {
105 #[doc(hidden)]
106 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent>;
107}
108
109impl IntoRawMessageLikeEventContent for Raw<AnyMessageLikeEventContent> {
110 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
111 self
112 }
113}
114
115impl IntoRawMessageLikeEventContent for &Raw<AnyMessageLikeEventContent> {
116 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
117 self.clone()
118 }
119}
120
121impl IntoRawMessageLikeEventContent for JsonValue {
122 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
123 (&self).into_raw_message_like_event_content()
124 }
125}
126
127impl IntoRawMessageLikeEventContent for &JsonValue {
128 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
129 Raw::new(self).expect("serde_json::Value never fails to serialize").cast()
130 }
131}
132
133impl IntoRawMessageLikeEventContent for Box<RawJsonValue> {
134 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
135 Raw::from_json(self)
136 }
137}
138
139impl IntoRawMessageLikeEventContent for &RawJsonValue {
140 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
141 self.to_owned().into_raw_message_like_event_content()
142 }
143}
144
145impl IntoRawMessageLikeEventContent for &Box<RawJsonValue> {
146 fn into_raw_message_like_event_content(self) -> Raw<AnyMessageLikeEventContent> {
147 self.clone().into_raw_message_like_event_content()
148 }
149}
150
151pub trait IntoRawStateEventContent {
153 #[doc(hidden)]
154 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent>;
155}
156
157impl IntoRawStateEventContent for Raw<AnyStateEventContent> {
158 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
159 self
160 }
161}
162
163impl IntoRawStateEventContent for &Raw<AnyStateEventContent> {
164 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
165 self.clone()
166 }
167}
168
169impl IntoRawStateEventContent for JsonValue {
170 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
171 (&self).into_raw_state_event_content()
172 }
173}
174
175impl IntoRawStateEventContent for &JsonValue {
176 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
177 Raw::new(self).expect("serde_json::Value never fails to serialize").cast()
178 }
179}
180
181impl IntoRawStateEventContent for Box<RawJsonValue> {
182 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
183 Raw::from_json(self)
184 }
185}
186
187impl IntoRawStateEventContent for &RawJsonValue {
188 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
189 self.to_owned().into_raw_state_event_content()
190 }
191}
192
193impl IntoRawStateEventContent for &Box<RawJsonValue> {
194 fn into_raw_state_event_content(self) -> Raw<AnyStateEventContent> {
195 self.clone().into_raw_state_event_content()
196 }
197}
198
199const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:{}\\";
200
201pub fn is_room_alias_format_valid(alias: String) -> bool {
207 let alias_parts: Vec<&str> = alias.split(':').collect();
208 if alias_parts.len() != 2 {
209 return false;
210 }
211
212 let local_part = alias_parts[0];
213 let has_valid_format = local_part.chars().skip(1).all(|c| {
214 c.is_ascii()
215 && !c.is_whitespace()
216 && !c.is_control()
217 && !INVALID_ROOM_ALIAS_NAME_CHARS.contains(c)
218 });
219
220 let is_lowercase = alias.to_lowercase() == alias;
221
222 has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok()
224}
225
226#[cfg(feature = "markdown")]
232pub fn formatted_body_from(
233 body: Option<&str>,
234 formatted_body: Option<FormattedBody>,
235) -> Option<FormattedBody> {
236 if formatted_body.is_some() {
237 formatted_body
238 } else {
239 body.and_then(FormattedBody::markdown)
240 }
241}
242
243#[cfg(test)]
244mod test {
245 #[cfg(feature = "markdown")]
246 use assert_matches2::{assert_let, assert_matches};
247 #[cfg(feature = "markdown")]
248 use ruma::events::room::message::FormattedBody;
249
250 #[cfg(feature = "markdown")]
251 use crate::utils::formatted_body_from;
252 use crate::utils::is_room_alias_format_valid;
253
254 #[cfg(feature = "e2e-encryption")]
255 #[test]
256 fn test_channel_observable_get_set() {
257 let observable = super::ChannelObservable::new(0);
258
259 assert_eq!(observable.get(), 0);
260 assert_eq!(observable.set(1), 0);
261 assert_eq!(observable.set(10), 1);
262 assert_eq!(observable.get(), 10);
263 }
264
265 #[test]
266 fn test_is_room_alias_format_valid_when_it_has_no_leading_hash_char_is_not_valid() {
267 assert!(!is_room_alias_format_valid("alias:domain.org".to_owned()))
268 }
269
270 #[test]
271 fn test_is_room_alias_format_valid_when_it_has_several_colon_chars_is_not_valid() {
272 assert!(!is_room_alias_format_valid("#alias:something:domain.org".to_owned()))
273 }
274
275 #[test]
276 fn test_is_room_alias_format_valid_when_it_has_no_colon_chars_is_not_valid() {
277 assert!(!is_room_alias_format_valid("#alias.domain.org".to_owned()))
278 }
279
280 #[test]
281 fn test_is_room_alias_format_valid_when_server_part_is_not_valid() {
282 assert!(!is_room_alias_format_valid("#alias:".to_owned()))
283 }
284
285 #[test]
286 fn test_is_room_alias_format_valid_when_name_part_has_whitespace_is_not_valid() {
287 assert!(!is_room_alias_format_valid("#alias with whitespace:domain.org".to_owned()))
288 }
289
290 #[test]
291 fn test_is_room_alias_format_valid_when_name_part_has_control_char_is_not_valid() {
292 assert!(!is_room_alias_format_valid("#alias\u{0009}:domain.org".to_owned()))
293 }
294
295 #[test]
296 fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() {
297 assert!(!is_room_alias_format_valid("#a#lias,{t\\est}:domain.org".to_owned()))
298 }
299
300 #[test]
301 fn test_is_room_alias_format_valid_when_name_part_is_not_lowercase_is_not_valid() {
302 assert!(!is_room_alias_format_valid("#Alias:domain.org".to_owned()))
303 }
304
305 #[test]
306 fn test_is_room_alias_format_valid_when_server_part_is_not_lowercase_is_not_valid() {
307 assert!(!is_room_alias_format_valid("#alias:Domain.org".to_owned()))
308 }
309
310 #[test]
311 fn test_is_room_alias_format_valid_when_has_valid_format() {
312 assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned()))
313 }
314
315 #[test]
316 #[cfg(feature = "markdown")]
317 fn test_formatted_body_from_nothing_returns_none() {
318 assert_matches!(formatted_body_from(None, None), None);
319 }
320
321 #[test]
322 #[cfg(feature = "markdown")]
323 fn test_formatted_body_from_only_formatted_body_returns_the_formatted_body() {
324 let formatted_body = FormattedBody::html(r"<h1>Hello!</h1>");
325
326 assert_let!(
327 Some(result_formatted_body) = formatted_body_from(None, Some(formatted_body.clone()))
328 );
329
330 assert_eq!(formatted_body.body, result_formatted_body.body);
331 assert_eq!(result_formatted_body.format, result_formatted_body.format);
332 }
333
334 #[test]
335 #[cfg(feature = "markdown")]
336 fn test_formatted_body_from_markdown_body_returns_a_processed_formatted_body() {
337 let markdown_body = Some(r"# Parsed");
338
339 assert_let!(Some(result_formatted_body) = formatted_body_from(markdown_body, None));
340
341 let expected_formatted_body = FormattedBody::html("<h1>Parsed</h1>\n".to_owned());
342 assert_eq!(expected_formatted_body.body, result_formatted_body.body);
343 assert_eq!(expected_formatted_body.format, result_formatted_body.format);
344 }
345
346 #[test]
347 #[cfg(feature = "markdown")]
348 fn test_formatted_body_from_body_and_formatted_body_returns_the_formatted_body() {
349 let markdown_body = Some(r"# Markdown");
350 let formatted_body = FormattedBody::html(r"<h1>HTML</h1>");
351
352 assert_let!(
353 Some(result_formatted_body) =
354 formatted_body_from(markdown_body, Some(formatted_body.clone()))
355 );
356
357 assert_eq!(formatted_body.body, result_formatted_body.body);
358 assert_eq!(formatted_body.format, result_formatted_body.format);
359 }
360}