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