From 5a6385f0b0c54cfa55c5f552af4d6d9fe0352d00 Mon Sep 17 00:00:00 2001 From: videogame hacker Date: Fri, 10 Sep 2021 05:44:00 +0100 Subject: [PATCH] Implement the... bridging --- Cargo.lock | 1 + Cargo.toml | 1 + src/discord.rs | 42 ++++-- src/main.rs | 159 +++++++++++++++++++- src/matrix.rs | 138 ++++++++++++----- src/{ast => message_ast}/convert_discord.rs | 32 ++++ src/message_ast/convert_plain.rs | 5 + src/{ast => message_ast}/mod.rs | 4 +- 8 files changed, 335 insertions(+), 47 deletions(-) rename src/{ast => message_ast}/convert_discord.rs (56%) create mode 100644 src/message_ast/convert_plain.rs rename src/{ast => message_ast}/mod.rs (81%) diff --git a/Cargo.lock b/Cargo.lock index 5e8645a..d615900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,7 @@ dependencies = [ "sled", "tokio", "tracing", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9953ef3..ca124db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ serenity = "0.10.8" sled = "0.34.6" tokio = { version = "1.8.0", features = ["full"] } tracing = "0.1.26" +url = "2.2.2" diff --git a/src/discord.rs b/src/discord.rs index 60931ba..592e04a 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,7 +1,10 @@ use serenity::{async_trait, model::prelude::*, prelude::*}; +use tokio::sync::mpsc; use tracing::info; -use crate::{ast, MessageReference}; +use crate::{message_ast, MessageReference, SentMessage}; + +pub use serenity::client::Context; impl From<&Message> for MessageReference { fn from(message: &Message) -> Self { @@ -11,22 +14,43 @@ impl From<&Message> for MessageReference { // TODO: Some way to emit messages for matrix, // and some way to receive messages from matrix. -struct DiscordSide {} +struct DiscordHandler { + ctx_tx: mpsc::UnboundedSender, + message_tx: mpsc::UnboundedSender, +} #[async_trait] -impl EventHandler for DiscordSide { - async fn ready(&self, _ctx: Context, _ready: Ready) { +impl EventHandler for DiscordHandler { + async fn ready(&self, ctx: Context, _ready: Ready) { info!("Discord side: Ready"); - // TODO + + let _ = self.ctx_tx.send(ctx); + // TODO: Scan for channels to link } - async fn message(&self, ctx: Context, message: Message) { - let _message_ref = MessageReference::from(&message); + async fn message(&self, _ctx: Context, message: Message) { + let message_ref = MessageReference::from(&message); // TODO: Store this message ref & associations in the DB let content = discord_message_format::parse(&message.content); - let content = ast::convert_discord(&content); + let content = message_ast::convert_discord(&content); - // TODO: Broadcast the message to other platforms + let _ = self.message_tx.send(SentMessage { + source: message_ref, + content, + }); } } + +pub async fn create_discord_client( + ctx_tx: mpsc::UnboundedSender, + message_tx: mpsc::UnboundedSender, + token: &str, +) -> Client { + let handler = DiscordHandler { ctx_tx, message_tx }; + + Client::builder(token) + .event_handler(handler) + .await + .expect("Failed to create discord client") +} diff --git a/src/main.rs b/src/main.rs index b357baa..7f0af64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,27 @@ +use std::{ + cell::RefCell, + str::FromStr, + sync::{Arc, Mutex}, +}; + +use discord::create_discord_client; +use matrix::create_matrix_client; +use matrix_sdk::{ + ruma::{ + events::{room::message::MessageEventContent, AnyMessageEventContent}, + RoomId, + }, + SyncSettings, +}; use serde::{Deserialize, Serialize}; -mod ast; mod discord; mod matrix; +mod message_ast; + +use message_ast::MessageContent; +use serenity::model::id::ChannelId; +use tokio::sync::mpsc; #[derive(Serialize, Deserialize)] pub enum MessageReference { @@ -10,6 +29,140 @@ pub enum MessageReference { Matrix(String, String), } -fn main() { - println!("Hello, world!"); +pub struct SentMessage { + pub source: MessageReference, + pub content: MessageContent, +} + +struct Bridgers { + discord: Mutex>>, + matrix: Mutex>>, +} + +impl Bridgers { + fn new() -> Self { + Self { + discord: Mutex::new(RefCell::new(None)), + matrix: Mutex::new(RefCell::new(None)), + } + } + + async fn send_message( + &self, + source: MessageReference, + content: MessageContent, + ) -> Vec { + let mut created_messages = Vec::new(); + + if let Some(discord) = self.discord.lock().unwrap().borrow().as_ref() { + // We probably want a function that returns an Option taking the source + match &source { + MessageReference::Matrix(_room_id, _event_id) => { + let channel_id = ChannelId(885690775193661463); // TODO: Look up linked channel + let discord_message = channel_id + .send_message(&discord.http, |m| { + m.content(message_ast::format_discord(&content)) + }) + .await + .expect("Failed to send discord message"); + + created_messages.push(MessageReference::from(&discord_message)); + } + _ => {} + }; + } + + if let Some(matrix) = self.matrix.lock().unwrap().borrow().as_ref() { + match &source { + MessageReference::Discord(_, _) => { + let room_id = RoomId::from_str("asdfghj").unwrap(); // TODO: Get a room id + if let Some(room) = matrix.get_joined_room(&room_id) { + let event = room + .send( + AnyMessageEventContent::RoomMessage( + MessageEventContent::text_plain(message_ast::format_discord( + &content, // TODO: Format as HTML + )), + ), + None, + ) + .await + .unwrap(); + + created_messages.push(MessageReference::from((&room_id, &event.event_id))); + } + } + _ => {} + } + } + + created_messages + } +} + +async fn setup_discord( + token: String, + bridgers: Arc, + discord_tx: mpsc::UnboundedSender, +) { + let (discord_ctx_tx, mut discord_ctx_rx) = mpsc::unbounded_channel::(); + tokio::spawn(async move { + let mut discord = create_discord_client(discord_ctx_tx, discord_tx, &token).await; + discord.start().await.unwrap(); + }); + + // Hack to grab the Context object when discord is ready + tokio::spawn(async move { + while let Some(discord) = discord_ctx_rx.recv().await { + bridgers.discord.lock().unwrap().replace(Some(discord)); + } + }); +} + +async fn setup_matrix( + homeserver_url: String, + username: String, + password: String, + bridgers: Arc, + message_tx: mpsc::UnboundedSender, +) { + let client = create_matrix_client(homeserver_url, username, password, message_tx).await; + let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); + + bridgers + .matrix + .lock() + .unwrap() + .replace(Some(client.clone())); + + tokio::spawn(async move { + client.sync(settings).await; + }); +} + +#[tokio::main] +async fn main() { + let bridgers = Arc::new(Bridgers::new()); + + let (message_tx, mut message_rx) = tokio::sync::mpsc::unbounded_channel::(); + + setup_discord( + "token".to_string(), + Arc::clone(&bridgers), + message_tx.clone(), + ) + .await; + + setup_matrix( + "https://matrix.org".to_string(), + "username".to_string(), + "password".to_string(), + Arc::clone(&bridgers), + message_tx.clone(), + ) + .await; + + while let Some(message) = message_rx.recv().await { + let _ = bridgers.send_message(message.source, message.content).await; + } } diff --git a/src/matrix.rs b/src/matrix.rs index cdc1e10..6121f5d 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -2,58 +2,128 @@ use matrix_sdk::{ async_trait, room::Room, ruma::{ - api::client::r0::room::get_room_event, events::{ - room::message::{MessageEventContent, MessageFormat, MessageType}, - AnyMessageEvent, AnyRoomEvent, SyncMessageEvent, + room::{ + message::{MessageEventContent, MessageFormat, MessageType, Relation}, + redaction::RedactionEventContent, + }, + AnyMessageEventContent, AnySyncRoomEvent, SyncMessageEvent, }, + EventId, RoomId, }, - EventHandler, + ClientConfig, EventHandler, SyncSettings, }; -use crate::MessageReference; +pub use matrix_sdk::Client; +use tokio::sync::mpsc; +use url::Url; -impl From<(&Room, &SyncMessageEvent)> for MessageReference { - fn from((room, event): (&Room, &SyncMessageEvent)) -> Self { - let room_string = room.room_id().as_str().to_string(); - let event_string = event.event_id.as_str().to_string(); +use crate::{message_ast::convert_plain, 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) } } -struct MatrixHandler; +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, +} #[async_trait] impl EventHandler for MatrixHandler { async fn on_room_message(&self, room: Room, event: &SyncMessageEvent) { - let event_id = &event.event_id; + let message_ref = MessageReference::from((room.room_id(), &event.event_id)); - if let Room::Joined(room) = room { - let event = room - .event(get_room_event::Request::new(room.room_id(), event_id)) - .await - .unwrap() - .event - .deserialize() - .unwrap(); + let message_type = + if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { + &replacement.new_content.msgtype + } else { + &event.content.msgtype + }; - if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) = event { - if let MessageEventContent { - msgtype: MessageType::Text(text_content), - .. - } = &message.content + match message_type { + MessageType::Text(text) => { + let content = if let Some(_html) = text + .formatted + .as_ref() + .filter(|f| f.format == MessageFormat::Html) + .map(|f| &f.body) { - if let Some(html_body) = text_content - .formatted - .as_ref() - .filter(|f| f.format == MessageFormat::Html) - .map(|f| &f.body) - { - // TODO: Parse the html_body into the AST - } - } + todo!("Parse html_body into MessageContent AST") + } else { + convert_plain(&text.body) + }; + + let _ = self.message_tx.send(SentMessage { + source: message_ref, + content, + }); } - } + + MessageType::Emote(_emote) => { + // TODO + } + + _ => {} + }; } } + +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("./matrix"); + + 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(); + + client + .login(&username, &password, None, Some("phoebe")) + .await + .expect("Failed to log in"); + + client.sync_once(SyncSettings::default()).await.unwrap(); + + let event_handler = MatrixHandler { message_tx }; + client.set_event_handler(Box::new(event_handler)).await; + + client +} diff --git a/src/ast/convert_discord.rs b/src/message_ast/convert_discord.rs similarity index 56% rename from src/ast/convert_discord.rs rename to src/message_ast/convert_discord.rs index 870a480..1c6378d 100644 --- a/src/ast/convert_discord.rs +++ b/src/message_ast/convert_discord.rs @@ -41,3 +41,35 @@ pub fn convert_discord(discord_message: &[DiscordComponent<'_>]) -> MessageConte .map(Styled::from) .collect::() } + +pub fn format_discord(message_content: &MessageContent) -> String { + message_content + .iter() + .map(|component| match component { + Styled::Plain(text) => text.to_string(), // TODO: Escape + + Styled::Link { target, .. } => target.to_string(), // TODO: Link text + + Styled::Italic(inner) => format!("*{}*", format_discord(inner)), + Styled::Bold(inner) => format!("**{}**", format_discord(inner)), + Styled::Strikethrough(inner) => format!("~~{}~~", format_discord(inner)), + Styled::Underline(inner) => format!("__{}__", format_discord(inner)), + + Styled::Code(code) => format!("`{}`", code), // TODO: Double-grave delimiting when code contains '`' + Styled::CodeBlock { lang, source } => { + format!( + "```{}\n{}\n```", + lang.as_ref() + .map(|s| s.to_string()) + .unwrap_or("".to_string()), + source.to_string() + ) + } + + Styled::Spoiler { content, .. } => format!("||{}||", format_discord(content)), // TODO: Spoiler reason + + Styled::HardBreak => "\n".to_string(), + Styled::BlockQuote(inner) => format!("> {}", format_discord(inner)), + }) + .collect() +} diff --git a/src/message_ast/convert_plain.rs b/src/message_ast/convert_plain.rs new file mode 100644 index 0000000..1c38551 --- /dev/null +++ b/src/message_ast/convert_plain.rs @@ -0,0 +1,5 @@ +use super::{MessageContent, Styled}; + +pub fn convert_plain(message: &str) -> MessageContent { + vec![Styled::Plain(message.to_string())] +} diff --git a/src/ast/mod.rs b/src/message_ast/mod.rs similarity index 81% rename from src/ast/mod.rs rename to src/message_ast/mod.rs index 33c0d0d..7131671 100644 --- a/src/ast/mod.rs +++ b/src/message_ast/mod.rs @@ -1,4 +1,5 @@ mod convert_discord; +mod convert_plain; pub type MessageContent = Vec; @@ -29,4 +30,5 @@ pub enum Styled { BlockQuote(MessageContent), } -pub use convert_discord::convert_discord; +pub use convert_discord::{convert_discord, format_discord}; +pub use convert_plain::convert_plain;