phoebe/src/matrix.rs

410 lines
12 KiB
Rust

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<AnyMessageEventContent> {
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<MessageEvent>,
}
impl MatrixHandler {
async fn get_message_author(&self, room: &Joined, user_id: &UserId) -> Option<MessageAuthor> {
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<FormattedBody>) -> 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<events::MessageEvent<MessageEventContent>> {
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<MatrixHandler>,
event: SyncMessageEvent<MessageEventContent>,
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<MatrixHandler>,
event: &SyncMessageEvent<MessageEventContent>,
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<MatrixHandler>,
event: &SyncMessageEvent<MessageEventContent>,
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<MatrixHandler>, 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##"<p><font data-mx-color="#9a9a9a" data-mx-bg-color="#000000">{} <small>({})</small></font>:</p> <p>{}</p>"##,
&author.display_name,
&author.service_name,
format_matrix(content),
);
(plaintext_message, html_message)
}
fn generate_event_content_struct(
replied_message_event: Option<&events::MessageEvent<MessageEventContent>>,
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<EventId>,
) -> Option<MessageReference> {
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<MessageReference> {
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<MessageEvent>,
) -> 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
}