use std::{borrow::Cow, str::FromStr};
use gettextrs::gettext;
use gtk::{glib, prelude::*};
use matrix_sdk::{
config::RequestConfig,
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
encryption::{BackupDownloadStrategy, EncryptionSettings},
matrix_auth::{MatrixSession, MatrixSessionTokens},
Client, ClientBuildError, SessionMeta,
};
use matrix_sdk_ui::timeline::{Message, TimelineItemContent};
use ruma::{
events::{
room::{member::MembershipState, message::MessageType},
AnyMessageLikeEventContent, AnyStrippedStateEvent, AnySyncMessageLikeEvent,
AnySyncTimelineEvent,
},
html::{
matrix::{AnchorUri, MatrixElement},
Children, Html, HtmlSanitizerMode, NodeRef, RemoveReplyFallback, StrTendril,
},
matrix_uri::MatrixId,
serde::Raw,
EventId, IdParseError, MatrixToUri, MatrixUri, MatrixUriError, OwnedEventId, OwnedRoomAliasId,
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomId, RoomOrAliasId, UserId,
};
use thiserror::Error;
mod media_message;
pub use self::media_message::{MediaMessage, VisualMediaMessage};
use crate::{
components::Pill,
gettext_f,
prelude::*,
secret::{Secret, StoredSession},
session::model::{RemoteRoom, Room},
};
#[derive(Debug, Default, Clone, Copy)]
pub struct PasswordValidity {
pub has_lowercase: bool,
pub has_uppercase: bool,
pub has_number: bool,
pub has_symbol: bool,
pub has_length: bool,
pub progress: u32,
}
impl PasswordValidity {
pub fn new() -> Self {
Self::default()
}
}
pub fn validate_password(password: &str) -> PasswordValidity {
let mut validity = PasswordValidity::new();
for char in password.chars() {
if char.is_numeric() {
validity.has_number = true;
} else if char.is_lowercase() {
validity.has_lowercase = true;
} else if char.is_uppercase() {
validity.has_uppercase = true;
} else {
validity.has_symbol = true;
}
}
validity.has_length = password.len() >= 8;
let mut passed = 0;
if validity.has_number {
passed += 1;
}
if validity.has_lowercase {
passed += 1;
}
if validity.has_uppercase {
passed += 1;
}
if validity.has_symbol {
passed += 1;
}
if validity.has_length {
passed += 1;
}
validity.progress = passed * 100 / 5;
validity
}
#[derive(Debug, Clone)]
pub enum AnySyncOrStrippedTimelineEvent {
Sync(AnySyncTimelineEvent),
Stripped(AnyStrippedStateEvent),
}
impl AnySyncOrStrippedTimelineEvent {
pub fn from_raw(raw: &RawAnySyncOrStrippedTimelineEvent) -> Result<Self, serde_json::Error> {
let ev = match raw {
RawAnySyncOrStrippedTimelineEvent::Sync(ev) => Self::Sync(ev.deserialize()?),
RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => Self::Stripped(ev.deserialize()?),
};
Ok(ev)
}
pub fn sender(&self) -> &UserId {
match self {
AnySyncOrStrippedTimelineEvent::Sync(ev) => ev.sender(),
AnySyncOrStrippedTimelineEvent::Stripped(ev) => ev.sender(),
}
}
pub fn event_id(&self) -> Option<&EventId> {
match self {
AnySyncOrStrippedTimelineEvent::Sync(ev) => Some(ev.event_id()),
AnySyncOrStrippedTimelineEvent::Stripped(_) => None,
}
}
}
pub fn get_event_body(
event: &AnySyncOrStrippedTimelineEvent,
sender_name: &str,
own_user: &UserId,
show_sender: bool,
) -> Option<String> {
match event {
AnySyncOrStrippedTimelineEvent::Sync(AnySyncTimelineEvent::MessageLike(message)) => {
get_message_event_body(message, sender_name, show_sender)
}
AnySyncOrStrippedTimelineEvent::Stripped(state) => {
get_stripped_state_event_body(state, sender_name, own_user)
}
_ => None,
}
}
pub fn get_message_event_body(
event: &AnySyncMessageLikeEvent,
sender_name: &str,
show_sender: bool,
) -> Option<String> {
match event.original_content()? {
AnyMessageLikeEventContent::RoomMessage(mut message) => {
message.sanitize(HtmlSanitizerMode::Compat, RemoveReplyFallback::Yes);
let body = match message.msgtype {
MessageType::Audio(_) => {
gettext_f("{user} sent an audio file.", &[("user", sender_name)])
}
MessageType::Emote(content) => format!("{sender_name} {}", content.body),
MessageType::File(_) => gettext_f("{user} sent a file.", &[("user", sender_name)]),
MessageType::Image(_) => {
gettext_f("{user} sent an image.", &[("user", sender_name)])
}
MessageType::Location(_) => {
gettext_f("{user} sent their location.", &[("user", sender_name)])
}
MessageType::Notice(content) => {
text_event_body(content.body, sender_name, show_sender)
}
MessageType::ServerNotice(content) => {
text_event_body(content.body, sender_name, show_sender)
}
MessageType::Text(content) => {
text_event_body(content.body, sender_name, show_sender)
}
MessageType::Video(_) => {
gettext_f("{user} sent a video.", &[("user", sender_name)])
}
_ => return None,
};
Some(body)
}
AnyMessageLikeEventContent::Sticker(_) => Some(gettext_f(
"{user} sent a sticker.",
&[("user", sender_name)],
)),
_ => None,
}
}
fn text_event_body(message: String, sender_name: &str, show_sender: bool) -> String {
if show_sender {
gettext_f(
"{user}: {message}",
&[("user", sender_name), ("message", &message)],
)
} else {
message
}
}
pub fn get_stripped_state_event_body(
event: &AnyStrippedStateEvent,
sender_name: &str,
own_user: &UserId,
) -> Option<String> {
if let AnyStrippedStateEvent::RoomMember(member_event) = event {
if member_event.content.membership == MembershipState::Invite
&& member_event.state_key == own_user
{
return Some(gettext_f("{user} invited you", &[("user", sender_name)]));
}
}
None
}
#[derive(Error, Debug)]
pub enum ClientSetupError {
#[error(transparent)]
Client(#[from] ClientBuildError),
#[error(transparent)]
Sdk(#[from] matrix_sdk::Error),
#[error("Could not generate unique session ID")]
NoSessionId,
}
impl UserFacingError for ClientSetupError {
fn to_user_facing(&self) -> String {
match self {
Self::Client(err) => err.to_user_facing(),
Self::Sdk(err) => err.to_user_facing(),
Self::NoSessionId => gettext("Could not generate unique session ID"),
}
}
}
pub async fn client_with_stored_session(
session: StoredSession,
) -> Result<Client, ClientSetupError> {
let data_path = session.data_path();
let cache_path = session.cache_path();
let StoredSession {
homeserver,
user_id,
device_id,
id: _,
secret: Secret {
access_token,
passphrase,
},
} = session;
let session_data = MatrixSession {
meta: SessionMeta { user_id, device_id },
tokens: MatrixSessionTokens {
access_token,
refresh_token: None,
},
};
let encryption_settings = EncryptionSettings {
auto_enable_cross_signing: true,
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
auto_enable_backups: true,
};
let client = Client::builder()
.homeserver_url(homeserver)
.sqlite_store_with_cache_path(data_path, cache_path, Some(&passphrase))
.request_config(RequestConfig::new().retry_limit(2).force_auth())
.with_encryption_settings(encryption_settings)
.build()
.await?;
client.restore_session(session_data).await?;
Ok(client)
}
pub fn find_html_mentions(html: &str, room: &Room) -> Vec<(Pill, StrTendril)> {
let mut mentions = Vec::new();
let html = Html::parse(html);
append_children_mentions(&mut mentions, html.children(), room);
mentions
}
fn append_children_mentions(
mentions: &mut Vec<(Pill, StrTendril)>,
children: Children,
room: &Room,
) {
for node in children {
if let Some(mention) = node_as_mention(&node, room) {
mentions.push(mention);
continue;
}
append_children_mentions(mentions, node.children(), room);
}
}
fn node_as_mention(node: &NodeRef, room: &Room) -> Option<(Pill, StrTendril)> {
let MatrixElement::A(anchor) = node.as_element()?.to_matrix().element else {
return None;
};
let id = MatrixIdUri::try_from(anchor.href?).ok()?;
let child = node.children().next()?;
if child.next_sibling().is_some() {
return None;
}
let content = child.as_text()?.borrow().clone();
let pill = id.into_pill(room)?;
Some((pill, content))
}
pub const AT_ROOM: &str = "@room";
pub fn find_at_room(s: &str) -> Option<usize> {
for (pos, _) in s.match_indices(AT_ROOM) {
let is_at_word_start = pos == 0 || s[..pos].ends_with(char_is_ascii_word_boundary);
if !is_at_word_start {
continue;
}
let pos_after_match = pos + 5;
let is_at_word_end = pos_after_match == s.len()
|| s[pos_after_match..].starts_with(char_is_ascii_word_boundary);
if is_at_word_end {
return Some(pos);
}
}
None
}
fn char_is_ascii_word_boundary(c: char) -> bool {
!c.is_ascii_alphanumeric() && c != '_'
}
pub fn raw_eq<T, U>(lhs: Option<&Raw<T>>, rhs: Option<&Raw<U>>) -> bool {
let Some(lhs) = lhs else {
return rhs.is_none();
};
let Some(rhs) = rhs else {
return false;
};
lhs.json().get() == rhs.json().get()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatrixIdUri {
Room(MatrixRoomIdUri),
User(OwnedUserId),
Event(MatrixEventIdUri),
}
impl MatrixIdUri {
fn try_from_parts(id: MatrixId, via: &[OwnedServerName]) -> Result<Self, ()> {
let uri = match id {
MatrixId::Room(room_id) => Self::Room(MatrixRoomIdUri {
id: room_id.into(),
via: via.to_owned(),
}),
MatrixId::RoomAlias(room_alias) => Self::Room(MatrixRoomIdUri {
id: room_alias.into(),
via: via.to_owned(),
}),
MatrixId::User(user_id) => Self::User(user_id),
MatrixId::Event(room_id, event_id) => Self::Event(MatrixEventIdUri {
event_id,
room_uri: MatrixRoomIdUri {
id: room_id,
via: via.to_owned(),
},
}),
_ => return Err(()),
};
Ok(uri)
}
pub fn parse(s: &str) -> Result<Self, MatrixIdUriParseError> {
if let Ok(uri) = MatrixToUri::parse(s) {
return uri.try_into();
}
MatrixUri::parse(s)?.try_into()
}
pub fn into_pill(self, room: &Room) -> Option<Pill> {
match self {
Self::Room(room_uri) => {
let session = room.session()?;
session
.room_list()
.get_by_identifier(&room_uri.id)
.as_ref()
.map(Pill::new)
.or_else(|| Some(Pill::new(&RemoteRoom::new(&session, room_uri))))
}
MatrixIdUri::User(user_id) => {
let user = room.get_or_create_members().get_or_create(user_id);
Some(Pill::new(&user))
}
_ => None,
}
}
}
impl TryFrom<&MatrixUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(uri: &MatrixUri) -> Result<Self, Self::Error> {
Self::try_from_parts(uri.id().clone(), uri.via())
.map_err(|_| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
}
}
impl TryFrom<MatrixUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(uri: MatrixUri) -> Result<Self, Self::Error> {
Self::try_from(&uri)
}
}
impl TryFrom<&MatrixToUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(uri: &MatrixToUri) -> Result<Self, Self::Error> {
Self::try_from_parts(uri.id().clone(), uri.via())
.map_err(|_| MatrixIdUriParseError::UnsupportedId(uri.id().clone()))
}
}
impl TryFrom<MatrixToUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(uri: MatrixToUri) -> Result<Self, Self::Error> {
Self::try_from(&uri)
}
}
impl FromStr for MatrixIdUri {
type Err = MatrixIdUriParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl TryFrom<&str> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl TryFrom<&AnchorUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(value: &AnchorUri) -> Result<Self, Self::Error> {
match value {
AnchorUri::Matrix(uri) => MatrixIdUri::try_from(uri),
AnchorUri::MatrixTo(uri) => MatrixIdUri::try_from(uri),
_ => Err(IdParseError::InvalidMatrixUri(MatrixUriError::WrongScheme).into()),
}
}
}
impl TryFrom<AnchorUri> for MatrixIdUri {
type Error = MatrixIdUriParseError;
fn try_from(value: AnchorUri) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}
impl StaticVariantType for MatrixIdUri {
fn static_variant_type() -> Cow<'static, glib::VariantTy> {
String::static_variant_type()
}
}
impl FromVariant for MatrixIdUri {
fn from_variant(variant: &glib::Variant) -> Option<Self> {
Self::parse(&variant.get::<String>()?).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatrixRoomIdUri {
pub id: OwnedRoomOrAliasId,
pub via: Vec<OwnedServerName>,
}
impl MatrixRoomIdUri {
pub fn parse(s: &str) -> Option<MatrixRoomIdUri> {
MatrixIdUri::parse(s)
.ok()
.and_then(|uri| match uri {
MatrixIdUri::Room(room_uri) => Some(room_uri),
_ => None,
})
.or_else(|| RoomOrAliasId::parse(s).ok().map(Into::into))
}
}
impl From<OwnedRoomOrAliasId> for MatrixRoomIdUri {
fn from(id: OwnedRoomOrAliasId) -> Self {
Self {
id,
via: Vec::new(),
}
}
}
impl From<OwnedRoomId> for MatrixRoomIdUri {
fn from(value: OwnedRoomId) -> Self {
OwnedRoomOrAliasId::from(value).into()
}
}
impl From<OwnedRoomAliasId> for MatrixRoomIdUri {
fn from(value: OwnedRoomAliasId) -> Self {
OwnedRoomOrAliasId::from(value).into()
}
}
impl From<&MatrixRoomIdUri> for MatrixUri {
fn from(value: &MatrixRoomIdUri) -> Self {
match <&RoomId>::try_from(&*value.id) {
Ok(room_id) => room_id.matrix_uri_via(value.via.clone(), false),
Err(alias) => alias.matrix_uri(false),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatrixEventIdUri {
pub event_id: OwnedEventId,
pub room_uri: MatrixRoomIdUri,
}
#[derive(Debug, Clone, Error)]
pub enum MatrixIdUriParseError {
#[error(transparent)]
InvalidUri(#[from] IdParseError),
#[error("unsupported Matrix ID: {0:?}")]
UnsupportedId(MatrixId),
}
pub trait AtMentionExt {
fn can_contain_at_room(&self) -> bool;
}
impl AtMentionExt for TimelineItemContent {
fn can_contain_at_room(&self) -> bool {
match self {
TimelineItemContent::Message(msg) => msg.can_contain_at_room(),
_ => false,
}
}
}
impl AtMentionExt for Message {
fn can_contain_at_room(&self) -> bool {
let Some(mentions) = self.mentions() else {
return true;
};
mentions.room
}
}