Implement the... bridging
parent
97b98aff3e
commit
5a6385f0b0
|
@ -1475,6 +1475,7 @@ dependencies = [
|
||||||
"sled",
|
"sled",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -12,3 +12,4 @@ serenity = "0.10.8"
|
||||||
sled = "0.34.6"
|
sled = "0.34.6"
|
||||||
tokio = { version = "1.8.0", features = ["full"] }
|
tokio = { version = "1.8.0", features = ["full"] }
|
||||||
tracing = "0.1.26"
|
tracing = "0.1.26"
|
||||||
|
url = "2.2.2"
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use serenity::{async_trait, model::prelude::*, prelude::*};
|
use serenity::{async_trait, model::prelude::*, prelude::*};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{ast, MessageReference};
|
use crate::{message_ast, MessageReference, SentMessage};
|
||||||
|
|
||||||
|
pub use serenity::client::Context;
|
||||||
|
|
||||||
impl From<&Message> for MessageReference {
|
impl From<&Message> for MessageReference {
|
||||||
fn from(message: &Message) -> Self {
|
fn from(message: &Message) -> Self {
|
||||||
|
@ -11,22 +14,43 @@ impl From<&Message> for MessageReference {
|
||||||
|
|
||||||
// TODO: Some way to emit messages for matrix,
|
// TODO: Some way to emit messages for matrix,
|
||||||
// and some way to receive messages from matrix.
|
// and some way to receive messages from matrix.
|
||||||
struct DiscordSide {}
|
struct DiscordHandler {
|
||||||
|
ctx_tx: mpsc::UnboundedSender<Context>,
|
||||||
|
message_tx: mpsc::UnboundedSender<SentMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventHandler for DiscordSide {
|
impl EventHandler for DiscordHandler {
|
||||||
async fn ready(&self, _ctx: Context, _ready: Ready) {
|
async fn ready(&self, ctx: Context, _ready: Ready) {
|
||||||
info!("Discord side: 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) {
|
async fn message(&self, _ctx: Context, message: Message) {
|
||||||
let _message_ref = MessageReference::from(&message);
|
let message_ref = MessageReference::from(&message);
|
||||||
// TODO: Store this message ref & associations in the DB
|
// TODO: Store this message ref & associations in the DB
|
||||||
|
|
||||||
let content = discord_message_format::parse(&message.content);
|
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<Context>,
|
||||||
|
message_tx: mpsc::UnboundedSender<SentMessage>,
|
||||||
|
token: &str,
|
||||||
|
) -> Client {
|
||||||
|
let handler = DiscordHandler { ctx_tx, message_tx };
|
||||||
|
|
||||||
|
Client::builder(token)
|
||||||
|
.event_handler(handler)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create discord client")
|
||||||
|
}
|
||||||
|
|
159
src/main.rs
159
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod ast;
|
|
||||||
mod discord;
|
mod discord;
|
||||||
mod matrix;
|
mod matrix;
|
||||||
|
mod message_ast;
|
||||||
|
|
||||||
|
use message_ast::MessageContent;
|
||||||
|
use serenity::model::id::ChannelId;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum MessageReference {
|
pub enum MessageReference {
|
||||||
|
@ -10,6 +29,140 @@ pub enum MessageReference {
|
||||||
Matrix(String, String),
|
Matrix(String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
pub struct SentMessage {
|
||||||
println!("Hello, world!");
|
pub source: MessageReference,
|
||||||
|
pub content: MessageContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bridgers {
|
||||||
|
discord: Mutex<RefCell<Option<discord::Context>>>,
|
||||||
|
matrix: Mutex<RefCell<Option<matrix::Client>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MessageReference> {
|
||||||
|
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<ChannelId> 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<Bridgers>,
|
||||||
|
discord_tx: mpsc::UnboundedSender<SentMessage>,
|
||||||
|
) {
|
||||||
|
let (discord_ctx_tx, mut discord_ctx_rx) = mpsc::unbounded_channel::<discord::Context>();
|
||||||
|
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<Bridgers>,
|
||||||
|
message_tx: mpsc::UnboundedSender<SentMessage>,
|
||||||
|
) {
|
||||||
|
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::<SentMessage>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
138
src/matrix.rs
138
src/matrix.rs
|
@ -2,58 +2,128 @@ use matrix_sdk::{
|
||||||
async_trait,
|
async_trait,
|
||||||
room::Room,
|
room::Room,
|
||||||
ruma::{
|
ruma::{
|
||||||
api::client::r0::room::get_room_event,
|
|
||||||
events::{
|
events::{
|
||||||
room::message::{MessageEventContent, MessageFormat, MessageType},
|
room::{
|
||||||
AnyMessageEvent, AnyRoomEvent, SyncMessageEvent,
|
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<MessageEventContent>)> for MessageReference {
|
use crate::{message_ast::convert_plain, MessageReference, SentMessage};
|
||||||
fn from((room, event): (&Room, &SyncMessageEvent<MessageEventContent>)) -> Self {
|
|
||||||
let room_string = room.room_id().as_str().to_string();
|
impl From<(&RoomId, &EventId)> for MessageReference {
|
||||||
let event_string = event.event_id.as_str().to_string();
|
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)
|
Self::Matrix(room_string, event_string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MatrixHandler;
|
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<SentMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventHandler for MatrixHandler {
|
impl EventHandler for MatrixHandler {
|
||||||
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
||||||
let event_id = &event.event_id;
|
let message_ref = MessageReference::from((room.room_id(), &event.event_id));
|
||||||
|
|
||||||
if let Room::Joined(room) = room {
|
let message_type =
|
||||||
let event = room
|
if let Some(Relation::Replacement(replacement)) = &event.content.relates_to {
|
||||||
.event(get_room_event::Request::new(room.room_id(), event_id))
|
&replacement.new_content.msgtype
|
||||||
.await
|
} else {
|
||||||
.unwrap()
|
&event.content.msgtype
|
||||||
.event
|
};
|
||||||
.deserialize()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) = event {
|
match message_type {
|
||||||
if let MessageEventContent {
|
MessageType::Text(text) => {
|
||||||
msgtype: MessageType::Text(text_content),
|
let content = if let Some(_html) = text
|
||||||
..
|
.formatted
|
||||||
} = &message.content
|
.as_ref()
|
||||||
|
.filter(|f| f.format == MessageFormat::Html)
|
||||||
|
.map(|f| &f.body)
|
||||||
{
|
{
|
||||||
if let Some(html_body) = text_content
|
todo!("Parse html_body into MessageContent AST")
|
||||||
.formatted
|
} else {
|
||||||
.as_ref()
|
convert_plain(&text.body)
|
||||||
.filter(|f| f.format == MessageFormat::Html)
|
};
|
||||||
.map(|f| &f.body)
|
|
||||||
{
|
let _ = self.message_tx.send(SentMessage {
|
||||||
// TODO: Parse the html_body into the AST
|
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<SentMessage>,
|
||||||
|
) -> 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
|
||||||
|
}
|
||||||
|
|
|
@ -41,3 +41,35 @@ pub fn convert_discord(discord_message: &[DiscordComponent<'_>]) -> MessageConte
|
||||||
.map(Styled::from)
|
.map(Styled::from)
|
||||||
.collect::<MessageContent>()
|
.collect::<MessageContent>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
use super::{MessageContent, Styled};
|
||||||
|
|
||||||
|
pub fn convert_plain(message: &str) -> MessageContent {
|
||||||
|
vec![Styled::Plain(message.to_string())]
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
mod convert_discord;
|
mod convert_discord;
|
||||||
|
mod convert_plain;
|
||||||
|
|
||||||
pub type MessageContent = Vec<Styled>;
|
pub type MessageContent = Vec<Styled>;
|
||||||
|
|
||||||
|
@ -29,4 +30,5 @@ pub enum Styled {
|
||||||
BlockQuote(MessageContent),
|
BlockQuote(MessageContent),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use convert_discord::convert_discord;
|
pub use convert_discord::{convert_discord, format_discord};
|
||||||
|
pub use convert_plain::convert_plain;
|
Loading…
Reference in New Issue