use crate::{ regex, ChannelMappingKey, MembersKey, OptionReplacer, OptionStringKey, RefContentLimitKey, SenderKey, UserIdKey, }; use ellipse::Ellipse; 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, id::GuildId, prelude::{ChannelId, Ready, Role, RoleId}, user::User, }, prelude::*, }; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Write; struct StrChunks<'a> { v: &'a str, size: usize, } impl<'a> Iterator for StrChunks<'a> { type Item = &'a str; fn next(&mut self) -> Option { 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; } } }; self.v = &self.v[offset..]; Some(res) } } impl<'a> StrChunks<'a> { fn new(v: &'a str, size: usize) -> Self { Self { v, size } } } async fn create_prefix(msg: &Message, is_reply: bool, http: impl CacheHttp) -> (String, usize) { let mut nick = match msg.member(http).await { Ok(Member { nick: Some(nick), .. }) => Cow::Owned(nick), _ => Cow::Borrowed(&msg.author.name), }; if option_env!("DIRCORD_POLARIAN_MODE").is_some() { nick = Cow::Owned("polarbear".to_string()); } 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..] ); // 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(); (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::().copied().unwrap(); let sender = ctx_data.get::().unwrap(); let members = ctx_data.get::().unwrap(); let raw_prefix = ctx_data .get::() .unwrap() .as_deref() .unwrap_or("++"); let mapping = ctx_data.get::().unwrap().clone(); let ref_content_limit = ctx_data.get::().unwrap(); if user_id == msg.author.id || msg.author.bot { return; } let (prefix, content_limit) = create_prefix(&msg, false, &ctx).await; let (channel, channel_id) = match mapping.iter().find(|(_, &v)| v == msg.channel_id.0) { 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; let computed = discord_to_irc_processing(&msg.content, &**members_lock, &ctx, &roles).await; 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 let (reply_prefix, reply_content_limit) = create_prefix(&reply, true, &ctx).await; let mut content = reply.content; content = content.replace("\r\n", " "); // just in case content = content.replace('\n', " "); let atts: Vec<&str> = reply.attachments.iter().map(|a| &*a.url).collect(); content = format!("{} {}", content, atts.join(" ")); content = discord_to_irc_processing(&content, &**members_lock, &ctx, &roles).await; let to_send = (&*content).truncate_ellipse( ref_content_limit .map(|l| l as usize) .unwrap_or(reply_content_limit), ); sender .send_privmsg(channel, format!("{}{}", reply_prefix, to_send)) .unwrap(); } } if let Some((stripped, false)) = computed .strip_prefix(&raw_prefix) .map(str::trim) .and_then(|v| v.strip_suffix('\x0F')) .map(|v| (v, v.is_empty())) { let to_send = stripped.trim_matches('\u{f}'); sender.send_privmsg(channel, &prefix).unwrap(); sender.send_privmsg(channel, to_send).unwrap(); } else { for line in computed.lines() { for chunk in StrChunks::new(line, content_limit) { let to_send = chunk.trim_matches('\u{f}'); sender .send_privmsg(channel, &format!("{}{}", prefix, to_send)) .unwrap(); } } } for attachment in attachments { sender .send_privmsg(channel, &format!("{}{}", prefix, attachment)) .unwrap(); } } async fn ready(&self, ctx: Context, info: Ready) { let id = info.user.id; let mut data = ctx.data.write().await; data.insert::(id); } async fn guild_member_addition(&self, ctx: Context, new_member: Member) { let ctx_data = ctx.data.read().await; let mut members = ctx_data.get::().unwrap().lock().await; members.push(new_member); } async fn guild_member_update(&self, ctx: Context, _: Option, new: Member) { let ctx_data = ctx.data.read().await; let mut members = ctx_data.get::().unwrap().lock().await; let x = members .iter() .position(|m| m.user.id == new.user.id) .unwrap(); members.remove(x); members.push(new); } async fn guild_member_removal( &self, ctx: Context, _guild_id: GuildId, user: User, _member: Option, ) { let ctx_data = ctx.data.read().await; let mut members = ctx_data.get::().unwrap().lock().await; let pos = members.iter().position(|m| m.user.id == user.id).unwrap(); members.remove(pos); } } async fn discord_to_irc_processing( message: &str, members: &[Member], ctx: &Context, roles: &HashMap, ) -> 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::().unwrap(); let display_name = self.members.iter().find_map(|member| { (id == member.user.id.0).then(|| member.display_name().into_owned()) }); if let Some(display_name) = display_name { write!(dst, "@{}", display_name).unwrap(); } 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]+)>"; static EMOJI_RE = r"<:(\w+):[0-9]+>"; static CHANNEL_RE = r"<#([0-9]+)>"; static ROLE_RE = r"<@&([0-9]+)>"; static URL_ESCAPE_RE = r"<(https?://[^\s/$.?#].\S*)>"; } let mut computed = message.to_owned(); computed = URL_ESCAPE_RE.replace_all(&computed, "$1").into_owned(); computed = PING_RE_1 .replace_all(&computed, MemberReplacer { members }) .into_owned(); computed = PING_RE_2 .replace_all(&computed, MemberReplacer { members }) .into_owned(); computed = EMOJI_RE.replace_all(&computed, ":$1:").into_owned(); // 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)), Ok(Channel::Category(cat)) => Cow::Owned(format!("#{}", cat.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(); computed = { #[allow(clippy::enum_glob_use)] use pulldown_cmark::{Event::*, Tag::*}; let mut new = String::with_capacity(computed.len()); for line in computed.lines() { let parser = Parser::new(line); let mut computed_line = String::with_capacity(line.len()); for event in parser { match event { Text(t) | Html(t) => computed_line.push_str(&t), Code(t) => write!(computed_line, "`{}`", t).unwrap(), End(_) => computed_line.push('\x0F'), Start(Emphasis) => computed_line.push('\x1D'), Start(Strong) => computed_line.push('\x02'), Start(Link(_, dest, _)) => { computed_line.push_str(&dest); continue; } Start(List(num)) => { if let Some(num) = num { write!(computed_line, "{}. ", num).unwrap(); } else { computed_line.push_str("- "); } } Start(BlockQuote) => computed_line.push_str("> "), _ => {} } } computed_line.push('\n'); new.push_str(&computed_line); } new }; computed }