use std::fmt::{self as fmt, Write};
use std::ops::Add;
use crate::model::guild::Emoji;
use crate::model::id::{ChannelId, RoleId, UserId};
use crate::model::mention::Mentionable;
#[derive(Clone, Debug, Default)]
pub struct MessageBuilder(pub String);
impl MessageBuilder {
#[must_use]
pub fn new() -> MessageBuilder {
MessageBuilder::default()
}
pub fn build(&mut self) -> String {
self.clone().0
}
#[inline]
pub fn channel<C: Into<ChannelId>>(&mut self, channel: C) -> &mut Self {
self._channel(channel.into())
}
fn _channel(&mut self, channel: ChannelId) -> &mut Self {
self.0.push_str(&channel.mention().to_string());
self
}
pub fn emoji(&mut self, emoji: &Emoji) -> &mut Self {
self.0.push_str(&emoji.to_string());
self
}
pub fn mention<M: Mentionable>(&mut self, item: &M) -> &mut Self {
self.0.push_str(&item.mention().to_string());
self
}
#[inline]
pub fn push(&mut self, content: impl Into<Content>) -> &mut Self {
self._push(&content.into().to_string())
}
fn _push(&mut self, content: &str) -> &mut Self {
self.0.push_str(content);
self
}
pub fn push_codeblock(
&mut self,
content: impl Into<Content>,
language: Option<&str>,
) -> &mut Self {
self.0.push_str("```");
if let Some(language) = language {
self.0.push_str(language);
}
self.0.push('\n');
self.0.push_str(&content.into().to_string());
self.0.push_str("\n```");
self
}
pub fn push_mono(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push('`');
self.0.push_str(&content.into().to_string());
self.0.push('`');
self
}
pub fn push_italic(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push('_');
self.0.push_str(&content.into().to_string());
self.0.push('_');
self
}
pub fn push_bold(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("**");
self.0.push_str(&content.into().to_string());
self.0.push_str("**");
self
}
pub fn push_underline(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("__");
self.0.push_str(&content.into().to_string());
self.0.push_str("__");
self
}
pub fn push_strike(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("~~");
self.0.push_str(&content.into().to_string());
self.0.push_str("~~");
self
}
pub fn push_spoiler(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("||");
self.0.push_str(&content.into().to_string());
self.0.push_str("||");
self
}
pub fn push_quote(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("> ");
self.0.push_str(&content.into().to_string());
self
}
pub fn push_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push(content);
self.0.push('\n');
self
}
pub fn push_mono_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_mono(content);
self.0.push('\n');
self
}
pub fn push_italic_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_italic(content);
self.0.push('\n');
self
}
pub fn push_bold_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_bold(content);
self.0.push('\n');
self
}
pub fn push_underline_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_underline(content);
self.0.push('\n');
self
}
pub fn push_strike_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_strike(content);
self.0.push('\n');
self
}
pub fn push_spoiler_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_spoiler(content);
self.0.push('\n');
self
}
pub fn push_quote_line(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_quote(content);
self.0.push('\n');
self
}
pub fn push_safe(&mut self, content: impl Into<Content>) -> &mut Self {
{
let mut c = content.into();
c.inner =
normalize(&c.inner).replace('*', "\\*").replace('`', "\\`").replace('_', "\\_");
self.0.push_str(&c.to_string());
}
self
}
pub fn push_codeblock_safe(
&mut self,
content: impl Into<Content>,
language: Option<&str>,
) -> &mut Self {
self.0.push_str("```");
if let Some(language) = language {
self.0.push_str(language);
}
self.0.push('\n');
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("```", " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("\n```");
self
}
pub fn push_mono_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push('`');
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace('`', "'");
self.0.push_str(&c.to_string());
}
self.0.push('`');
self
}
pub fn push_italic_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push('_');
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace('_', " ");
self.0.push_str(&c.to_string());
}
self.0.push('_');
self
}
pub fn push_bold_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("**");
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("**", " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("**");
self
}
pub fn push_underline_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("__");
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("__", " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("__");
self
}
pub fn push_strike_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("~~");
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("~~", " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("~~");
self
}
pub fn push_spoiler_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("||");
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("||", " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("||");
self
}
pub fn push_quote_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.0.push_str("> ");
{
let mut c = content.into();
c.inner = normalize(&c.inner).replace("> ", " ");
self.0.push_str(&c.to_string());
}
self
}
pub fn push_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_safe(content);
self.0.push('\n');
self
}
pub fn push_mono_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_mono_safe(content);
self.0.push('\n');
self
}
pub fn push_italic_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_italic_safe(content);
self.0.push('\n');
self
}
pub fn push_bold_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_bold_safe(content);
self.0.push('\n');
self
}
pub fn push_underline_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_underline_safe(content);
self.0.push('\n');
self
}
pub fn push_strike_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_strike_safe(content);
self.0.push('\n');
self
}
pub fn push_spoiler_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_spoiler_safe(content);
self.0.push('\n');
self
}
pub fn push_quote_line_safe(&mut self, content: impl Into<Content>) -> &mut Self {
self.push_quote_safe(content);
self.0.push('\n');
self
}
pub fn quote_rest(&mut self) -> &mut Self {
self.0.push_str("\n>>> ");
self
}
pub fn role<R: Into<RoleId>>(&mut self, role: R) -> &mut Self {
self.0.push_str(&role.into().mention().to_string());
self
}
pub fn user<U: Into<UserId>>(&mut self, user: U) -> &mut Self {
self.0.push_str(&user.into().mention().to_string());
self
}
}
impl fmt::Display for MessageBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
pub trait EmbedMessageBuilding {
fn push_named_link(&mut self, name: impl Into<Content>, url: impl Into<Content>) -> &mut Self;
fn push_named_link_safe(
&mut self,
name: impl Into<Content>,
url: impl Into<Content>,
) -> &mut Self;
}
impl EmbedMessageBuilding for MessageBuilder {
fn push_named_link(&mut self, name: impl Into<Content>, url: impl Into<Content>) -> &mut Self {
let name = name.into().to_string();
let url = url.into().to_string();
write!(self.0, "[{}]({})", name, url).unwrap();
self
}
fn push_named_link_safe(
&mut self,
name: impl Into<Content>,
url: impl Into<Content>,
) -> &mut Self {
self.0.push('[');
{
let mut c = name.into();
c.inner = normalize(&c.inner).replace(']', " ");
self.0.push_str(&c.to_string());
}
self.0.push_str("](");
{
let mut c = url.into();
c.inner = normalize(&c.inner).replace(')', " ");
self.0.push_str(&c.to_string());
}
self.0.push(')');
self
}
}
#[non_exhaustive]
pub enum ContentModifier {
Italic,
Bold,
Strikethrough,
Code,
Underline,
Spoiler,
}
#[derive(Debug, Default, Clone)]
pub struct Content {
pub italic: bool,
pub bold: bool,
pub strikethrough: bool,
pub inner: String,
pub code: bool,
pub underline: bool,
pub spoiler: bool,
}
impl<T: ToString> Add<T> for Content {
type Output = Content;
fn add(mut self, rhs: T) -> Content {
self.inner += &rhs.to_string();
self
}
}
impl<T: ToString> Add<T> for ContentModifier {
type Output = Content;
fn add(self, rhs: T) -> Content {
let mut nc = self.to_content();
nc.inner += &rhs.to_string();
nc
}
}
impl Add<ContentModifier> for Content {
type Output = Content;
fn add(mut self, rhs: ContentModifier) -> Content {
self.apply(&rhs);
self
}
}
impl Add<ContentModifier> for ContentModifier {
type Output = Content;
fn add(self, rhs: ContentModifier) -> Content {
let mut nc = self.to_content();
nc.apply(&rhs);
nc
}
}
impl ContentModifier {
fn to_content(&self) -> Content {
let mut nc = Content::default();
nc.apply(self);
nc
}
}
impl Content {
pub fn apply(&mut self, modifier: &ContentModifier) {
match *modifier {
ContentModifier::Italic => {
self.italic = true;
},
ContentModifier::Bold => {
self.bold = true;
},
ContentModifier::Strikethrough => {
self.strikethrough = true;
},
ContentModifier::Code => {
self.code = true;
},
ContentModifier::Underline => {
self.underline = true;
},
ContentModifier::Spoiler => {
self.spoiler = true;
},
}
}
#[allow(clippy::inherent_to_string)]
#[must_use]
pub fn to_string(&self) -> String {
trait UnwrapWith {
fn unwrap_with(&self, n: usize) -> usize;
}
impl UnwrapWith for bool {
fn unwrap_with(&self, n: usize) -> usize {
if *self {
n
} else {
0
}
}
}
let capacity = self.inner.len()
+ self.spoiler.unwrap_with(4)
+ self.bold.unwrap_with(4)
+ self.italic.unwrap_with(2)
+ self.strikethrough.unwrap_with(4)
+ self.underline.unwrap_with(4)
+ self.code.unwrap_with(2);
let mut new_str = String::with_capacity(capacity);
if self.spoiler {
new_str.push_str("||");
}
if self.bold {
new_str.push_str("**");
}
if self.italic {
new_str.push('*');
}
if self.strikethrough {
new_str.push_str("~~");
}
if self.underline {
new_str.push_str("__");
}
if self.code {
new_str.push('`');
}
new_str.push_str(&self.inner);
if self.code {
new_str.push('`');
}
if self.underline {
new_str.push_str("__");
}
if self.strikethrough {
new_str.push_str("~~");
}
if self.italic {
new_str.push('*');
}
if self.bold {
new_str.push_str("**");
}
if self.spoiler {
new_str.push_str("||");
}
new_str
}
}
impl<T: fmt::Display> From<T> for Content {
fn from(t: T) -> Content {
Content {
italic: false,
bold: false,
strikethrough: false,
inner: t.to_string(),
code: false,
underline: false,
spoiler: false,
}
}
}
fn normalize(text: &str) -> String {
text.replace("discord.gg", "discord\u{2024}gg")
.replace("discord.me", "discord\u{2024}me")
.replace("discordlist.net", "discordlist\u{2024}net")
.replace("discordservers.com", "discordservers\u{2024}com")
.replace("discord.com/invite", "discord\u{2024}com/invite")
.replace("discordapp.com/invite", "discordapp\u{2024}com/invite")
.replace('\u{202E}', " ") .replace('\u{200F}', " ") .replace('\u{202B}', " ") .replace('\u{200B}', " ") .replace('\u{200D}', " ") .replace('\u{200C}', " ") .replace("@everyone", "@\u{200B}everyone")
.replace("@here", "@\u{200B}here")
}
#[cfg(test)]
mod test {
use super::ContentModifier::{Bold, Code, Italic, Spoiler};
use super::MessageBuilder;
use crate::model::prelude::*;
macro_rules! gen {
($($fn:ident => [$($text:expr => $expected:expr),+]),+) => ({
$(
$(
assert_eq!(MessageBuilder::new().$fn($text).0, $expected);
)+
)+
});
}
#[test]
fn code_blocks() {
let content = MessageBuilder::new().push_codeblock("test", Some("rb")).build();
assert_eq!(content, "```rb\ntest\n```");
}
#[test]
fn safe_content() {
let content = MessageBuilder::new().push_safe("@everyone discord.gg/discord-api").build();
assert_ne!(content, "@everyone discord.gg/discord-api");
}
#[test]
fn no_free_formatting() {
let content = MessageBuilder::new().push_bold_safe("test**test").build();
assert_ne!(content, "**test**test**");
}
#[test]
fn mentions() {
let content_emoji = MessageBuilder::new()
.emoji(&Emoji {
animated: false,
available: true,
id: EmojiId(32),
name: "Rohrkatze".to_string(),
managed: false,
require_colons: true,
roles: vec![],
user: None,
})
.build();
let content_mentions =
MessageBuilder::new().channel(1).mention(&UserId(2)).role(3).user(4).build();
assert_eq!(content_mentions, "<#1><@2><@&3><@4>");
assert_eq!(content_emoji, "<:Rohrkatze:32>");
}
#[test]
fn content() {
let content = Bold + Italic + Code + "Fun!";
assert_eq!(content.to_string(), "***`Fun!`***");
let content = Spoiler + Bold + "Divert your eyes elsewhere";
assert_eq!(content.to_string(), "||**Divert your eyes elsewhere**||");
}
#[test]
fn init() {
assert_eq!(MessageBuilder::new().0, "");
assert_eq!(MessageBuilder::default().0, "");
}
#[test]
fn message_content() {
let message_content = MessageBuilder::new().push(Bold + Italic + Code + "Fun!").build();
assert_eq!(message_content, "***`Fun!`***");
}
#[test]
fn message_content_safe() {
let message_content = MessageBuilder::new().push_safe(Bold + Italic + "test**test").build();
assert_eq!(message_content, "***test\\*\\*test***");
}
#[test]
fn push() {
assert_eq!(MessageBuilder::new().push('a').0, "a");
assert!(MessageBuilder::new().push("").0.is_empty());
}
#[test]
fn push_codeblock() {
let content = &MessageBuilder::new().push_codeblock("foo", None).0.clone();
assert_eq!(content, "```\nfoo\n```");
let content = &MessageBuilder::new().push_codeblock("fn main() { }", Some("rs")).0.clone();
assert_eq!(content, "```rs\nfn main() { }\n```");
}
#[test]
fn push_codeblock_safe() {
assert_eq!(
MessageBuilder::new().push_codeblock_safe("foo", Some("rs")).0,
"```rs\nfoo\n```",
);
assert_eq!(MessageBuilder::new().push_codeblock_safe("", None).0, "```\n\n```",);
assert_eq!(MessageBuilder::new().push_codeblock_safe("1 * 2", None).0, "```\n1 * 2\n```",);
assert_eq!(
MessageBuilder::new().push_codeblock_safe("`1 * 3`", None).0,
"```\n`1 * 3`\n```",
);
assert_eq!(MessageBuilder::new().push_codeblock_safe("```.```", None).0, "```\n . \n```",);
}
#[test]
fn push_safe() {
gen! {
push_safe => [
"" => "",
"foo" => "foo",
"1 * 2" => "1 \\* 2"
],
push_bold_safe => [
"" => "****",
"foo" => "**foo**",
"*foo*" => "***foo***",
"f*o**o" => "**f*o o**"
],
push_italic_safe => [
"" => "__",
"foo" => "_foo_",
"f_o_o" => "_f o o_"
],
push_mono_safe => [
"" => "``",
"foo" => "`foo`",
"asterisk *" => "`asterisk *`",
"`ticks`" => "`'ticks'`"
],
push_strike_safe => [
"" => "~~~~",
"foo" => "~~foo~~",
"foo ~" => "~~foo ~~~",
"~~foo" => "~~ foo~~",
"~~fo~~o~~" => "~~ fo o ~~"
],
push_underline_safe => [
"" => "____",
"foo" => "__foo__",
"foo _" => "__foo ___",
"__foo__ bar" => "__ foo bar__"
],
push_spoiler_safe => [
"" => "||||",
"foo" => "||foo||",
"foo |" => "||foo |||",
"||foo|| bar" =>"|| foo bar||"
],
push_line_safe => [
"" => "\n",
"foo" => "foo\n",
"1 * 2" => "1 \\* 2\n"
],
push_mono_line_safe => [
"" => "``\n",
"a ` b `" => "`a ' b '`\n"
],
push_italic_line_safe => [
"" => "__\n",
"a * c" => "_a * c_\n"
],
push_bold_line_safe => [
"" => "****\n",
"a ** d" => "**a d**\n"
],
push_underline_line_safe => [
"" => "____\n",
"a __ e" => "__a e__\n"
],
push_strike_line_safe => [
"" => "~~~~\n",
"a ~~ f" => "~~a f~~\n"
],
push_spoiler_line_safe => [
"" => "||||\n",
"a || f" => "||a f||\n"
]
};
}
#[test]
fn push_unsafe() {
gen! {
push_bold => [
"a" => "**a**",
"" => "****",
'*' => "*****",
"**" => "******"
],
push_bold_line => [
"" => "****\n",
"foo" => "**foo**\n"
],
push_italic => [
"a" => "_a_",
"" => "__",
"_" => "___",
"__" => "____"
],
push_italic_line => [
"" => "__\n",
"foo" => "_foo_\n",
"_?" => "__?_\n"
],
push_line => [
"" => "\n",
"foo" => "foo\n",
"\n\n" => "\n\n\n",
"\nfoo\n" => "\nfoo\n\n"
],
push_mono => [
"a" => "`a`",
"" => "``",
"`" => "```",
"``" => "````"
],
push_mono_line => [
"" => "``\n",
"foo" => "`foo`\n",
"\n" => "`\n`\n",
"`\n`\n" => "``\n`\n`\n"
],
push_strike => [
"a" => "~~a~~",
"" => "~~~~",
"~" => "~~~~~",
"~~" => "~~~~~~"
],
push_strike_line => [
"" => "~~~~\n",
"foo" => "~~foo~~\n"
],
push_underline => [
"a" => "__a__",
"" => "____",
"_" => "_____",
"__" => "______"
],
push_underline_line => [
"" => "____\n",
"foo" => "__foo__\n"
],
push_spoiler => [
"a" => "||a||",
"" => "||||",
"|" => "|||||",
"||" => "||||||"
],
push_spoiler_line => [
"" => "||||\n",
"foo" => "||foo||\n"
]
};
}
#[test]
fn normalize() {
assert_eq!(super::normalize("@everyone"), "@\u{200B}everyone");
assert_eq!(super::normalize("@here"), "@\u{200B}here");
assert_eq!(super::normalize("discord.gg"), "discord\u{2024}gg");
assert_eq!(super::normalize("discord.me"), "discord\u{2024}me");
assert_eq!(super::normalize("discordlist.net"), "discordlist\u{2024}net");
assert_eq!(super::normalize("discordservers.com"), "discordservers\u{2024}com");
assert_eq!(super::normalize("discord.com/invite"), "discord\u{2024}com/invite");
assert_eq!(super::normalize("\u{202E}"), " ");
assert_eq!(super::normalize("\u{200F}"), " ");
assert_eq!(super::normalize("\u{202B}"), " ");
assert_eq!(super::normalize("\u{200B}"), " ");
assert_eq!(super::normalize("\u{200D}"), " ");
assert_eq!(super::normalize("\u{200C}"), " ");
}
}