use log::info; use serenity::{async_trait, model::prelude::*, prelude::*}; use tokio::sync::mpsc; use crate::{ channels::ChannelReference, message_ast::{self, format_discord}, messages::{ DeletedMessage, EditedMessage, MessageAuthor, MessageEvent, MessageReference, SentMessage, }, }; pub use serenity::client::Context; pub use serenity::model::id::{ChannelId, MessageId}; impl From<&Message> for MessageReference { fn from(message: &Message) -> Self { Self::Discord(message.channel_id.0, message.id.0) } } struct DiscordHandler { ctx_tx: mpsc::UnboundedSender, event_tx: mpsc::UnboundedSender, } async fn get_message_author(ctx: &Context, message: &Message) -> MessageAuthor { async fn user_tag_color(ctx: &Context, message: &Message) -> Option { Some( message .member(ctx) .await .ok()? .colour(ctx) .await? .hex() .to_ascii_lowercase(), ) } MessageAuthor { display_name: message .author_nick(ctx) .await .unwrap_or_else(|| message.author.name.clone()), avatar_url: message .author .static_avatar_url() .unwrap_or_else(|| message.author.default_avatar_url()), service_name: "discord".to_string(), display_color: user_tag_color(ctx, message).await, } } #[async_trait] impl EventHandler for DiscordHandler { async fn ready(&self, ctx: Context, _ready: Ready) { let _ = self.ctx_tx.send(ctx); info!("Discord ready!"); } async fn message(&self, ctx: Context, message: Message) { if let Some(target) = message.content.strip_prefix("phoebe!link ") { if message .member(&ctx) .await .unwrap() .roles(&ctx) .await .unwrap() .iter() .any(|r| r.name == "Phoebe") { let _ = self.event_tx.send(MessageEvent::AdminLinkChannels(vec![ ChannelReference::Discord(message.channel_id.0), ChannelReference::Matrix(target.to_string()), ])); message.reply(&ctx, "Linking with matrix.").await.unwrap(); return; } } let message_ref = MessageReference::from(&message); let content = discord_message_format::parse(&message.content); let content = message_ast::convert_discord(&content); let replies_to = message .referenced_message .as_ref() .map(|m| MessageReference::from(m.as_ref())); let _ = self.event_tx.send(MessageEvent::Send(Box::new(SentMessage { source: message_ref, content, author: get_message_author(&ctx, &message).await, replies_to, }))); } async fn message_update( &self, ctx: Context, _old_if_available: Option, new: Option, event: MessageUpdateEvent, ) { if let Ok(new_message) = { if let Some(m) = new { Ok(m) } else { event.channel_id.message(&ctx, event.id).await } } { let message_ref = MessageReference::from(&new_message); let content = discord_message_format::parse(&new_message.content); let content = message_ast::convert_discord(&content); let _ = self .event_tx .send(MessageEvent::Edit(Box::new(EditedMessage { replacing: message_ref, content, author: get_message_author(&ctx, &new_message).await, }))); } } async fn message_delete( &self, _ctx: Context, channel_id: ChannelId, deleted_message_id: MessageId, _guild_id: Option, ) { let message_ref = MessageReference::Discord(channel_id.0, deleted_message_id.0); let _ = self .event_tx .send(MessageEvent::Delete(Box::new(DeletedMessage { reference: message_ref, }))); } } async fn get_webhook_for_channel(discord_ctx: &Context, channel: &ChannelId) -> Option { if let Ok(webhooks) = channel.webhooks(discord_ctx).await { for webhook in webhooks { if matches!(webhook.name.as_deref(), Some("phoebe")) { return Some(webhook); } } } None } async fn get_or_create_webhook_for_channel( discord_ctx: &Context, channel: &ChannelId, ) -> Option { if let Some(webhook) = get_webhook_for_channel(discord_ctx, channel).await { return Some(webhook); } if let Ok(webhook) = channel.create_webhook(discord_ctx, "phoebe").await { return Some(webhook); } None } async fn create_webhook_reply_embeds( discord_ctx: &Context, reply: Option<(u64, u64)>, ) -> Vec { if let Some((channel_id, message_id)) = reply { if let Ok(replied_message) = ChannelId(channel_id) .message(discord_ctx, MessageId(message_id)) .await { let replied_author_name = format!( "{} ↩️", replied_message .author_nick(discord_ctx) .await .as_ref() .unwrap_or(&replied_message.author.name) ); let reply_description = format!( "**[Reply to:]({})**\n{}", replied_message.id.link( ChannelId(channel_id), discord_ctx .cache .guild_channel(channel_id) .await .map(|gc| gc.guild_id) ), &replied_message.content ); return vec![Embed::fake(|e| { e.author(|a| { a.icon_url( &replied_message .author .static_avatar_url() .unwrap_or_else(|| replied_message.author.default_avatar_url()), ) .name(replied_author_name) }) .description(reply_description) })]; } } vec![] } pub async fn forward_to_discord( discord_ctx: &Context, channel: ChannelId, message: &SentMessage, reply: Option<(u64, u64)>, ) -> Option { if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel).await { let reply_embeds = create_webhook_reply_embeds(discord_ctx, reply).await; return webhook .execute(discord_ctx, true, |w| { w.content(format_discord(&message.content)) .username(format!( "{} ({})", &message.author.display_name, &message.author.service_name )) .avatar_url(&message.author.avatar_url) .embeds(reply_embeds) }) .await .ok() .flatten() .as_ref() .map(MessageReference::from); } channel .send_message(discord_ctx, |m| { let content = format_discord(&message.content); if let Some((channel_id, message_id)) = reply { m.content(&content) .reference_message((ChannelId(channel_id), MessageId(message_id))) } else { m.content(&content) } }) .await .as_ref() .ok() .map(MessageReference::from) } pub async fn edit_on_discord( discord_ctx: &Context, channel_id: ChannelId, message_id: MessageId, message: &EditedMessage, ) -> Option { if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await { return webhook .edit_message(discord_ctx, message_id, |w| { w.content(format_discord(&message.content)) }) .await .as_ref() .ok() .map(MessageReference::from); } channel_id .edit_message(&discord_ctx, &message_id, |m| { m.content(format_discord(&message.content)) }) .await .as_ref() .ok() .map(MessageReference::from) } pub async fn delete_on_discord( discord_ctx: &Context, channel_id: ChannelId, message_id: MessageId, _message: &DeletedMessage, ) -> bool { if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await { return webhook .delete_message(discord_ctx, message_id) .await .is_ok(); } channel_id .delete_message(&discord_ctx, &message_id) .await .is_ok() } pub async fn create_discord_client( ctx_tx: mpsc::UnboundedSender, message_tx: mpsc::UnboundedSender, token: &str, ) -> Client { let handler = DiscordHandler { ctx_tx, event_tx: message_tx, }; info!("Discord logging in…"); let client = Client::builder(token) .event_handler(handler) .await .expect("Failed to create discord client"); info!("Discord starting…"); client }