From d6a17fdd92b690e2584d743c153b45ba4d684d30 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Fri, 14 May 2021 00:50:37 -0300 Subject: [Project] CW2 Plugin for Hexchat A nice Content Warning plugin for Hexchat! --- src/hexchat_plugin_ext.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 146 ++++++++++++++++++++++++++++++++++++ src/parsing.rs | 174 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 src/hexchat_plugin_ext.rs create mode 100644 src/lib.rs create mode 100644 src/parsing.rs (limited to 'src') diff --git a/src/hexchat_plugin_ext.rs b/src/hexchat_plugin_ext.rs new file mode 100644 index 0000000..3c1d3ae --- /dev/null +++ b/src/hexchat_plugin_ext.rs @@ -0,0 +1,187 @@ +// This file is part of CW2 +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#![allow(dead_code)] + +use std::cell::Cell; +use std::panic::RefUnwindSafe; + +use hexchat_plugin::CommandHookHandle; +use hexchat_plugin::Eat; +use hexchat_plugin::EventAttrs; +use hexchat_plugin::PluginHandle as Ph; +use hexchat_plugin::PrintHookHandle; +use hexchat_plugin::ServerHookHandle; +use hexchat_plugin::WordEol as Eol; +use hexchat_plugin::Word; + +/// Useful extensions to hexchat_plugin::PluginHandle +pub trait PhExt { + /// Builds a command hook. + fn make_command<'a, F>(&'a mut self, cmd: &'a str, cb: F) + -> CommandBuilder<'a, F> + where F: Fn(&mut Ph, Word, Eol) -> Eat + 'static + RefUnwindSafe; + + /// Builds a server hook with attributes. + fn make_server_attrs<'a, F>(&'a mut self, cmd: &'a str, cb: F) + -> ServerAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, Eol, EventAttrs) -> Eat + 'static + RefUnwindSafe; + + /// Builds a print hook with attributes. + fn make_print_attrs<'a, F>(&'a mut self, msg: &'a str, cb: F) + -> PrintAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe; +} + +/// Helper for building command hooks. Created with `PhExt::make_command`. +pub struct CommandBuilder<'a, F> + where F: Fn(&mut Ph, Word, Eol) -> Eat + 'static + RefUnwindSafe +{ + ph: &'a mut Ph, + cmd: &'a str, + cb: F, + pri: i32, + help: Option<&'a str>, +} + +/// Helper for building server hooks. Created with `PhExt::make_server_attrs`. +pub struct ServerAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, Eol, EventAttrs) -> Eat + 'static + RefUnwindSafe +{ + ph: &'a mut Ph, + cmd: &'a str, + cb: F, + pri: i32, +} + +/// Helper for building print hooks. Created with `PhExt::make_print_attrs`. +pub struct PrintAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe +{ + ph: &'a mut Ph, + msg: &'a str, + cb: F, + pri: i32, +} + +impl PhExt for Ph { + fn make_command<'a, F>(&'a mut self, cmd: &'a str, cb: F) + -> CommandBuilder<'a, F> + where F: Fn(&mut Ph, Word, Eol) -> Eat + 'static + RefUnwindSafe + { + CommandBuilder { + ph: self, + cmd, + cb, + pri: 0, + help: None, + } + } + + fn make_server_attrs<'a, F>(&'a mut self, cmd: &'a str, cb: F) + -> ServerAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, Eol, EventAttrs) -> Eat + 'static + RefUnwindSafe + { + ServerAttrsBuilder { + ph: self, + cmd, + cb, + pri: 0, + } + } + + fn make_print_attrs<'a, F>(&'a mut self, msg: &'a str, cb: F) + -> PrintAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe + { + PrintAttrsBuilder { + ph: self, + msg, + cb, + pri: 0, + } + } +} + +impl<'a, F> CommandBuilder<'a, F> + where F: Fn(&mut Ph, Word, Eol) -> Eat + 'static + RefUnwindSafe +{ + /// Sets the priority of this hook. The default is `PRI_NORM` or `0`. + pub fn set_priority(self, priority: i32) -> Self { + Self { pri: priority, ..self } + } + + /// Sets the help message of this hook. The default is `None`. + pub fn set_help>>(self, help: S) -> Self { + Self { help: help.into(), ..self } + } + + /// Registers the hook and stores the handle at the given `Cell`. + pub fn build_into(self, handle: &Cell>) { + handle.set(Some(self.build())); + } + + /// Registers the hook and returns the handle for it. + pub fn build(self) -> CommandHookHandle { + self.ph.hook_command(self.cmd, self.cb, self.pri, self.help) + } +} + +impl<'a, F> ServerAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, Eol, EventAttrs) -> Eat + 'static + RefUnwindSafe +{ + /// Sets the priority of this hook. The default is `PRI_NORM` or `0`. + pub fn set_priority(self, priority: i32) -> Self { + Self { pri: priority, ..self } + } + + /// Registers the hook and stores the handle at the given `Cell`. + pub fn build_into(self, handle: &Cell>) { + handle.set(Some(self.build())); + } + + /// Registers the hook and returns the handle for it. + pub fn build(self) -> ServerHookHandle { + self.ph.hook_server_attrs(self.cmd, self.cb, self.pri) + } +} + +impl<'a, F> PrintAttrsBuilder<'a, F> + where + F: Fn(&mut Ph, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe +{ + /// Sets the priority of this hook. The default is `PRI_NORM` or `0`. + pub fn set_priority(self, priority: i32) -> Self { + Self { pri: priority, ..self } + } + + /// Registers the hook and stores the handle at the given `Cell`. + pub fn build_into(self, handle: &Cell>) { + handle.set(Some(self.build())); + } + + /// Registers the hook and returns the handle for it. + pub fn build(self) -> PrintHookHandle { + self.ph.hook_print_attrs(self.msg, self.cb, self.pri) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fd67b88 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,146 @@ +// CW2 - Hexchat Plugin for Content Warnings +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#[macro_use] +extern crate hexchat_plugin; + +mod parsing; + +mod hexchat_plugin_ext; + +use std::cell::Cell; + +use hexchat_plugin::Plugin as HexchatPlugin; +use hexchat_plugin::PluginHandle as Ph; +use hexchat_plugin::CommandHookHandle; +use hexchat_plugin::PrintHookHandle; + +use hexchat_plugin_ext::PhExt; + +#[derive(Default)] +struct Cw2Plugin { + cmd_cw: Cell>, + cmd_cwmsg: Cell>, + print_hooks: Cell>, +} + +const PLUG_NAME: &'static str = "CW2"; +const PLUG_VER: &'static str = "1.0.0"; +const PLUG_DESC: &'static str = "Adds support for content warnings."; + +const CMD_CW: &'static str = "CW"; +const CMD_CW_HELP: &'static str = "Sends a content warned msg. \ + Example usage: /cw [thing] message"; +const CMD_CW_EPARSE: &'static str = "Error parsing CW. \ + Example usage: /cw [thing] message"; +const CMD_CW_ENOARG: &'static str = "This command requires more arguments. \ + Example usage: /cw [thing] message"; + +const CMD_CWMSG: &'static str = "CWMSG"; +const CMD_CWMSG_HELP: &'static str = "Sends a content warned msg to an user. \ + Example usage: /cwmsg user [thing] message"; +const CMD_CWMSG_EPARSE: &'static str = "Error parsing CW. \ + Example usage: /cwmsg user [thing] message"; +const CMD_CWMSG_ENOARG: &'static str = "This command requires more arguments. \ + Example usage: /cwmsg user [thing] message"; + +impl HexchatPlugin for Cw2Plugin { + fn init(&self, ph: &mut Ph, _: Option<&str>) -> bool { + ph.register(PLUG_NAME, PLUG_VER, PLUG_DESC); + + ph.make_command(CMD_CW, |ph, arg, arg_eol| { + if arg.len() < 2 { + ph.print(CMD_CW_ENOARG); + return hexchat_plugin::EAT_ALL + } + if let Some(message) = parsing::try_to_cw2(arg_eol[1]) { + ph.ensure_valid_context(|ph| { + ph.command(&format!("say {}", message)); + }); + } else { + ph.print(CMD_CW_EPARSE); + } + hexchat_plugin::EAT_ALL + }).set_help(CMD_CW_HELP).build_into(&self.cmd_cw); + + ph.make_command(CMD_CWMSG, |ph, arg, arg_eol| { + if arg.len() < 3 { + ph.print(CMD_CWMSG_ENOARG); + return hexchat_plugin::EAT_ALL + } + let user = arg_eol[1]; + if let Some(message) = parsing::try_to_cw2(arg_eol[2]) { + ph.ensure_valid_context(|ph| { + ph.command(&format!("msg {} {}", user, message)); + }); + } else { + ph.print(CMD_CWMSG_EPARSE); + } + hexchat_plugin::EAT_ALL + }).set_help(CMD_CWMSG_HELP).build_into(&self.cmd_cwmsg); + + let mut hooks = Vec::new(); + let to_hook = [ + ("Channel Message", 2,), + ("Channel Msg Hilight", 2,), + ("Channel Notice", 3,), + ("Private Message", 2,), + ("Private Message to Dialog", 2,), + ("Notice", 2,), + ("Your Message", 2,), + ("Notice Send", 2,), + ("Message Send", 2,), + ]; + for &(msg, idx) in &to_hook { + let h = ph.make_print_attrs(msg, move |ph, arg, attrs| { + if arg.len() < idx { + return hexchat_plugin::EAT_NONE + } + // hexchat uses 1-indexed arg + // but hexchat_plugin uses 0-indexed + // (ah well) + let x = arg[idx-1]; + if let Some((reason, content)) = parsing::try_parse_cw2(x) { + let mut newargs = Vec::new(); + newargs.extend(arg.iter().map(|x| String::from(*x))); + // NOTE: must not start with "[CW ", as that'd cause an + // infinite loop! + newargs[idx-1] = format!("[Content Hidden: {}]", reason); + ph.print(&format!("\x0326*\t[Content Hidden: {}]\x03\x08{}\x08 \x0324(Copy and paste this line to expand)\x03", reason, content)); + ph.ensure_valid_context(|ph| { + let iter = newargs.iter().map(|x| &**x); + // TODO ideally we'd avoid recursion but eh it's fine + // .-. + // (we guess as long as the above isn't "[CW {}]", + // this is probably fine.) + ph.emit_print_attrs(attrs, msg, iter); + }); + hexchat_plugin::EAT_ALL + } else { + hexchat_plugin::EAT_NONE + } + }).set_priority(i32::MAX).build(); + hooks.push(h); + } + self.print_hooks.set(hooks); + + ph.print("CW2 plugin loaded!"); + + true + } +} + +hexchat_plugin!(Cw2Plugin); diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..bf0f839 --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,174 @@ +// This file is part of CW2 +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::convert::TryFrom; + +/// Converts the given reason and content to a CW2. +/// +/// A CW2 is a string of `[CW (reason)](content)`. +pub fn to_cw2(reason: &str, content: &str) -> String { + let expected_size = reason.len() + content.len() + "[CW ]".len(); + let mut sb = String::with_capacity(expected_size); + let mut count: isize = 0; + let iter = reason.matches(|_| true); + iter.for_each(|c| { + match c { + "\x10" => { + sb += "\x10\x10"; + }, + "[" => { + count += 1; + sb += c; + }, + "]" => { + count -= 1; + if count < 0 { + sb += "\x10"; + count = 0; + } + sb += c; + }, + c => { + sb += c; + }, + } + }); + if count > 0 { + let size = sb.len() + usize::try_from(count).unwrap(); + let mut sb2 = String::with_capacity(size); + for c in sb.matches(|_| true).rev() { + match c { + "[" if count > 0 => { + count -= 1; + sb2 += c; + sb2 += "\x10"; + }, + c => { + sb2 += c; + }, + } + } + sb = String::with_capacity(sb2.len()); + sb.extend(sb2.matches(|_| true).rev()); + } + format!("[CW {}]{}", sb, content) +} + +/// Attempts to convert the given message to a CW2. +/// +/// The message is expected to be in the format `[(reason)](content)`. Returns +/// `None` if it isn't, otherwise returns `[CW (reason)](content)`. +pub fn try_to_cw2(message: &str) -> Option { + parse_cw_helper(message, false).map(|(r, c)| to_cw2(&r, c)) +} + +/// Parses an incoming message for CW. +/// +/// Splits `[CW (reason)](content)` into `(reason)` and `(content)`, or +/// returns `None`. +pub fn try_parse_cw2(message: &str) -> Option<(String, &str)> { + parse_cw_helper(message, true) +} + +/// Parses a message for CW, with a flag to check for `CW` in the reason. +fn parse_cw_helper(message: &str, check_cw: bool) -> Option<(String, &str)> { + let mut count = 0; + let mut mquote = false; + let mut last_size = 0; + let mut skipped = if check_cw { + message.starts_with("[CW ").then(|| 4)? + } else { + 1 + }; + let cw_start = skipped; + // figure out the last pos we need + let pos = message.match_indices(|_| true).take_while(|&(_, c)| { + match c { + "[" if !mquote => count += 1, + "]" if !mquote => count -= 1, + "\x10" if !mquote => mquote = true, + _ if mquote => { + mquote = false; + skipped += 1; + }, + _ => {}, + } + last_size = c.len(); + count != 0 + }).last()?.0; + (count == 0).then(|| { + // at this point we have `[CW foo]bar` as `[CW foo` and `bar`, but + // `pos` is only the start of the last `o` in `foo`. + let start_of_contents = pos + last_size + 1; + // build the string + let mut sb = String::with_capacity(pos - skipped); + let mut mquote = false; + let iter = message[cw_start..(start_of_contents-1)].matches(|_| true); + iter.for_each(|c| { + match c { + "\x10" if !mquote => mquote = true, + c => { + mquote = false; + sb += c; + }, + } + }); + let reason = sb; + let contents = &message[start_of_contents..]; + (reason, contents) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_correct_cw_parsing() { + let tests = [ + ("[CW FOO] BAR", true, Some(("FOO".into(), " BAR"))), + ("[CW FOO\x10]] BAR", true, Some(("FOO]".into(), " BAR"))), + ("[CW FOO\x10[] BAR", true, Some(("FOO[".into(), " BAR"))), + ("[CW FOO\x10] BAR", true, None), + ("message", true, None), + + ("[CW FOO] BAR", false, Some(("CW FOO".into(), " BAR"))), + ("[CW FOO\x10]] BAR", false, Some(("CW FOO]".into(), " BAR"))), + ("[CW FOO\x10[] BAR", false, Some(("CW FOO[".into(), " BAR"))), + ("[CW FOO\x10] BAR", false, None), + ("message", false, None), + ]; + for test in &tests { + dbg!(test); + assert_eq!(parse_cw_helper(test.0, test.1), test.2); + } + } + + #[test] + fn test_correct_cw_building() { + let tests = [ + ("", "", String::from("[CW ]")), + ("[", "", "[CW \x10[]".into()), + ("]", "", "[CW \x10]]".into()), + ("[[]", "", "[CW [\x10[]]".into()), + ("[]]", "", "[CW []\x10]]".into()), + ]; + for test in &tests { + dbg!(test); + assert_eq!(to_cw2(test.0, test.1), test.2); + } + } +} -- cgit 1.4.1