2022-06-17 00:56:10 -05:00
|
|
|
use crate::{
|
2022-08-02 18:57:43 -05:00
|
|
|
regex, ChannelMappingKey, MembersKey, OptionReplacer, OptionStringKey, RefContentLimitKey,
|
|
|
|
SenderKey, UserIdKey,
|
2022-06-17 00:56:10 -05:00
|
|
|
};
|
2022-07-28 18:32:17 -05:00
|
|
|
use ellipse::Ellipse;
|
2022-06-17 00:56:10 -05:00
|
|
|
use fancy_regex::{Captures, Replacer};
|
|
|
|
use pulldown_cmark::Parser;
|
|
|
|
use serenity::{
|
|
|
|
async_trait,
|
|
|
|
client::Context,
|
|
|
|
http::CacheHttp,
|
|
|
|
model::{
|
|
|
|
channel::{Channel, Message, MessageReference, MessageType},
|
|
|
|
guild::Member,
|
2022-07-28 18:36:32 -05:00
|
|
|
id::GuildId,
|
2023-06-26 21:17:22 -05:00
|
|
|
prelude::{ChannelId, GuildMemberUpdateEvent, Ready, Role, RoleId},
|
2022-07-28 18:36:32 -05:00
|
|
|
user::User,
|
2022-06-17 00:56:10 -05:00
|
|
|
},
|
|
|
|
prelude::*,
|
|
|
|
};
|
|
|
|
use std::borrow::Cow;
|
|
|
|
use std::collections::HashMap;
|
2022-07-05 13:51:05 -05:00
|
|
|
use std::fmt::Write;
|
2022-06-17 00:56:10 -05:00
|
|
|
|
2022-06-17 01:18:20 -05:00
|
|
|
struct StrChunks<'a> {
|
|
|
|
v: &'a str,
|
|
|
|
size: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for StrChunks<'a> {
|
|
|
|
type Item = &'a str;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
if self.v.is_empty() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
if self.v.len() < self.size {
|
|
|
|
let res = self.v;
|
|
|
|
self.v = &self.v[self.v.len()..];
|
|
|
|
return Some(res);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut offset = self.size;
|
|
|
|
|
|
|
|
let res = loop {
|
|
|
|
match self.v.get(..offset) {
|
|
|
|
Some(v) => break v,
|
|
|
|
None => {
|
|
|
|
offset -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-06-17 13:36:42 -05:00
|
|
|
self.v = &self.v[offset..];
|
2022-06-17 01:18:20 -05:00
|
|
|
|
|
|
|
Some(res)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StrChunks<'a> {
|
|
|
|
fn new(v: &'a str, size: usize) -> Self {
|
|
|
|
Self { v, size }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-17 00:56:10 -05:00
|
|
|
async fn create_prefix(msg: &Message, is_reply: bool, http: impl CacheHttp) -> (String, usize) {
|
2023-06-26 21:17:22 -05:00
|
|
|
// it's okay to unwrap here since we know we're in a guild
|
|
|
|
let nick = msg.member(http).await.unwrap().display_name().to_owned();
|
2022-07-11 02:07:43 -05:00
|
|
|
|
2022-06-17 00:56:10 -05:00
|
|
|
let mut chars = nick.char_indices();
|
|
|
|
let first_char = chars.next().unwrap().1;
|
|
|
|
let second_char_offset = chars.next().unwrap().0;
|
|
|
|
|
|
|
|
let colour_index = (first_char as usize + nick.len()) % 12;
|
|
|
|
|
|
|
|
let prefix = format!(
|
|
|
|
"{}<\x03{:02}{}\u{200B}{}\x0F> ",
|
|
|
|
if is_reply { "(reply to) " } else { "" },
|
|
|
|
colour_index,
|
|
|
|
&nick[..second_char_offset],
|
|
|
|
&nick[second_char_offset..]
|
|
|
|
);
|
2022-06-17 13:36:42 -05:00
|
|
|
// this 400 is basically just a guess. we cant send exactly 512 byte messages, because
|
|
|
|
// if we do then the server cant send them back without going over the 512 limit itself.
|
|
|
|
let content_limit = 400 - prefix.len();
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
(prefix, content_limit)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Handler;
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl EventHandler for Handler {
|
|
|
|
async fn message(&self, ctx: Context, msg: Message) {
|
|
|
|
match msg.kind {
|
|
|
|
MessageType::Regular | MessageType::InlineReply => {}
|
|
|
|
_ => return,
|
|
|
|
}
|
|
|
|
|
|
|
|
let ctx_data = ctx.data.read().await;
|
|
|
|
|
|
|
|
let user_id = ctx_data.get::<UserIdKey>().copied().unwrap();
|
|
|
|
let sender = ctx_data.get::<SenderKey>().unwrap();
|
|
|
|
let members = ctx_data.get::<MembersKey>().unwrap();
|
|
|
|
let raw_prefix = ctx_data
|
|
|
|
.get::<OptionStringKey>()
|
|
|
|
.unwrap()
|
|
|
|
.as_deref()
|
|
|
|
.unwrap_or("++");
|
|
|
|
let mapping = ctx_data.get::<ChannelMappingKey>().unwrap().clone();
|
2022-08-02 18:57:43 -05:00
|
|
|
let ref_content_limit = ctx_data.get::<RefContentLimitKey>().unwrap();
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
if user_id == msg.author.id || msg.author.bot {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let (prefix, content_limit) = create_prefix(&msg, false, &ctx).await;
|
|
|
|
|
2023-06-26 21:17:22 -05:00
|
|
|
let (channel, channel_id) = match mapping.iter().find(|(_, &v)| v == msg.channel_id.0.get())
|
|
|
|
{
|
2022-06-17 00:56:10 -05:00
|
|
|
Some((k, v)) => (k.as_str(), ChannelId::from(*v)),
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
|
|
|
|
let attachments: Vec<&str> = msg.attachments.iter().map(|a| a.url.as_str()).collect();
|
|
|
|
|
|
|
|
let roles = channel_id
|
|
|
|
.to_channel(&ctx)
|
|
|
|
.await
|
|
|
|
.unwrap()
|
|
|
|
.guild()
|
|
|
|
.unwrap()
|
|
|
|
.guild_id
|
|
|
|
.roles(&ctx)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let members_lock = members.lock().await;
|
|
|
|
|
2023-03-04 21:11:27 -06:00
|
|
|
let computed = discord_to_irc_processing(&msg.content, &members_lock, &ctx, &roles).await;
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
if let Some(MessageReference {
|
|
|
|
guild_id,
|
|
|
|
channel_id,
|
|
|
|
message_id: Some(message_id),
|
|
|
|
..
|
|
|
|
}) = msg.message_reference
|
|
|
|
{
|
|
|
|
if let Ok(mut reply) = channel_id.message(&ctx, message_id).await {
|
|
|
|
reply.guild_id = guild_id; // lmao
|
2022-08-02 18:57:43 -05:00
|
|
|
let (reply_prefix, reply_content_limit) = create_prefix(&reply, true, &ctx).await;
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
let mut content = reply.content;
|
|
|
|
content = content.replace("\r\n", " "); // just in case
|
|
|
|
content = content.replace('\n', " ");
|
2022-07-16 06:45:18 -05:00
|
|
|
let atts: Vec<&str> = reply.attachments.iter().map(|a| &*a.url).collect();
|
|
|
|
content = format!("{} {}", content, atts.join(" "));
|
2022-06-17 00:56:10 -05:00
|
|
|
|
2023-03-04 21:11:27 -06:00
|
|
|
content = discord_to_irc_processing(&content, &members_lock, &ctx, &roles).await;
|
2022-07-05 13:21:02 -05:00
|
|
|
|
2022-08-02 18:57:43 -05:00
|
|
|
let to_send = (&*content).truncate_ellipse(
|
|
|
|
ref_content_limit
|
|
|
|
.map(|l| l as usize)
|
|
|
|
.unwrap_or(reply_content_limit),
|
|
|
|
);
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
sender
|
2023-04-22 19:12:46 -05:00
|
|
|
.send_privmsg(channel, format!("{reply_prefix}{to_send}"))
|
2022-06-17 00:56:10 -05:00
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some((stripped, false)) = computed
|
2023-03-04 21:11:27 -06:00
|
|
|
.strip_prefix(raw_prefix)
|
2022-06-17 00:56:10 -05:00
|
|
|
.map(str::trim)
|
|
|
|
.map(|v| (v, v.is_empty()))
|
|
|
|
{
|
2022-07-29 18:45:36 -05:00
|
|
|
let to_send = stripped.trim_matches('\u{f}');
|
2022-06-17 00:56:10 -05:00
|
|
|
sender.send_privmsg(channel, &prefix).unwrap();
|
2022-07-29 18:45:36 -05:00
|
|
|
sender.send_privmsg(channel, to_send).unwrap();
|
2022-06-17 00:56:10 -05:00
|
|
|
} else {
|
|
|
|
for line in computed.lines() {
|
|
|
|
for chunk in StrChunks::new(line, content_limit) {
|
2022-07-29 18:45:36 -05:00
|
|
|
let to_send = chunk.trim_matches('\u{f}');
|
2022-06-17 00:56:10 -05:00
|
|
|
sender
|
2023-04-22 19:12:46 -05:00
|
|
|
.send_privmsg(channel, &format!("{prefix}{to_send}"))
|
2022-06-17 00:56:10 -05:00
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for attachment in attachments {
|
|
|
|
sender
|
2023-04-22 19:12:46 -05:00
|
|
|
.send_privmsg(channel, &format!("{prefix}{attachment}"))
|
2022-06-17 00:56:10 -05:00
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn ready(&self, ctx: Context, info: Ready) {
|
|
|
|
let id = info.user.id;
|
|
|
|
|
|
|
|
let mut data = ctx.data.write().await;
|
|
|
|
data.insert::<UserIdKey>(id);
|
|
|
|
}
|
|
|
|
|
2022-07-24 11:38:19 -05:00
|
|
|
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
|
2022-06-17 00:56:10 -05:00
|
|
|
let ctx_data = ctx.data.read().await;
|
|
|
|
let mut members = ctx_data.get::<MembersKey>().unwrap().lock().await;
|
|
|
|
members.push(new_member);
|
|
|
|
}
|
|
|
|
|
2023-06-26 21:17:22 -05:00
|
|
|
async fn guild_member_update(
|
|
|
|
&self,
|
|
|
|
ctx: Context,
|
|
|
|
_: Option<Member>,
|
|
|
|
new: Option<Member>,
|
|
|
|
_: GuildMemberUpdateEvent,
|
|
|
|
) {
|
2022-06-17 00:56:10 -05:00
|
|
|
let ctx_data = ctx.data.read().await;
|
|
|
|
let mut members = ctx_data.get::<MembersKey>().unwrap().lock().await;
|
|
|
|
|
2023-06-26 21:17:22 -05:00
|
|
|
if let Some(new) = new {
|
|
|
|
let x = members
|
|
|
|
.iter()
|
|
|
|
.position(|m| m.user.id == new.user.id)
|
|
|
|
.unwrap();
|
|
|
|
members.remove(x);
|
|
|
|
members.push(new);
|
|
|
|
}
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
2022-07-28 18:36:32 -05:00
|
|
|
|
|
|
|
async fn guild_member_removal(
|
|
|
|
&self,
|
|
|
|
ctx: Context,
|
|
|
|
_guild_id: GuildId,
|
|
|
|
user: User,
|
|
|
|
_member: Option<Member>,
|
|
|
|
) {
|
|
|
|
let ctx_data = ctx.data.read().await;
|
|
|
|
let mut members = ctx_data.get::<MembersKey>().unwrap().lock().await;
|
|
|
|
|
|
|
|
let pos = members.iter().position(|m| m.user.id == user.id).unwrap();
|
|
|
|
members.remove(pos);
|
|
|
|
}
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn discord_to_irc_processing(
|
|
|
|
message: &str,
|
|
|
|
members: &[Member],
|
|
|
|
ctx: &Context,
|
|
|
|
roles: &HashMap<RoleId, Role>,
|
|
|
|
) -> String {
|
|
|
|
struct MemberReplacer<'a> {
|
|
|
|
members: &'a [Member],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Replacer for MemberReplacer<'a> {
|
|
|
|
fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
|
|
|
|
let id = caps[1].parse::<u64>().unwrap();
|
|
|
|
|
|
|
|
let display_name = self.members.iter().find_map(|member| {
|
2023-06-26 21:17:22 -05:00
|
|
|
(id == member.user.id.0.get()).then(|| member.display_name().to_owned())
|
2022-06-17 00:56:10 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
if let Some(display_name) = display_name {
|
2023-04-22 19:12:46 -05:00
|
|
|
write!(dst, "@{display_name}").unwrap();
|
2022-06-17 00:56:10 -05:00
|
|
|
} else {
|
|
|
|
dst.push_str(caps.get(0).unwrap().as_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
regex! {
|
|
|
|
static PING_RE_1 = r"<@([0-9]+)>";
|
|
|
|
static PING_RE_2 = r"<@!([0-9]+)>";
|
2023-04-25 14:12:37 -05:00
|
|
|
static PING_RE_3 = r"\{@([0-9]+)\}";
|
2022-06-17 00:56:10 -05:00
|
|
|
static EMOJI_RE = r"<:(\w+):[0-9]+>";
|
|
|
|
static CHANNEL_RE = r"<#([0-9]+)>";
|
|
|
|
static ROLE_RE = r"<@&([0-9]+)>";
|
2022-07-25 19:05:03 -05:00
|
|
|
static URL_ESCAPE_RE = r"<(https?://[^\s/$.?#].\S*)>";
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
let mut computed = message.to_owned();
|
|
|
|
|
2022-07-25 19:05:03 -05:00
|
|
|
computed = URL_ESCAPE_RE.replace_all(&computed, "$1").into_owned();
|
|
|
|
|
2022-06-17 00:56:10 -05:00
|
|
|
computed = PING_RE_1
|
|
|
|
.replace_all(&computed, MemberReplacer { members })
|
|
|
|
.into_owned();
|
|
|
|
|
|
|
|
computed = PING_RE_2
|
|
|
|
.replace_all(&computed, MemberReplacer { members })
|
|
|
|
.into_owned();
|
|
|
|
|
2022-08-04 23:53:29 -05:00
|
|
|
computed = EMOJI_RE.replace_all(&computed, ":$1:").into_owned();
|
2022-06-17 00:56:10 -05:00
|
|
|
|
|
|
|
// FIXME: the await makes it impossible to use `replace_all`, idk how to fix this
|
|
|
|
for caps in CHANNEL_RE.captures_iter(&computed.clone()) {
|
|
|
|
let replacement = match ChannelId(caps.unwrap()[1].parse().unwrap())
|
|
|
|
.to_channel(&ctx)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(Channel::Guild(gc)) => Cow::Owned(format!("#{}", gc.name)),
|
|
|
|
_ => Cow::Borrowed("#deleted-channel"),
|
|
|
|
};
|
|
|
|
|
|
|
|
computed = CHANNEL_RE.replace(&computed, replacement).to_string();
|
|
|
|
}
|
|
|
|
|
|
|
|
computed = ROLE_RE
|
|
|
|
.replace_all(
|
|
|
|
&computed,
|
|
|
|
OptionReplacer(|caps: &Captures| {
|
|
|
|
roles
|
|
|
|
.get(&RoleId(caps[1].parse().unwrap()))
|
|
|
|
.map(|role| format!("@{}", role.name))
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
.into_owned();
|
|
|
|
|
2023-04-25 14:12:37 -05:00
|
|
|
// switch brackets of unknown pings
|
|
|
|
computed = PING_RE_1.replace_all(&computed, "{@$1}").into_owned();
|
|
|
|
|
2022-06-17 00:56:10 -05:00
|
|
|
computed = {
|
|
|
|
#[allow(clippy::enum_glob_use)]
|
|
|
|
use pulldown_cmark::{Event::*, Tag::*};
|
|
|
|
|
|
|
|
let mut new = String::with_capacity(computed.len());
|
|
|
|
|
2023-04-17 21:40:02 -05:00
|
|
|
let parser = Parser::new(&computed);
|
|
|
|
|
|
|
|
let mut list_level = 0;
|
|
|
|
let mut numbered = false;
|
|
|
|
let mut next_num = 0;
|
|
|
|
|
|
|
|
for event in parser {
|
|
|
|
match event {
|
|
|
|
Text(t) | Html(t) => new.push_str(&t),
|
2023-04-22 19:12:46 -05:00
|
|
|
Code(t) => write!(new, "`{t}`").unwrap(),
|
2023-04-17 21:40:02 -05:00
|
|
|
Start(Emphasis) => new.push('\x1D'),
|
|
|
|
Start(Strong) => new.push('\x02'),
|
|
|
|
Start(Link(_, _, _)) => {
|
|
|
|
new.push('[');
|
|
|
|
}
|
|
|
|
End(Link(_, url, title)) => {
|
2023-04-22 19:12:46 -05:00
|
|
|
write!(new, "]: {url}").unwrap();
|
2023-04-17 21:40:02 -05:00
|
|
|
if !title.is_empty() {
|
2023-04-22 19:12:46 -05:00
|
|
|
write!(new, " ({title})").unwrap();
|
2023-04-16 19:54:35 -05:00
|
|
|
}
|
2023-04-17 21:40:02 -05:00
|
|
|
}
|
|
|
|
Start(List(num)) => {
|
|
|
|
list_level += 1;
|
|
|
|
if let Some(num) = num {
|
|
|
|
numbered = true;
|
|
|
|
next_num = num;
|
|
|
|
} else {
|
|
|
|
numbered = false;
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
2023-04-17 21:40:02 -05:00
|
|
|
}
|
|
|
|
End(List(_)) => list_level -= 1,
|
|
|
|
Start(Item) => {
|
|
|
|
let prefix = if numbered {
|
2023-04-22 19:12:46 -05:00
|
|
|
format!("{next_num}.")
|
2023-04-17 21:40:02 -05:00
|
|
|
} else {
|
|
|
|
if list_level > 1 { '◦' } else { '•' }.into()
|
|
|
|
};
|
|
|
|
write!(new, "\n{}{} ", " ".repeat(list_level - 1), prefix).unwrap();
|
|
|
|
}
|
|
|
|
End(Item) => {
|
|
|
|
if numbered {
|
2023-04-22 19:12:46 -05:00
|
|
|
next_num += 1;
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
|
|
|
}
|
2023-04-17 21:40:02 -05:00
|
|
|
Start(BlockQuote) => new.push_str("> "),
|
|
|
|
Start(Heading(ty, _, _)) => {
|
|
|
|
write!(new, "{} \x02", "#".repeat(ty as usize)).unwrap();
|
|
|
|
}
|
|
|
|
SoftBreak | HardBreak | End(Paragraph) => new.push('\n'),
|
|
|
|
End(_) => new.push('\x0F'),
|
|
|
|
_ => {}
|
2022-06-17 00:56:10 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
new
|
|
|
|
};
|
|
|
|
|
2023-04-25 14:12:37 -05:00
|
|
|
// switch them back
|
|
|
|
computed = PING_RE_3.replace_all(&computed, "<@$1>").into_owned();
|
|
|
|
|
2022-06-17 00:56:10 -05:00
|
|
|
computed
|
|
|
|
}
|