use std::num::TryFromIntError;
use async_trait::async_trait;
use serenity::{
builder::CreateMessage,
http::Http as Bot,
model::{
channel::Message as DcMessage,
id::{ChannelId, MessageId as DcMessageId, UserId},
},
};
use super::{
error::SinkError,
message::{length_limiter::MessageLengthLimiter, Media, Message, MessageId},
Sink,
};
use crate::utils::OptionExt;
const MAX_MSG_LEN: usize = 2000;
const MAX_EMBED_DESCIPTION_LEN: usize = 2000;
#[derive(Debug)]
pub struct Discord {
bot: Bot,
target: TargetInner,
}
#[derive(Clone, Copy, Debug)]
pub enum Target {
Channel(u64),
User(u64),
}
#[derive(Debug)]
enum TargetInner {
Channel(ChannelId),
User(UserId),
}
impl Discord {
#[must_use]
pub fn new(token: &str, target: Target) -> Self {
Self {
bot: Bot::new(token),
target: match target {
Target::Channel(i) => TargetInner::Channel(i.into()),
Target::User(i) => TargetInner::User(i.into()),
},
}
}
}
#[async_trait]
impl Sink for Discord {
async fn send(
&self,
msg: Message,
reply_to: Option<&MessageId>,
tag: Option<&str>,
) -> Result<Option<MessageId>, SinkError> {
let mut last_message = reply_to.try_map(|msgid| {
let dc_msgid = DcMessageId::from(u64::try_from(msgid.0)?);
Ok::<_, TryFromIntError>(dc_msgid)
})?;
let Message {
title,
body,
link,
media,
} = msg.clone();
if body.as_ref().map_or(0, |s| s.chars().count()) > MAX_EMBED_DESCIPTION_LEN {
let mut head = title;
if let Some(tag) = tag {
let tag = tag.replace(
|c| match c {
'_' => false,
c if c.is_alphabetic() || c.is_ascii_digit() => false,
_ => true,
},
"_",
);
head = Some({
let mut head = head
.map(|mut s| {
s.insert(0, '\n');
s
})
.unwrap_or_default();
head.insert_str(0, &format!("#{tag}\n"));
head
});
}
let link = link.map(|s| s.to_string());
let mut composed_msg = MessageLengthLimiter {
head: head.as_deref(),
body: body.as_deref(),
tail: link.as_deref(),
};
while let Some(text) = composed_msg.split_at(MAX_MSG_LEN) {
let msg = self
.target
.send_message(&self.bot, |msg| msg.content(&text))
.await
.map_err(|e| SinkError::Discord {
source: e,
msg: Box::new(text),
})?;
last_message = Some(msg.id);
}
}
else {
let msg = self
.target
.send_message(&self.bot, |msg| {
msg.embed(|embed| {
if let Some(title) = title {
embed.title(title);
}
if let Some(body) = body {
embed.description(body);
}
if let Some(link) = link {
embed.url(link);
}
if let Some(tag) = tag {
embed.footer(|footer| footer.text(tag));
}
if let Some(media) = media {
for media in media {
if let Media::Photo(image) = media {
embed.image(image);
}
}
}
embed
})
})
.await
.map_err(|e| SinkError::Discord {
source: e,
msg: Box::new(msg),
})?;
last_message = Some(msg.id);
}
let msgid = last_message.map(|id| i64::try_from(id.0).expect("not sure if Discord will ever return an ID that doesn't fit into MessageId. It shouldn't do that, probably...").into());
Ok(msgid)
}
}
impl TargetInner {
async fn send_message<'a, F>(&self, bot: &Bot, f: F) -> Result<DcMessage, serenity::Error>
where
F: for<'b> FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>,
{
let msg = match self {
TargetInner::Channel(chan) => chan.send_message(bot, f).await?,
TargetInner::User(user) => {
user.create_dm_channel(bot)
.await?
.send_message(bot, f)
.await?
}
};
Ok(msg)
}
}