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,
20 OwnedTransactionId, OwnedUserId, 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::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(Box::new(ev.deserialize()?))
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("Matrix client build error: {0}")]
159 Client(#[from] ClientBuildError),
160 #[error("Matrix client restoration error: {0}")]
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: false,
217 };
218
219 let mut client_builder = Client::builder()
220 .homeserver_url(homeserver)
221 .sqlite_store_with_cache_path(data_path, cache_path, Some(&passphrase))
222 .request_config(RequestConfig::new().retry_limit(2).force_auth())
226 .with_encryption_settings(encryption_settings);
227
228 if has_refresh_token {
229 client_builder = client_builder.handle_refresh_tokens();
230 }
231
232 let client = client_builder.build().await?;
233
234 client.restore_session(session_data).await?;
235
236 Ok(client)
237}
238
239pub(crate) fn find_html_mentions(html: &str, room: &Room) -> Vec<(Pill, StrTendril)> {
243 let mut mentions = Vec::new();
244 let html = Html::parse(html);
245
246 append_children_mentions(&mut mentions, html.children(), room);
247
248 mentions
249}
250
251fn append_children_mentions(
253 mentions: &mut Vec<(Pill, StrTendril)>,
254 children: Children,
255 room: &Room,
256) {
257 for node in children {
258 if let Some(mention) = node_as_mention(&node, room) {
259 mentions.push(mention);
260 continue;
261 }
262
263 append_children_mentions(mentions, node.children(), room);
264 }
265}
266
267fn node_as_mention(node: &NodeRef, room: &Room) -> Option<(Pill, StrTendril)> {
271 let MatrixElement::A(anchor) = node.as_element()?.to_matrix().element else {
273 return None;
274 };
275
276 let id = MatrixIdUri::try_from(anchor.href?).ok()?;
278
279 let child = node.children().next()?;
281
282 if child.next_sibling().is_some() {
283 return None;
284 }
285
286 let content = child.as_text()?.borrow().clone();
287 let pill = id.into_pill(room)?;
288
289 Some((pill, content))
290}
291
292pub(crate) const AT_ROOM: &str = "@room";
294
295pub(crate) fn find_at_room(s: &str) -> Option<usize> {
302 for (pos, _) in s.match_indices(AT_ROOM) {
303 let is_at_word_start = pos == 0 || s[..pos].ends_with(char_is_ascii_word_boundary);
304 if !is_at_word_start {
305 continue;
306 }
307
308 let pos_after_match = pos + 5;
309 let is_at_word_end = pos_after_match == s.len()
310 || s[pos_after_match..].starts_with(char_is_ascii_word_boundary);
311 if is_at_word_end {
312 return Some(pos);
313 }
314 }
315
316 None
317}
318
319fn char_is_ascii_word_boundary(c: char) -> bool {
324 !c.is_ascii_alphanumeric() && c != '_'
325}
326
327pub(crate) fn raw_eq<T, U>(lhs: Option<&Raw<T>>, rhs: Option<&Raw<U>>) -> bool {
329 let Some(lhs) = lhs else {
330 return rhs.is_none();
332 };
333 let Some(rhs) = rhs else {
334 return false;
336 };
337
338 lhs.json().get() == rhs.json().get()
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343pub(crate) enum MatrixIdUri {
344 Room(MatrixRoomIdUri),
346 User(OwnedUserId),
348 Event(MatrixEventIdUri),
350}
351
352impl MatrixIdUri {
353 fn try_from_parts(id: MatrixId, via: &[OwnedServerName]) -> Result<Self, ()> {
355 let uri = match id {
356 MatrixId::Room(room_id) => Self::Room(MatrixRoomIdUri {
357 id: room_id.into(),
358 via: via.to_owned(),
359 }),
360 MatrixId::RoomAlias(room_alias) => Self::Room(MatrixRoomIdUri {
361 id: room_alias.into(),
362 via: via.to_owned(),
363 }),
364 MatrixId::User(user_id) => Self::User(user_id),
365 MatrixId::Event(room_id, event_id) => Self::Event(MatrixEventIdUri {
366 event_id,
367 room_uri: MatrixRoomIdUri {
368 id: room_id,
369 via: via.to_owned(),
370 },
371 }),
372 _ => return Err(()),
373 };
374
375 Ok(uri)
376 }
377
378 pub(crate) fn parse(s: &str) -> Result<Self, MatrixIdUriParseError> {
380 if let Ok(uri) = MatrixToUri::parse(s) {
381 return uri.try_into();
382 }
383
384 MatrixUri::parse(s)?.try_into()
385 }
386
387 pub(crate) fn into_pill(self, room: &Room) -> Option<Pill> {
389 match self {
390 Self::Room(room_uri) => {
391 let session = room.session()?;
392
393 let pill =
394 if let Some(uri_room) = session.room_list().get_by_identifier(&room_uri.id) {
395 Pill::new(&uri_room, AvatarImageSafetySetting::None, None)
398 } else {
399 Pill::new(
400 &session.remote_cache().room(room_uri),
401 AvatarImageSafetySetting::MediaPreviews,
402 Some(room.clone()),
403 )
404 };
405
406 Some(pill)
407 }
408 Self::User(user_id) => {
409 let user = room.get_or_create_members().get_or_create(user_id);
412
413 Some(Pill::new(&user, AvatarImageSafetySetting::None, None))
415 }
416 Self::Event(_) => None,
417 }
418 }
419
420 pub(crate) fn as_matrix_uri(&self) -> MatrixUri {
422 match self {
423 MatrixIdUri::Room(room_uri) => match <&RoomId>::try_from(&*room_uri.id) {
424 Ok(room_id) => room_id.matrix_uri_via(room_uri.via.clone(), false),
425 Err(room_alias) => room_alias.matrix_uri(false),
426 },
427 MatrixIdUri::User(user_id) => user_id.matrix_uri(false),
428 MatrixIdUri::Event(event_uri) => {
429 let room_id = <&RoomId>::try_from(&*event_uri.room_uri.id)
430 .expect("room alias should not be used to construct event URI");
431
432 room_id.matrix_event_uri_via(
433 event_uri.event_id.clone(),
434 event_uri.room_uri.via.clone(),
435 )
436 }
437 }
438 }
439}
440
441impl fmt::Display for MatrixIdUri {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 self.as_matrix_uri().fmt(f)
444 }
445}
446
447impl TryFrom<&MatrixUri> for MatrixIdUri {
448 type Error = MatrixIdUriParseError;
449
450 fn try_from(uri: &MatrixUri) -> Result<Self, Self::Error> {
451 Self::try_from_parts(uri.id().clone(), uri.via())
453 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
454 }
455}
456
457impl TryFrom<MatrixUri> for MatrixIdUri {
458 type Error = MatrixIdUriParseError;
459
460 fn try_from(uri: MatrixUri) -> Result<Self, Self::Error> {
461 Self::try_from(&uri)
462 }
463}
464
465impl TryFrom<&MatrixToUri> for MatrixIdUri {
466 type Error = MatrixIdUriParseError;
467
468 fn try_from(uri: &MatrixToUri) -> Result<Self, Self::Error> {
469 Self::try_from_parts(uri.id().clone(), uri.via())
470 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
471 }
472}
473
474impl TryFrom<MatrixToUri> for MatrixIdUri {
475 type Error = MatrixIdUriParseError;
476
477 fn try_from(uri: MatrixToUri) -> Result<Self, Self::Error> {
478 Self::try_from(&uri)
479 }
480}
481
482impl FromStr for MatrixIdUri {
483 type Err = MatrixIdUriParseError;
484
485 fn from_str(s: &str) -> Result<Self, Self::Err> {
486 Self::parse(s)
487 }
488}
489
490impl TryFrom<&str> for MatrixIdUri {
491 type Error = MatrixIdUriParseError;
492
493 fn try_from(s: &str) -> Result<Self, Self::Error> {
494 Self::parse(s)
495 }
496}
497
498impl TryFrom<&AnchorUri> for MatrixIdUri {
499 type Error = MatrixIdUriParseError;
500
501 fn try_from(value: &AnchorUri) -> Result<Self, Self::Error> {
502 match value {
503 AnchorUri::Matrix(uri) => MatrixIdUri::try_from(uri),
504 AnchorUri::MatrixTo(uri) => MatrixIdUri::try_from(uri),
505 _ => Err(IdParseError::InvalidMatrixUri(MatrixUriError::WrongScheme).into()),
507 }
508 }
509}
510
511impl TryFrom<AnchorUri> for MatrixIdUri {
512 type Error = MatrixIdUriParseError;
513
514 fn try_from(value: AnchorUri) -> Result<Self, Self::Error> {
515 Self::try_from(&value)
516 }
517}
518
519impl StaticVariantType for MatrixIdUri {
520 fn static_variant_type() -> Cow<'static, glib::VariantTy> {
521 String::static_variant_type()
522 }
523}
524
525impl ToVariant for MatrixIdUri {
526 fn to_variant(&self) -> glib::Variant {
527 self.to_string().to_variant()
528 }
529}
530
531impl FromVariant for MatrixIdUri {
532 fn from_variant(variant: &glib::Variant) -> Option<Self> {
533 Self::parse(&variant.get::<String>()?).ok()
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
539pub(crate) struct MatrixRoomIdUri {
540 pub(crate) id: OwnedRoomOrAliasId,
542 pub(crate) via: Vec<OwnedServerName>,
544}
545
546impl MatrixRoomIdUri {
547 pub(crate) fn parse(s: &str) -> Option<MatrixRoomIdUri> {
549 MatrixIdUri::parse(s)
550 .ok()
551 .and_then(|uri| match uri {
552 MatrixIdUri::Room(room_uri) => Some(room_uri),
553 _ => None,
554 })
555 .or_else(|| RoomOrAliasId::parse(s).ok().map(Into::into))
556 }
557}
558
559impl From<OwnedRoomOrAliasId> for MatrixRoomIdUri {
560 fn from(id: OwnedRoomOrAliasId) -> Self {
561 Self {
562 id,
563 via: Vec::new(),
564 }
565 }
566}
567
568impl From<OwnedRoomId> for MatrixRoomIdUri {
569 fn from(value: OwnedRoomId) -> Self {
570 OwnedRoomOrAliasId::from(value).into()
571 }
572}
573
574impl From<OwnedRoomAliasId> for MatrixRoomIdUri {
575 fn from(value: OwnedRoomAliasId) -> Self {
576 OwnedRoomOrAliasId::from(value).into()
577 }
578}
579
580impl From<&MatrixRoomIdUri> for MatrixUri {
581 fn from(value: &MatrixRoomIdUri) -> Self {
582 match <&RoomId>::try_from(&*value.id) {
583 Ok(room_id) => room_id.matrix_uri_via(value.via.clone(), false),
584 Err(alias) => alias.matrix_uri(false),
585 }
586 }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
591pub(crate) struct MatrixEventIdUri {
592 pub event_id: OwnedEventId,
594 pub room_uri: MatrixRoomIdUri,
596}
597
598#[derive(Debug, Clone, Error)]
600pub(crate) enum MatrixIdUriParseError {
601 #[error(transparent)]
603 InvalidUri(#[from] IdParseError),
604 #[error("unsupported Matrix ID: {0:?}")]
606 UnsupportedId(MatrixId),
607}
608
609pub(crate) fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> glib::DateTime {
611 seconds_since_unix_epoch_to_date(ts.as_secs().into())
612}
613
614pub(crate) fn seconds_since_unix_epoch_to_date(secs: i64) -> glib::DateTime {
616 glib::DateTime::from_unix_utc(secs)
617 .and_then(|date| date.to_local())
618 .expect("constructing GDateTime from timestamp should work")
619}
620
621#[derive(Debug, Clone, Default)]
629pub(crate) struct MessageCacheKey {
630 pub(crate) transaction_id: Option<OwnedTransactionId>,
635 pub(crate) event_id: Option<OwnedEventId>,
640 pub(crate) is_edited: bool,
644}
645
646impl MessageCacheKey {
647 pub(crate) fn should_reload(&self, new: &MessageCacheKey) -> bool {
650 if new.is_edited {
651 return true;
652 }
653
654 let transaction_id_invalidated = self.transaction_id.is_none()
655 || new.transaction_id.is_none()
656 || self.transaction_id != new.transaction_id;
657 let event_id_invalidated =
658 self.event_id.is_none() || new.event_id.is_none() || self.event_id != new.event_id;
659
660 transaction_id_invalidated && event_id_invalidated
661 }
662}