diff --git a/.gitignore b/.gitignore index ea8c4bf..e906ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/credentials.txt +/matrix_state diff --git a/Cargo.lock b/Cargo.lock index f8e40ca..c2e8326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -285,17 +296,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "command_attr" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6c3666f685cb1efc0628b8c984dbad9c372d080450736c7732089c385ed81d" -dependencies = [ - "proc-macro2 1.0.28", - "quote 1.0.9", - "syn 1.0.74", -] - [[package]] name = "const-oid" version = "0.6.0" @@ -518,6 +518,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "flate2" version = "1.0.20" @@ -869,6 +882,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.11" @@ -1467,12 +1486,13 @@ version = "0.1.0" dependencies = [ "bincode", "discord_message_format", + "env_logger", + "log", "matrix-sdk", "serde", "serenity", "sled", "tokio", - "tracing", "url", ] @@ -2224,19 +2244,16 @@ dependencies = [ "bitflags", "bytes 1.0.1", "chrono", - "command_attr", "flate2", "futures", "percent-encoding", "reqwest", "serde", "serde_json", - "static_assertions", "tokio", "tracing", "typemap_rev", "url", - "uwl", ] [[package]] @@ -2348,12 +2365,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stdweb" version = "0.4.20" @@ -2457,6 +2468,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.26" @@ -2801,12 +2821,6 @@ dependencies = [ "serde", ] -[[package]] -name = "uwl" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" - [[package]] name = "vcpkg" version = "0.2.15" @@ -2975,6 +2989,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 24ad372..c4e8706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,16 @@ bincode = "1.3.3" discord_message_format = { git = "https://git.lavender.software/charlotte/discord-message-format.git" } matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git" } serde = { version = "1.0.129", features = ["derive"] } -serenity = "0.10.9" sled = "0.34.6" tokio = { version = "1.10.1", features = ["full"] } -tracing = "0.1.26" url = "2.2.2" +log = "0.4.14" +env_logger = "0.9.0" + +[dependencies.serenity] +version = "0.10.9" +default-features = false +features = ["builder", "cache", "client", "gateway", "model", "http", "utils", "rustls_backend"] [patch.crates-io] olm-sys = { path = "./target/patch/olm-sys-1.1.2" } diff --git a/src/bridgers.rs b/src/bridgers.rs new file mode 100644 index 0000000..a9075bf --- /dev/null +++ b/src/bridgers.rs @@ -0,0 +1,77 @@ +use std::{cell::RefCell, str::FromStr, sync::Mutex}; + +use matrix_sdk::ruma::{ + events::{room::message::MessageEventContent, AnyMessageEventContent}, + RoomId, +}; +use serenity::model::id::ChannelId; + +use crate::{ + discord, matrix, + message_ast::{self, format_discord, MessageContent}, + messages::MessageReference, +}; + +pub struct Bridgers { + pub discord: Mutex>>, + pub matrix: Mutex>>, +} + +impl Bridgers { + pub fn new() -> Self { + Self { + discord: Mutex::new(RefCell::new(None)), + matrix: Mutex::new(RefCell::new(None)), + } + } + + pub 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(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("!SjQatGOikRshcWNcln:matrix.org").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 + } +} diff --git a/src/discord.rs b/src/discord.rs index 592e04a..0bba73f 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,8 +1,11 @@ +use log::info; use serenity::{async_trait, model::prelude::*, prelude::*}; use tokio::sync::mpsc; -use tracing::info; -use crate::{message_ast, MessageReference, SentMessage}; +use crate::{ + message_ast, + messages::{MessageAuthor, MessageReference, SentMessage}, +}; pub use serenity::client::Context; @@ -22,13 +25,17 @@ struct DiscordHandler { #[async_trait] impl EventHandler for DiscordHandler { async fn ready(&self, ctx: Context, _ready: Ready) { - info!("Discord side: Ready"); - let _ = self.ctx_tx.send(ctx); + info!("Discord ready!"); + // TODO: Scan for channels to link } - async fn message(&self, _ctx: Context, message: Message) { + async fn message(&self, ctx: Context, message: Message) { + if message.author.id == ctx.cache.current_user_id().await { + return; + } + let message_ref = MessageReference::from(&message); // TODO: Store this message ref & associations in the DB @@ -38,6 +45,12 @@ impl EventHandler for DiscordHandler { let _ = self.message_tx.send(SentMessage { source: message_ref, content, + author: MessageAuthor { + display_name: message + .author_nick(&ctx.http) + .await + .unwrap_or(message.author.name), + }, }); } } @@ -49,8 +62,12 @@ pub async fn create_discord_client( ) -> Client { let handler = DiscordHandler { ctx_tx, message_tx }; - Client::builder(token) + info!("Discord logging in…"); + let client = Client::builder(token) .event_handler(handler) .await - .expect("Failed to create discord client") + .expect("Failed to create discord client"); + info!("Discord starting…"); + + client } diff --git a/src/main.rs b/src/main.rs index 7f0af64..e8c561d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,104 +1,20 @@ -use std::{ - cell::RefCell, - str::FromStr, - sync::{Arc, Mutex}, -}; +use std::sync::Arc; 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 discord; -mod matrix; +mod bridgers; mod message_ast; +mod messages; -use message_ast::MessageContent; -use serenity::model::id::ChannelId; +pub mod discord; +pub mod matrix; + +use bridgers::Bridgers; +use messages::SentMessage; use tokio::sync::mpsc; -#[derive(Serialize, Deserialize)] -pub enum MessageReference { - Discord(u64, u64), - Matrix(String, String), -} - -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 - } -} +use crate::message_ast::Styled; async fn setup_discord( token: String, @@ -127,7 +43,6 @@ async fn setup_matrix( 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 @@ -135,6 +50,7 @@ async fn setup_matrix( .unwrap() .replace(Some(client.clone())); + let settings = matrix_sdk::SyncSettings::default().token(client.sync_token().await.unwrap()); tokio::spawn(async move { client.sync(settings).await; }); @@ -142,27 +58,41 @@ async fn setup_matrix( #[tokio::main] async fn main() { + env_logger::init(); + let bridgers = Arc::new(Bridgers::new()); let (message_tx, mut message_rx) = tokio::sync::mpsc::unbounded_channel::(); + #[inline] + fn get_env_var(key: &str) -> String { + std::env::var(key).expect("DISCORD_TOKEN not set in environment") + } + setup_discord( - "token".to_string(), + get_env_var("DISCORD_TOKEN"), Arc::clone(&bridgers), message_tx.clone(), ) .await; setup_matrix( - "https://matrix.org".to_string(), - "username".to_string(), - "password".to_string(), + get_env_var("MATRIX_HOMESERVER"), + get_env_var("MATRIX_USERNAME"), + get_env_var("MATRIX_PASSWORD"), Arc::clone(&bridgers), message_tx.clone(), ) .await; while let Some(message) = message_rx.recv().await { - let _ = bridgers.send_message(message.source, message.content).await; + let mut content = message.content; + content.insert(0, Styled::Plain(": ".to_string())); + content.insert( + 0, + Styled::Bold(vec![Styled::Plain(message.author.display_name.to_string())]), + ); + + let _ = bridgers.send_message(message.source, content).await; } } diff --git a/src/matrix.rs b/src/matrix.rs index 6121f5d..4ba43ef 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -9,16 +9,20 @@ use matrix_sdk::{ }, AnyMessageEventContent, AnySyncRoomEvent, SyncMessageEvent, }, - EventId, RoomId, + EventId, RoomId, UserId, }, ClientConfig, EventHandler, SyncSettings, }; +use log::info; pub use matrix_sdk::Client; use tokio::sync::mpsc; use url::Url; -use crate::{message_ast::convert_plain, MessageReference, SentMessage}; +use crate::{ + message_ast::convert_plain, + messages::{MessageAuthor, MessageReference, SentMessage}, +}; impl From<(&RoomId, &EventId)> for MessageReference { fn from((room_id, event_id): (&RoomId, &EventId)) -> Self { @@ -61,45 +65,61 @@ fn _find_content(event: &AnySyncRoomEvent) -> Option { struct MatrixHandler { message_tx: mpsc::UnboundedSender, + current_user_id: UserId, } #[async_trait] impl EventHandler for MatrixHandler { async fn on_room_message(&self, room: Room, event: &SyncMessageEvent) { - let message_ref = MessageReference::from((room.room_id(), &event.event_id)); + if event.sender == self.current_user_id { + return; + } - let message_type = - if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { - &replacement.new_content.msgtype - } else { - &event.content.msgtype - }; + if let Room::Joined(room) = room { + let message_ref = MessageReference::from((room.room_id(), &event.event_id)); - 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) - { - todo!("Parse html_body into MessageContent AST") + let message_type = + if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { + &replacement.new_content.msgtype } else { - convert_plain(&text.body) + &event.content.msgtype }; - let _ = self.message_tx.send(SentMessage { - source: message_ref, - 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) + { + // TODO: Parse html_body into MessageContent AST + convert_plain(&text.body) + } else { + convert_plain(&text.body) + }; - MessageType::Emote(_emote) => { - // TODO - } + if let Ok(Some(sender)) = room.get_member(&event.sender).await { + let _ = self.message_tx.send(SentMessage { + source: message_ref, + content, + author: MessageAuthor { + display_name: sender + .display_name() + .unwrap_or(sender.name()) + .to_string(), + }, + }); + } + } - _ => {} - }; + MessageType::Emote(_emote) => { + // TODO + } + + _ => {} + }; + } } } @@ -109,20 +129,27 @@ pub async fn create_matrix_client( password: String, message_tx: mpsc::UnboundedSender, ) -> Client { - let client_config = ClientConfig::new().store_path("./matrix"); + let client_config = ClientConfig::new().store_path("./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 = MatrixHandler { message_tx }; + let current_user_id = client.user_id().await.unwrap(); + + let event_handler = MatrixHandler { + message_tx, + current_user_id, + }; client.set_event_handler(Box::new(event_handler)).await; client diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 0000000..242fff1 --- /dev/null +++ b/src/messages.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::message_ast::MessageContent; + +#[derive(Serialize, Deserialize)] +pub enum MessageReference { + Discord(u64, u64), + Matrix(String, String), +} + +pub struct MessageAuthor { + pub display_name: String, +} + +pub struct SentMessage { + pub source: MessageReference, + pub content: MessageContent, + pub author: MessageAuthor, +}