use std::sync::Arc; use matrix_sdk::{ config::ClientConfig, room::{Joined, Room}, ruma::{ api, events::{ self, room::{ message::{ FormattedBody, MessageEventContent, MessageFormat, MessageType, Relation, Replacement, }, redaction::{RedactionEventContent, SyncRedactionEvent}, }, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnySyncRoomEvent, SyncMessageEvent, }, UserId, }, }; pub use matrix_sdk::{ config::SyncSettings, ruma::{EventId, RoomId}, Client, }; use log::info; use tokio::sync::mpsc; use url::Url; use crate::{ message_ast::{ convert_matrix, convert_plain, format_discord, format_matrix, MessageComponent, MessageContent, }, messages::{ DeletedMessage, EditedMessage, MessageAuthor, MessageEvent, MessageReference, SentMessage, }, }; impl From<(&RoomId, &EventId)> for MessageReference { fn from((room_id, event_id): (&RoomId, &EventId)) -> Self { let room_string = room_id.as_str().to_string(); let event_string = event_id.as_str().to_string(); Self::Matrix(room_string, event_string) } } fn _find_content(event: &AnySyncRoomEvent) -> Option { match event { AnySyncRoomEvent::Message(message) => Some(message.content()), AnySyncRoomEvent::RedactedMessage(message) => { if let Some(ref redaction_event) = message.unsigned().redacted_because { Some(AnyMessageEventContent::RoomRedaction( redaction_event.content.clone(), )) } else { Some(AnyMessageEventContent::RoomRedaction( RedactionEventContent::new(), )) } } AnySyncRoomEvent::RedactedState(state) => { if let Some(ref redaction_event) = state.unsigned().redacted_because { Some(AnyMessageEventContent::RoomRedaction( redaction_event.content.clone(), )) } else { Some(AnyMessageEventContent::RoomRedaction( RedactionEventContent::new(), )) } } _ => None, } } struct MatrixHandler { message_tx: mpsc::UnboundedSender, } impl MatrixHandler { async fn get_message_author(&self, room: &Joined, user_id: &UserId) -> Option { if let Ok(Some(sender)) = room.get_member(user_id).await { Some(MessageAuthor { display_name: sender .display_name() .unwrap_or_else(|| sender.name()) .to_string(), avatar_url: sender .avatar_url() .map(|u| { format!( "https://matrix.org/_matrix/media/r0/thumbnail/{}?width=500&height=500", u.as_str().to_string().replace("mxc://", "") ) }) .unwrap_or_else(|| { "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" .to_string() }), service_name: "matrix".to_string(), }) } else { None } } fn get_content(&self, body: &str, formatted_body: &Option) -> MessageContent { if let Some(html) = formatted_body .as_ref() .filter(|f| f.format == MessageFormat::Html) .map(|f| &f.body) { convert_matrix(html) } else { convert_plain(body) } } } async fn get_room_message_event( room: &Joined, event_id: &EventId, ) -> Option> { let event = room .event(api::client::r0::room::get_room_event::Request::new( room.room_id(), event_id, )) .await .ok()? .event .deserialize() .ok()?; if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message_event)) = event { Some(message_event) } else { None } } async fn on_room_message_event( ctx: Arc, event: SyncMessageEvent, room: Room, ) { if let Room::Joined(room) = room { if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { on_message_edited(ctx, &event, room, replacement).await; } else { on_message_sent(ctx, &event, room).await; } } } async fn on_message_sent( ctx: Arc, event: &SyncMessageEvent, room: Joined, ) { let message_ref = MessageReference::from((room.room_id(), &event.event_id)); if let Some(author) = ctx.get_message_author(&room, &event.sender).await { let message_event = match &event.content.msgtype { MessageType::Text(text) => { let content = ctx.get_content(&text.body, &text.formatted); let replies_to = if let Some(Relation::Reply { in_reply_to }) = &event.content.relates_to { Some(MessageReference::from(( room.room_id(), &in_reply_to.event_id, ))) } else { None }; Some(MessageEvent::Send(SentMessage { source: message_ref, content, author, replies_to, })) } MessageType::Emote(emote) => { let mut content = ctx.get_content(&emote.body, &emote.formatted); content.insert(0, MessageComponent::Plain("* ".to_string())); Some(MessageEvent::Send(SentMessage { source: message_ref, content, author, replies_to: None, })) } // TODO: Handle reactions, uploads (audio, video, image, file), and any other types of event _ => None, }; if let Some(e) = message_event { let _ = ctx.message_tx.send(e); } } let _ = room.read_receipt(&event.event_id).await; } async fn on_message_edited( ctx: Arc, event: &SyncMessageEvent, room: Joined, replacement: &Replacement, ) { let message_ref = MessageReference::from((room.room_id(), &replacement.event_id)); if let MessageType::Text(text) = &replacement.new_content.msgtype { let content = ctx.get_content(&text.body, &text.formatted); if let Some(author) = ctx.get_message_author(&room, &event.sender).await { let _ = ctx.message_tx.send(MessageEvent::Edit(EditedMessage { replacing: message_ref, content, author, })); } } } async fn on_redact_event(ctx: Arc, event: SyncRedactionEvent, room: Room) { let message_ref = MessageReference::from((room.room_id(), &event.redacts)); let _ = ctx.message_tx.send(MessageEvent::Delete(DeletedMessage { reference: message_ref, })); } fn generate_html_content(content: &[MessageComponent], author: &MessageAuthor) -> (String, String) { let plaintext_message = format!( "{} ({}): {}", &author.display_name, &author.service_name, format_discord(content) ); // TODO: Do we want to do something with different users' discord name colors? That could be a neat way // to differentiate people easily without being able to see avatars let html_message = format!( r##"

{} ({}):

{}

"##, &author.display_name, &author.service_name, format_matrix(content), ); (plaintext_message, html_message) } fn generate_event_content_struct( replied_message_event: Option<&events::MessageEvent>, plaintext_content: String, html_content: String, ) -> MessageEventContent { if let Some(replied_message_event) = replied_message_event { MessageEventContent::text_reply_html(plaintext_content, html_content, replied_message_event) } else { MessageEventContent::text_html(plaintext_content, html_content) } } pub async fn forward_to_matrix( client: &Client, room_id: RoomId, message: &SentMessage, replying_to: Option, ) -> Option { if let Some(room) = client.get_joined_room(&room_id) { let replied_message_event = if let Some(event_id) = replying_to { get_room_message_event(&room, &event_id).await } else { None }; let (plaintext_content, html_content) = generate_html_content(&message.content, &message.author); let content = generate_event_content_struct( replied_message_event.as_ref(), plaintext_content, html_content, ); let event = room .send(AnyMessageEventContent::RoomMessage(content), None) .await .ok()?; return Some(MessageReference::from((&room_id, &event.event_id))); } None } pub async fn edit_on_matrix( client: &Client, room_id: RoomId, event_id: EventId, message: &EditedMessage, ) -> Option { if let Some(room) = client.get_joined_room(&room_id) { if let Some(original_message_event) = get_room_message_event(&room, &event_id).await { if let Some(Relation::Replacement(_)) = &original_message_event.content.relates_to { return None; } let replied_message_event = if let Some(Relation::Reply { in_reply_to }) = &original_message_event.content.relates_to { get_room_message_event(&room, &in_reply_to.event_id).await } else { None }; let (plaintext_content, html_content) = generate_html_content(&message.content, &message.author); let mut edit_content = generate_event_content_struct( replied_message_event.as_ref(), format!("* {}", &plaintext_content), format!("* {}", &html_content), ); let basic_content = generate_event_content_struct( replied_message_event.as_ref(), plaintext_content, html_content, ); edit_content.relates_to = Some(Relation::Replacement(Replacement::new( event_id, Box::new(basic_content), ))); let new_event = room .send(AnyMessageEventContent::RoomMessage(edit_content), None) .await .ok()?; return Some(MessageReference::from((&room_id, &new_event.event_id))); } } None } pub async fn delete_on_matrix( client: &Client, room_id: RoomId, event_id: EventId, _message: &DeletedMessage, ) -> bool { if let Some(room) = client.get_joined_room(&room_id) { room.redact(&event_id, None, None).await.is_ok() } else { false } } pub async fn create_matrix_client( homeserver_url: String, username: String, password: String, message_tx: mpsc::UnboundedSender, ) -> Client { let client_config = ClientConfig::new().store_path("./data/matrix_state"); let homeserver_url = Url::parse(&homeserver_url).expect("Failed to parse the matrix homeserver URL"); let client = Client::new_with_config(homeserver_url, client_config).unwrap(); info!("Matrix logging in…"); client .login(&username, &password, None, Some("phoebe")) .await .expect("Failed to log in"); info!("Matrix starting…"); client.sync_once(SyncSettings::default()).await.unwrap(); let event_handler = Arc::new(MatrixHandler { message_tx }); let on_msg_ctx = event_handler.clone(); client .register_event_handler(move |ev, room| on_room_message_event(on_msg_ctx.clone(), ev, room)) .await; let on_redact_ctx = event_handler.clone(); client .register_event_handler(move |ev, room| on_redact_event(on_redact_ctx.clone(), ev, room)) .await; client }