1use std::{borrow::Cow, fmt, str::FromStr};
4
5use gettextrs::gettext;
6use gtk::{glib, prelude::*};
7use matrix_sdk::{
8 AuthSession, Client, ClientBuildError, SessionMeta, SessionTokens,
9 authentication::{
10 matrix::MatrixSession,
11 oauth::{OAuthSession, UserSession},
12 },
13 config::RequestConfig,
14 deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
15 encryption::{BackupDownloadStrategy, EncryptionSettings},
16};
17use ruma::{
18 EventId, IdParseError, MatrixToUri, MatrixUri, MatrixUriError, MilliSecondsSinceUnixEpoch,
19 OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId,
20 RoomId, RoomOrAliasId, UserId,
21 events::{AnyStrippedStateEvent, AnySyncTimelineEvent},
22 html::{
23 Children, Html, NodeRef, StrTendril,
24 matrix::{AnchorUri, MatrixElement},
25 },
26 matrix_uri::MatrixId,
27 serde::Raw,
28};
29use thiserror::Error;
30use tracing::error;
31
32pub(crate) mod ext_traits;
33mod media_message;
34
35pub(crate) use self::media_message::*;
36use crate::{
37 components::{AvatarImageSafetySetting, Pill},
38 prelude::*,
39 secret::StoredSession,
40 session::model::Room,
41};
42
43#[derive(Debug, Default, Clone, Copy)]
45#[allow(clippy::struct_excessive_bools)]
46pub(crate) struct PasswordValidity {
47 pub(crate) has_lowercase: bool,
49 pub(crate) has_uppercase: bool,
51 pub(crate) has_number: bool,
53 pub(crate) has_symbol: bool,
55 pub(crate) has_length: bool,
57 pub(crate) progress: u32,
61}
62
63impl PasswordValidity {
64 pub fn new() -> Self {
65 Self::default()
66 }
67}
68
69pub(crate) fn validate_password(password: &str) -> PasswordValidity {
76 let mut validity = PasswordValidity::new();
77
78 for char in password.chars() {
79 if char.is_numeric() {
80 validity.has_number = true;
81 } else if char.is_lowercase() {
82 validity.has_lowercase = true;
83 } else if char.is_uppercase() {
84 validity.has_uppercase = true;
85 } else {
86 validity.has_symbol = true;
87 }
88 }
89
90 validity.has_length = password.len() >= 8;
91
92 let mut passed = 0;
93 if validity.has_number {
94 passed += 1;
95 }
96 if validity.has_lowercase {
97 passed += 1;
98 }
99 if validity.has_uppercase {
100 passed += 1;
101 }
102 if validity.has_symbol {
103 passed += 1;
104 }
105 if validity.has_length {
106 passed += 1;
107 }
108 validity.progress = passed * 100 / 5;
109
110 validity
111}
112
113#[derive(Debug, Clone)]
115pub(crate) enum AnySyncOrStrippedTimelineEvent {
116 Sync(Box<AnySyncTimelineEvent>),
118 Stripped(Box<AnyStrippedStateEvent>),
120}
121
122impl AnySyncOrStrippedTimelineEvent {
123 pub(crate) fn from_raw(
125 raw: &RawAnySyncOrStrippedTimelineEvent,
126 ) -> Result<Self, serde_json::Error> {
127 let ev = match raw {
128 RawAnySyncOrStrippedTimelineEvent::Sync(ev) => Self::Sync(ev.deserialize()?.into()),
129 RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => {
130 Self::Stripped(ev.deserialize()?.into())
131 }
132 };
133
134 Ok(ev)
135 }
136
137 pub(crate) fn sender(&self) -> &UserId {
139 match self {
140 AnySyncOrStrippedTimelineEvent::Sync(ev) => ev.sender(),
141 AnySyncOrStrippedTimelineEvent::Stripped(ev) => ev.sender(),
142 }
143 }
144
145 pub(crate) fn event_id(&self) -> Option<&EventId> {
147 match self {
148 AnySyncOrStrippedTimelineEvent::Sync(ev) => Some(ev.event_id()),
149 AnySyncOrStrippedTimelineEvent::Stripped(_) => None,
150 }
151 }
152}
153
154#[derive(Error, Debug)]
156pub(crate) enum ClientSetupError {
157 #[error(transparent)]
159 Client(#[from] ClientBuildError),
160 #[error(transparent)]
162 Sdk(#[from] matrix_sdk::Error),
163 #[error("Could not generate unique session ID")]
165 NoSessionId,
166 #[error("Could not access session tokens")]
168 NoSessionTokens,
169}
170
171impl UserFacingError for ClientSetupError {
172 fn to_user_facing(&self) -> String {
173 match self {
174 Self::Client(err) => err.to_user_facing(),
175 Self::Sdk(err) => err.to_user_facing(),
176 Self::NoSessionId => gettext("Could not generate unique session ID"),
177 Self::NoSessionTokens => gettext("Could not access the session tokens"),
178 }
179 }
180}
181
182pub(crate) async fn client_with_stored_session(
184 session: StoredSession,
185 tokens: SessionTokens,
186) -> Result<Client, ClientSetupError> {
187 let has_refresh_token = tokens.refresh_token.is_some();
188 let data_path = session.data_path();
189 let cache_path = session.cache_path();
190
191 let StoredSession {
192 homeserver,
193 user_id,
194 device_id,
195 passphrase,
196 client_id,
197 ..
198 } = session;
199
200 let meta = SessionMeta { user_id, device_id };
201 let session_data: AuthSession = if let Some(client_id) = client_id {
202 OAuthSession {
203 user: UserSession { meta, tokens },
204 client_id,
205 }
206 .into()
207 } else {
208 MatrixSession { meta, tokens }.into()
209 };
210
211 let encryption_settings = EncryptionSettings {
212 auto_enable_cross_signing: true,
213 backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
214 auto_enable_backups: true,
215 };
216
217 let mut client_builder = Client::builder()
218 .homeserver_url(homeserver)
219 .sqlite_store_with_cache_path(data_path, cache_path, Some(&passphrase))
220 .request_config(RequestConfig::new().retry_limit(2).force_auth())
224 .with_encryption_settings(encryption_settings);
225
226 if has_refresh_token {
227 client_builder = client_builder.handle_refresh_tokens();
228 }
229
230 let client = client_builder.build().await?;
231
232 client.restore_session(session_data).await?;
233
234 Ok(client)
235}
236
237pub(crate) fn find_html_mentions(html: &str, room: &Room) -> Vec<(Pill, StrTendril)> {
241 let mut mentions = Vec::new();
242 let html = Html::parse(html);
243
244 append_children_mentions(&mut mentions, html.children(), room);
245
246 mentions
247}
248
249fn append_children_mentions(
251 mentions: &mut Vec<(Pill, StrTendril)>,
252 children: Children,
253 room: &Room,
254) {
255 for node in children {
256 if let Some(mention) = node_as_mention(&node, room) {
257 mentions.push(mention);
258 continue;
259 }
260
261 append_children_mentions(mentions, node.children(), room);
262 }
263}
264
265fn node_as_mention(node: &NodeRef, room: &Room) -> Option<(Pill, StrTendril)> {
269 let MatrixElement::A(anchor) = node.as_element()?.to_matrix().element else {
271 return None;
272 };
273
274 let id = MatrixIdUri::try_from(anchor.href?).ok()?;
276
277 let child = node.children().next()?;
279
280 if child.next_sibling().is_some() {
281 return None;
282 }
283
284 let content = child.as_text()?.borrow().clone();
285 let pill = id.into_pill(room)?;
286
287 Some((pill, content))
288}
289
290pub(crate) const AT_ROOM: &str = "@room";
292
293pub(crate) fn find_at_room(s: &str) -> Option<usize> {
300 for (pos, _) in s.match_indices(AT_ROOM) {
301 let is_at_word_start = pos == 0 || s[..pos].ends_with(char_is_ascii_word_boundary);
302 if !is_at_word_start {
303 continue;
304 }
305
306 let pos_after_match = pos + 5;
307 let is_at_word_end = pos_after_match == s.len()
308 || s[pos_after_match..].starts_with(char_is_ascii_word_boundary);
309 if is_at_word_end {
310 return Some(pos);
311 }
312 }
313
314 None
315}
316
317fn char_is_ascii_word_boundary(c: char) -> bool {
322 !c.is_ascii_alphanumeric() && c != '_'
323}
324
325pub(crate) fn raw_eq<T, U>(lhs: Option<&Raw<T>>, rhs: Option<&Raw<U>>) -> bool {
327 let Some(lhs) = lhs else {
328 return rhs.is_none();
330 };
331 let Some(rhs) = rhs else {
332 return false;
334 };
335
336 lhs.json().get() == rhs.json().get()
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
341pub(crate) enum MatrixIdUri {
342 Room(MatrixRoomIdUri),
344 User(OwnedUserId),
346 Event(MatrixEventIdUri),
348}
349
350impl MatrixIdUri {
351 fn try_from_parts(id: MatrixId, via: &[OwnedServerName]) -> Result<Self, ()> {
353 let uri = match id {
354 MatrixId::Room(room_id) => Self::Room(MatrixRoomIdUri {
355 id: room_id.into(),
356 via: via.to_owned(),
357 }),
358 MatrixId::RoomAlias(room_alias) => Self::Room(MatrixRoomIdUri {
359 id: room_alias.into(),
360 via: via.to_owned(),
361 }),
362 MatrixId::User(user_id) => Self::User(user_id),
363 MatrixId::Event(room_id, event_id) => Self::Event(MatrixEventIdUri {
364 event_id,
365 room_uri: MatrixRoomIdUri {
366 id: room_id,
367 via: via.to_owned(),
368 },
369 }),
370 _ => return Err(()),
371 };
372
373 Ok(uri)
374 }
375
376 pub(crate) fn parse(s: &str) -> Result<Self, MatrixIdUriParseError> {
378 if let Ok(uri) = MatrixToUri::parse(s) {
379 return uri.try_into();
380 }
381
382 MatrixUri::parse(s)?.try_into()
383 }
384
385 pub(crate) fn into_pill(self, room: &Room) -> Option<Pill> {
387 match self {
388 Self::Room(room_uri) => {
389 let session = room.session()?;
390
391 let pill =
392 if let Some(uri_room) = session.room_list().get_by_identifier(&room_uri.id) {
393 Pill::new(&uri_room, AvatarImageSafetySetting::None, None)
396 } else {
397 Pill::new(
398 &session.remote_cache().room(room_uri),
399 AvatarImageSafetySetting::MediaPreviews,
400 Some(room.clone()),
401 )
402 };
403
404 Some(pill)
405 }
406 Self::User(user_id) => {
407 let user = room.get_or_create_members().get_or_create(user_id);
410
411 Some(Pill::new(&user, AvatarImageSafetySetting::None, None))
413 }
414 Self::Event(_) => None,
415 }
416 }
417
418 pub(crate) fn as_matrix_uri(&self) -> MatrixUri {
420 match self {
421 MatrixIdUri::Room(room_uri) => match <&RoomId>::try_from(&*room_uri.id) {
422 Ok(room_id) => room_id.matrix_uri_via(room_uri.via.clone(), false),
423 Err(room_alias) => room_alias.matrix_uri(false),
424 },
425 MatrixIdUri::User(user_id) => user_id.matrix_uri(false),
426 MatrixIdUri::Event(event_uri) => {
427 let room_id = <&RoomId>::try_from(&*event_uri.room_uri.id)
428 .expect("room alias should not be used to construct event URI");
429
430 room_id.matrix_event_uri_via(
431 event_uri.event_id.clone(),
432 event_uri.room_uri.via.clone(),
433 )
434 }
435 }
436 }
437}
438
439impl fmt::Display for MatrixIdUri {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 self.as_matrix_uri().fmt(f)
442 }
443}
444
445impl TryFrom<&MatrixUri> for MatrixIdUri {
446 type Error = MatrixIdUriParseError;
447
448 fn try_from(uri: &MatrixUri) -> Result<Self, Self::Error> {
449 Self::try_from_parts(uri.id().clone(), uri.via())
451 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
452 }
453}
454
455impl TryFrom<MatrixUri> for MatrixIdUri {
456 type Error = MatrixIdUriParseError;
457
458 fn try_from(uri: MatrixUri) -> Result<Self, Self::Error> {
459 Self::try_from(&uri)
460 }
461}
462
463impl TryFrom<&MatrixToUri> for MatrixIdUri {
464 type Error = MatrixIdUriParseError;
465
466 fn try_from(uri: &MatrixToUri) -> Result<Self, Self::Error> {
467 Self::try_from_parts(uri.id().clone(), uri.via())
468 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
469 }
470}
471
472impl TryFrom<MatrixToUri> for MatrixIdUri {
473 type Error = MatrixIdUriParseError;
474
475 fn try_from(uri: MatrixToUri) -> Result<Self, Self::Error> {
476 Self::try_from(&uri)
477 }
478}
479
480impl FromStr for MatrixIdUri {
481 type Err = MatrixIdUriParseError;
482
483 fn from_str(s: &str) -> Result<Self, Self::Err> {
484 Self::parse(s)
485 }
486}
487
488impl TryFrom<&str> for MatrixIdUri {
489 type Error = MatrixIdUriParseError;
490
491 fn try_from(s: &str) -> Result<Self, Self::Error> {
492 Self::parse(s)
493 }
494}
495
496impl TryFrom<&AnchorUri> for MatrixIdUri {
497 type Error = MatrixIdUriParseError;
498
499 fn try_from(value: &AnchorUri) -> Result<Self, Self::Error> {
500 match value {
501 AnchorUri::Matrix(uri) => MatrixIdUri::try_from(uri),
502 AnchorUri::MatrixTo(uri) => MatrixIdUri::try_from(uri),
503 _ => Err(IdParseError::InvalidMatrixUri(MatrixUriError::WrongScheme).into()),
505 }
506 }
507}
508
509impl TryFrom<AnchorUri> for MatrixIdUri {
510 type Error = MatrixIdUriParseError;
511
512 fn try_from(value: AnchorUri) -> Result<Self, Self::Error> {
513 Self::try_from(&value)
514 }
515}
516
517impl StaticVariantType for MatrixIdUri {
518 fn static_variant_type() -> Cow<'static, glib::VariantTy> {
519 String::static_variant_type()
520 }
521}
522
523impl ToVariant for MatrixIdUri {
524 fn to_variant(&self) -> glib::Variant {
525 self.to_string().to_variant()
526 }
527}
528
529impl FromVariant for MatrixIdUri {
530 fn from_variant(variant: &glib::Variant) -> Option<Self> {
531 Self::parse(&variant.get::<String>()?).ok()
532 }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq)]
537pub(crate) struct MatrixRoomIdUri {
538 pub(crate) id: OwnedRoomOrAliasId,
540 pub(crate) via: Vec<OwnedServerName>,
542}
543
544impl MatrixRoomIdUri {
545 pub(crate) fn parse(s: &str) -> Option<MatrixRoomIdUri> {
547 MatrixIdUri::parse(s)
548 .ok()
549 .and_then(|uri| match uri {
550 MatrixIdUri::Room(room_uri) => Some(room_uri),
551 _ => None,
552 })
553 .or_else(|| RoomOrAliasId::parse(s).ok().map(Into::into))
554 }
555}
556
557impl From<OwnedRoomOrAliasId> for MatrixRoomIdUri {
558 fn from(id: OwnedRoomOrAliasId) -> Self {
559 Self {
560 id,
561 via: Vec::new(),
562 }
563 }
564}
565
566impl From<OwnedRoomId> for MatrixRoomIdUri {
567 fn from(value: OwnedRoomId) -> Self {
568 OwnedRoomOrAliasId::from(value).into()
569 }
570}
571
572impl From<OwnedRoomAliasId> for MatrixRoomIdUri {
573 fn from(value: OwnedRoomAliasId) -> Self {
574 OwnedRoomOrAliasId::from(value).into()
575 }
576}
577
578impl From<&MatrixRoomIdUri> for MatrixUri {
579 fn from(value: &MatrixRoomIdUri) -> Self {
580 match <&RoomId>::try_from(&*value.id) {
581 Ok(room_id) => room_id.matrix_uri_via(value.via.clone(), false),
582 Err(alias) => alias.matrix_uri(false),
583 }
584 }
585}
586
587#[derive(Debug, Clone, PartialEq, Eq)]
589pub(crate) struct MatrixEventIdUri {
590 pub event_id: OwnedEventId,
592 pub room_uri: MatrixRoomIdUri,
594}
595
596#[derive(Debug, Clone, Error)]
598pub(crate) enum MatrixIdUriParseError {
599 #[error(transparent)]
601 InvalidUri(#[from] IdParseError),
602 #[error("unsupported Matrix ID: {0:?}")]
604 UnsupportedId(MatrixId),
605}
606
607pub(crate) fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> glib::DateTime {
609 seconds_since_unix_epoch_to_date(ts.as_secs().into())
610}
611
612pub(crate) fn seconds_since_unix_epoch_to_date(secs: i64) -> glib::DateTime {
614 glib::DateTime::from_unix_utc(secs)
615 .and_then(|date| date.to_local())
616 .expect("constructing GDateTime from timestamp should work")
617}