386 lines
11 KiB
Rust
386 lines
11 KiB
Rust
use log::info;
|
|
use serenity::{async_trait, http::AttachmentType, model::prelude::*, prelude::*};
|
|
use tokio::sync::mpsc;
|
|
|
|
use crate::{
|
|
channels::ChannelReference,
|
|
message_ast::{self, format_discord},
|
|
messages::{
|
|
DeletedMessage, EditedMessage, MessageAuthor, MessageEvent, MessageReference,
|
|
SentImageMessage, 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<Context>,
|
|
event_tx: mpsc::UnboundedSender<MessageEvent>,
|
|
}
|
|
|
|
async fn get_message_author(ctx: &Context, message: &Message) -> MessageAuthor {
|
|
async fn user_tag_color(ctx: &Context, message: &Message) -> Option<String> {
|
|
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) {
|
|
// TODO: Replace with proper management system for linking channels together
|
|
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::SendText(Box::new(SentMessage {
|
|
source: message_ref,
|
|
content,
|
|
author: get_message_author(&ctx, &message).await,
|
|
replies_to,
|
|
})));
|
|
|
|
for attachment in message.attachments.iter() {
|
|
let _ = self
|
|
.event_tx
|
|
.send(MessageEvent::SendImage(Box::new(SentImageMessage {
|
|
source: MessageReference::from(&message),
|
|
author: get_message_author(&ctx, &message).await,
|
|
image_url: attachment.proxy_url.clone(),
|
|
})));
|
|
}
|
|
}
|
|
|
|
async fn message_update(
|
|
&self,
|
|
ctx: Context,
|
|
_old_if_available: Option<Message>,
|
|
new: Option<Message>,
|
|
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::EditText(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<GuildId>,
|
|
) {
|
|
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<Webhook> {
|
|
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<Webhook> {
|
|
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<serde_json::Value> {
|
|
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<MessageReference> {
|
|
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!(
|
|
"{} ({}): {}",
|
|
&message.author.display_name,
|
|
&message.author.service_name,
|
|
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<MessageReference> {
|
|
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 forward_image_to_discord(
|
|
discord_ctx: &Context,
|
|
channel_id: ChannelId,
|
|
image: &SentImageMessage,
|
|
) -> Option<MessageReference> {
|
|
if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await {
|
|
return webhook
|
|
.execute(discord_ctx, true, |w| {
|
|
w.add_file(AttachmentType::Image(&image.image_url))
|
|
.username(format!(
|
|
"{} ({})",
|
|
&image.author.display_name, &image.author.service_name
|
|
))
|
|
.avatar_url(&image.author.avatar_url)
|
|
})
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.as_ref()
|
|
.map(MessageReference::from);
|
|
}
|
|
|
|
channel_id
|
|
.send_message(discord_ctx, |m| {
|
|
m.add_file(AttachmentType::Image(&image.image_url))
|
|
.content(format!(
|
|
"{} ({}):",
|
|
&image.author.display_name, &image.author.service_name
|
|
))
|
|
})
|
|
.await
|
|
.as_ref()
|
|
.ok()
|
|
.map(MessageReference::from)
|
|
}
|
|
|
|
pub async fn create_discord_client(
|
|
ctx_tx: mpsc::UnboundedSender<Context>,
|
|
message_tx: mpsc::UnboundedSender<MessageEvent>,
|
|
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
|
|
}
|