use std::num::TryFromIntError;
use serenity::{
all::{CreateEmbed, CreateEmbedFooter},
builder::CreateMessage,
http::Http as Bot,
model::{
channel::Message as DcMessage,
id::{ChannelId, MessageId as DcMessageId, UserId},
},
};
use super::{
Sink,
error::SinkError,
message::{Media, Message, MessageId, length_limiter::MessageLengthLimiter},
};
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()),
},
}
}
}
impl Sink for Discord {
type Err = SinkError;
async fn send(
&mut self,
msg: &Message,
reply_to: Option<&MessageId>,
tag: Option<&str>,
) -> Result<Option<MessageId>, Self::Err> {
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 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, CreateMessage::new().content(&text))
.await
.map_err(|e| SinkError::Discord {
source: e,
msg: Box::new(text),
})?;
last_message = Some(msg.id);
}
}
else {
let mut embed = CreateEmbed::new();
if let Some(title) = title {
embed = embed.title(title);
}
if let Some(body) = body {
embed = embed.description(body);
}
if let Some(link) = link {
embed = embed.url(link);
}
if let Some(tag) = tag {
embed = embed.footer(CreateEmbedFooter::new(tag));
}
if let Some(media) = media {
for media in media {
if let Media::Photo(image) = media {
embed = embed.image(image);
}
}
}
let msg = self
.target
.send_message(&self.bot, CreateMessage::new().embed(embed))
.await
.map_err(|e| SinkError::Discord {
source: e,
msg: Box::new(msg.clone()),
})?;
last_message = Some(msg.id);
}
let msgid = last_message.map(|id| i64::try_from(id.get()).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(
&self,
bot: &Bot,
message: CreateMessage,
) -> Result<DcMessage, serenity::Error> {
let msg = match self {
TargetInner::Channel(chan) => chan.send_message(bot, message).await?,
TargetInner::User(user) => {
user.create_dm_channel(bot)
.await?
.send_message(bot, message)
.await?
}
};
Ok(msg)
}
}