1use std::{borrow::Cow, str::FromStr};
4
5use gettextrs::gettext;
6use gtk::{glib, prelude::*};
7use matrix_sdk::{
8 authentication::matrix::MatrixSession,
9 config::RequestConfig,
10 deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
11 encryption::{BackupDownloadStrategy, EncryptionSettings},
12 Client, ClientBuildError, SessionMeta, SessionTokens,
13};
14use ruma::{
15 events::{
16 room::{member::MembershipState, message::MessageType},
17 AnyMessageLikeEventContent, AnyStrippedStateEvent, AnySyncMessageLikeEvent,
18 AnySyncTimelineEvent,
19 },
20 html::{
21 matrix::{AnchorUri, MatrixElement},
22 Children, Html, HtmlSanitizerMode, NodeRef, RemoveReplyFallback, StrTendril,
23 },
24 matrix_uri::MatrixId,
25 serde::Raw,
26 EventId, IdParseError, MatrixToUri, MatrixUri, MatrixUriError, MilliSecondsSinceUnixEpoch,
27 OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId,
28 RoomId, RoomOrAliasId, UserId,
29};
30use thiserror::Error;
31use tracing::error;
32
33pub mod ext_traits;
34mod media_message;
35
36pub use self::media_message::{MediaMessage, VisualMediaMessage};
37use crate::{
38 components::Pill,
39 gettext_f,
40 prelude::*,
41 secret::StoredSession,
42 session::model::{RemoteRoom, Room},
43};
44
45#[derive(Debug, Default, Clone, Copy)]
47#[allow(clippy::struct_excessive_bools)]
48pub struct PasswordValidity {
49 pub has_lowercase: bool,
51 pub has_uppercase: bool,
53 pub has_number: bool,
55 pub has_symbol: bool,
57 pub has_length: bool,
59 pub progress: u32,
63}
64
65impl PasswordValidity {
66 pub fn new() -> Self {
67 Self::default()
68 }
69}
70
71pub fn validate_password(password: &str) -> PasswordValidity {
78 let mut validity = PasswordValidity::new();
79
80 for char in password.chars() {
81 if char.is_numeric() {
82 validity.has_number = true;
83 } else if char.is_lowercase() {
84 validity.has_lowercase = true;
85 } else if char.is_uppercase() {
86 validity.has_uppercase = true;
87 } else {
88 validity.has_symbol = true;
89 }
90 }
91
92 validity.has_length = password.len() >= 8;
93
94 let mut passed = 0;
95 if validity.has_number {
96 passed += 1;
97 }
98 if validity.has_lowercase {
99 passed += 1;
100 }
101 if validity.has_uppercase {
102 passed += 1;
103 }
104 if validity.has_symbol {
105 passed += 1;
106 }
107 if validity.has_length {
108 passed += 1;
109 }
110 validity.progress = passed * 100 / 5;
111
112 validity
113}
114
115#[derive(Debug, Clone)]
117pub enum AnySyncOrStrippedTimelineEvent {
118 Sync(AnySyncTimelineEvent),
120 Stripped(AnyStrippedStateEvent),
122}
123
124impl AnySyncOrStrippedTimelineEvent {
125 pub fn from_raw(raw: &RawAnySyncOrStrippedTimelineEvent) -> Result<Self, serde_json::Error> {
127 let ev = match raw {
128 RawAnySyncOrStrippedTimelineEvent::Sync(ev) => Self::Sync(ev.deserialize()?),
129 RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => Self::Stripped(ev.deserialize()?),
130 };
131
132 Ok(ev)
133 }
134
135 pub fn sender(&self) -> &UserId {
137 match self {
138 AnySyncOrStrippedTimelineEvent::Sync(ev) => ev.sender(),
139 AnySyncOrStrippedTimelineEvent::Stripped(ev) => ev.sender(),
140 }
141 }
142
143 pub fn event_id(&self) -> Option<&EventId> {
145 match self {
146 AnySyncOrStrippedTimelineEvent::Sync(ev) => Some(ev.event_id()),
147 AnySyncOrStrippedTimelineEvent::Stripped(_) => None,
148 }
149 }
150}
151
152pub fn get_event_body(
159 event: &AnySyncOrStrippedTimelineEvent,
160 sender_name: &str,
161 own_user: &UserId,
162 show_sender: bool,
163) -> Option<String> {
164 match event {
165 AnySyncOrStrippedTimelineEvent::Sync(AnySyncTimelineEvent::MessageLike(message)) => {
166 get_message_event_body(message, sender_name, show_sender)
167 }
168 AnySyncOrStrippedTimelineEvent::Sync(_) => None,
169 AnySyncOrStrippedTimelineEvent::Stripped(state) => {
170 get_stripped_state_event_body(state, sender_name, own_user)
171 }
172 }
173}
174
175pub fn get_message_event_body(
181 event: &AnySyncMessageLikeEvent,
182 sender_name: &str,
183 show_sender: bool,
184) -> Option<String> {
185 match event.original_content()? {
186 AnyMessageLikeEventContent::RoomMessage(mut message) => {
187 message.sanitize(HtmlSanitizerMode::Compat, RemoveReplyFallback::Yes);
188
189 let body = match message.msgtype {
190 MessageType::Audio(_) => {
191 gettext_f("{user} sent an audio file.", &[("user", sender_name)])
192 }
193 MessageType::Emote(content) => format!("{sender_name} {}", content.body),
194 MessageType::File(_) => gettext_f("{user} sent a file.", &[("user", sender_name)]),
195 MessageType::Image(_) => {
196 gettext_f("{user} sent an image.", &[("user", sender_name)])
197 }
198 MessageType::Location(_) => {
199 gettext_f("{user} sent their location.", &[("user", sender_name)])
200 }
201 MessageType::Notice(content) => {
202 text_event_body(content.body, sender_name, show_sender)
203 }
204 MessageType::ServerNotice(content) => {
205 text_event_body(content.body, sender_name, show_sender)
206 }
207 MessageType::Text(content) => {
208 text_event_body(content.body, sender_name, show_sender)
209 }
210 MessageType::Video(_) => {
211 gettext_f("{user} sent a video.", &[("user", sender_name)])
212 }
213 _ => return None,
214 };
215 Some(body)
216 }
217 AnyMessageLikeEventContent::Sticker(_) => Some(gettext_f(
218 "{user} sent a sticker.",
219 &[("user", sender_name)],
220 )),
221 _ => None,
222 }
223}
224
225fn text_event_body(message: String, sender_name: &str, show_sender: bool) -> String {
226 if show_sender {
227 gettext_f(
228 "{user}: {message}",
229 &[("user", sender_name), ("message", &message)],
230 )
231 } else {
232 message
233 }
234}
235
236pub fn get_stripped_state_event_body(
242 event: &AnyStrippedStateEvent,
243 sender_name: &str,
244 own_user: &UserId,
245) -> Option<String> {
246 if let AnyStrippedStateEvent::RoomMember(member_event) = event {
247 if member_event.content.membership == MembershipState::Invite
248 && member_event.state_key == own_user
249 {
250 return Some(gettext_f("{user} invited you", &[("user", sender_name)]));
253 }
254 }
255
256 None
257}
258
259#[derive(Error, Debug)]
261pub enum ClientSetupError {
262 #[error(transparent)]
264 Client(#[from] ClientBuildError),
265 #[error(transparent)]
267 Sdk(#[from] matrix_sdk::Error),
268 #[error("Could not generate unique session ID")]
270 NoSessionId,
271 #[error("Could not access session tokens")]
273 NoSessionTokens,
274}
275
276impl UserFacingError for ClientSetupError {
277 fn to_user_facing(&self) -> String {
278 match self {
279 Self::Client(err) => err.to_user_facing(),
280 Self::Sdk(err) => err.to_user_facing(),
281 Self::NoSessionId => gettext("Could not generate unique session ID"),
282 Self::NoSessionTokens => gettext("Could not access the session tokens"),
283 }
284 }
285}
286
287pub async fn client_with_stored_session(
289 session: StoredSession,
290 tokens: SessionTokens,
291) -> Result<Client, ClientSetupError> {
292 let has_refresh_token = tokens.refresh_token.is_some();
293 let data_path = session.data_path();
294 let cache_path = session.cache_path();
295
296 let StoredSession {
297 homeserver,
298 user_id,
299 device_id,
300 passphrase,
301 ..
302 } = session;
303
304 let session_data = MatrixSession {
305 meta: SessionMeta { user_id, device_id },
306 tokens,
307 };
308
309 let encryption_settings = EncryptionSettings {
310 auto_enable_cross_signing: true,
311 backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
312 auto_enable_backups: true,
313 };
314
315 let mut client_builder = Client::builder()
316 .homeserver_url(homeserver)
317 .sqlite_store_with_cache_path(data_path, cache_path, Some(&passphrase))
318 .request_config(RequestConfig::new().retry_limit(2).force_auth())
322 .with_encryption_settings(encryption_settings);
323
324 if has_refresh_token {
325 client_builder = client_builder.handle_refresh_tokens();
326 }
327
328 let client = client_builder.build().await?;
329
330 client.restore_session(session_data).await?;
331
332 if let Err(error) = client.event_cache().enable_storage() {
333 error!("Failed to enable event cache storage: {error}");
334 }
335
336 Ok(client)
337}
338
339pub fn find_html_mentions(html: &str, room: &Room) -> Vec<(Pill, StrTendril)> {
343 let mut mentions = Vec::new();
344 let html = Html::parse(html);
345
346 append_children_mentions(&mut mentions, html.children(), room);
347
348 mentions
349}
350
351fn append_children_mentions(
353 mentions: &mut Vec<(Pill, StrTendril)>,
354 children: Children,
355 room: &Room,
356) {
357 for node in children {
358 if let Some(mention) = node_as_mention(&node, room) {
359 mentions.push(mention);
360 continue;
361 }
362
363 append_children_mentions(mentions, node.children(), room);
364 }
365}
366
367fn node_as_mention(node: &NodeRef, room: &Room) -> Option<(Pill, StrTendril)> {
371 let MatrixElement::A(anchor) = node.as_element()?.to_matrix().element else {
373 return None;
374 };
375
376 let id = MatrixIdUri::try_from(anchor.href?).ok()?;
378
379 let child = node.children().next()?;
381
382 if child.next_sibling().is_some() {
383 return None;
384 }
385
386 let content = child.as_text()?.borrow().clone();
387 let pill = id.into_pill(room)?;
388
389 Some((pill, content))
390}
391
392pub const AT_ROOM: &str = "@room";
394
395pub fn find_at_room(s: &str) -> Option<usize> {
402 for (pos, _) in s.match_indices(AT_ROOM) {
403 let is_at_word_start = pos == 0 || s[..pos].ends_with(char_is_ascii_word_boundary);
404 if !is_at_word_start {
405 continue;
406 }
407
408 let pos_after_match = pos + 5;
409 let is_at_word_end = pos_after_match == s.len()
410 || s[pos_after_match..].starts_with(char_is_ascii_word_boundary);
411 if is_at_word_end {
412 return Some(pos);
413 }
414 }
415
416 None
417}
418
419fn char_is_ascii_word_boundary(c: char) -> bool {
424 !c.is_ascii_alphanumeric() && c != '_'
425}
426
427pub fn raw_eq<T, U>(lhs: Option<&Raw<T>>, rhs: Option<&Raw<U>>) -> bool {
429 let Some(lhs) = lhs else {
430 return rhs.is_none();
432 };
433 let Some(rhs) = rhs else {
434 return false;
436 };
437
438 lhs.json().get() == rhs.json().get()
439}
440
441#[derive(Debug, Clone, PartialEq, Eq)]
443pub enum MatrixIdUri {
444 Room(MatrixRoomIdUri),
446 User(OwnedUserId),
448 Event(MatrixEventIdUri),
450}
451
452impl MatrixIdUri {
453 fn try_from_parts(id: MatrixId, via: &[OwnedServerName]) -> Result<Self, ()> {
455 let uri = match id {
456 MatrixId::Room(room_id) => Self::Room(MatrixRoomIdUri {
457 id: room_id.into(),
458 via: via.to_owned(),
459 }),
460 MatrixId::RoomAlias(room_alias) => Self::Room(MatrixRoomIdUri {
461 id: room_alias.into(),
462 via: via.to_owned(),
463 }),
464 MatrixId::User(user_id) => Self::User(user_id),
465 MatrixId::Event(room_id, event_id) => Self::Event(MatrixEventIdUri {
466 event_id,
467 room_uri: MatrixRoomIdUri {
468 id: room_id,
469 via: via.to_owned(),
470 },
471 }),
472 _ => return Err(()),
473 };
474
475 Ok(uri)
476 }
477
478 pub fn parse(s: &str) -> Result<Self, MatrixIdUriParseError> {
480 if let Ok(uri) = MatrixToUri::parse(s) {
481 return uri.try_into();
482 }
483
484 MatrixUri::parse(s)?.try_into()
485 }
486
487 pub fn into_pill(self, room: &Room) -> Option<Pill> {
489 match self {
490 Self::Room(room_uri) => {
491 let session = room.session()?;
492 session
493 .room_list()
494 .get_by_identifier(&room_uri.id)
495 .as_ref()
496 .map(Pill::new)
497 .or_else(|| Some(Pill::new(&RemoteRoom::new(&session, room_uri))))
498 }
499 Self::User(user_id) => {
500 let user = room.get_or_create_members().get_or_create(user_id);
503 Some(Pill::new(&user))
504 }
505 Self::Event(_) => None,
506 }
507 }
508}
509
510impl TryFrom<&MatrixUri> for MatrixIdUri {
511 type Error = MatrixIdUriParseError;
512
513 fn try_from(uri: &MatrixUri) -> Result<Self, Self::Error> {
514 Self::try_from_parts(uri.id().clone(), uri.via())
516 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
517 }
518}
519
520impl TryFrom<MatrixUri> for MatrixIdUri {
521 type Error = MatrixIdUriParseError;
522
523 fn try_from(uri: MatrixUri) -> Result<Self, Self::Error> {
524 Self::try_from(&uri)
525 }
526}
527
528impl TryFrom<&MatrixToUri> for MatrixIdUri {
529 type Error = MatrixIdUriParseError;
530
531 fn try_from(uri: &MatrixToUri) -> Result<Self, Self::Error> {
532 Self::try_from_parts(uri.id().clone(), uri.via())
533 .map_err(|()| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
534 }
535}
536
537impl TryFrom<MatrixToUri> for MatrixIdUri {
538 type Error = MatrixIdUriParseError;
539
540 fn try_from(uri: MatrixToUri) -> Result<Self, Self::Error> {
541 Self::try_from(&uri)
542 }
543}
544
545impl FromStr for MatrixIdUri {
546 type Err = MatrixIdUriParseError;
547
548 fn from_str(s: &str) -> Result<Self, Self::Err> {
549 Self::parse(s)
550 }
551}
552
553impl TryFrom<&str> for MatrixIdUri {
554 type Error = MatrixIdUriParseError;
555
556 fn try_from(s: &str) -> Result<Self, Self::Error> {
557 Self::parse(s)
558 }
559}
560
561impl TryFrom<&AnchorUri> for MatrixIdUri {
562 type Error = MatrixIdUriParseError;
563
564 fn try_from(value: &AnchorUri) -> Result<Self, Self::Error> {
565 match value {
566 AnchorUri::Matrix(uri) => MatrixIdUri::try_from(uri),
567 AnchorUri::MatrixTo(uri) => MatrixIdUri::try_from(uri),
568 _ => Err(IdParseError::InvalidMatrixUri(MatrixUriError::WrongScheme).into()),
570 }
571 }
572}
573
574impl TryFrom<AnchorUri> for MatrixIdUri {
575 type Error = MatrixIdUriParseError;
576
577 fn try_from(value: AnchorUri) -> Result<Self, Self::Error> {
578 Self::try_from(&value)
579 }
580}
581
582impl StaticVariantType for MatrixIdUri {
583 fn static_variant_type() -> Cow<'static, glib::VariantTy> {
584 String::static_variant_type()
585 }
586}
587
588impl FromVariant for MatrixIdUri {
589 fn from_variant(variant: &glib::Variant) -> Option<Self> {
590 Self::parse(&variant.get::<String>()?).ok()
591 }
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct MatrixRoomIdUri {
597 pub id: OwnedRoomOrAliasId,
599 pub via: Vec<OwnedServerName>,
601}
602
603impl MatrixRoomIdUri {
604 pub fn parse(s: &str) -> Option<MatrixRoomIdUri> {
606 MatrixIdUri::parse(s)
607 .ok()
608 .and_then(|uri| match uri {
609 MatrixIdUri::Room(room_uri) => Some(room_uri),
610 _ => None,
611 })
612 .or_else(|| RoomOrAliasId::parse(s).ok().map(Into::into))
613 }
614}
615
616impl From<OwnedRoomOrAliasId> for MatrixRoomIdUri {
617 fn from(id: OwnedRoomOrAliasId) -> Self {
618 Self {
619 id,
620 via: Vec::new(),
621 }
622 }
623}
624
625impl From<OwnedRoomId> for MatrixRoomIdUri {
626 fn from(value: OwnedRoomId) -> Self {
627 OwnedRoomOrAliasId::from(value).into()
628 }
629}
630
631impl From<OwnedRoomAliasId> for MatrixRoomIdUri {
632 fn from(value: OwnedRoomAliasId) -> Self {
633 OwnedRoomOrAliasId::from(value).into()
634 }
635}
636
637impl From<&MatrixRoomIdUri> for MatrixUri {
638 fn from(value: &MatrixRoomIdUri) -> Self {
639 match <&RoomId>::try_from(&*value.id) {
640 Ok(room_id) => room_id.matrix_uri_via(value.via.clone(), false),
641 Err(alias) => alias.matrix_uri(false),
642 }
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct MatrixEventIdUri {
649 pub event_id: OwnedEventId,
651 pub room_uri: MatrixRoomIdUri,
653}
654
655#[derive(Debug, Clone, Error)]
657pub enum MatrixIdUriParseError {
658 #[error(transparent)]
660 InvalidUri(#[from] IdParseError),
661 #[error("unsupported Matrix ID: {0:?}")]
663 UnsupportedId(MatrixId),
664}
665
666pub(crate) fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> glib::DateTime {
668 seconds_since_unix_epoch_to_date(ts.as_secs().into())
669}
670
671pub(crate) fn seconds_since_unix_epoch_to_date(secs: i64) -> glib::DateTime {
673 glib::DateTime::from_unix_utc(secs)
674 .and_then(|date| date.to_local())
675 .expect("constructing GDateTime from timestamp should work")
676}