commit 7c6cb754966df06dfcb66a3d50fb80e829b4017f Author: SoniEx2 Date: Sat Jan 19 20:30:25 2019 -0200 Version 3.0.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0489043 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +Hexchat CW1 plugin +================== + +Implements the CW1 CTCP. + +CW1 CTCP +======== + +Syntax: + + CW1 + +The `CW1` CTCP is the Type-1 CW. On clients that don't support `CW1`, it falls back to displaying rot13 text. + +Because rot13 only affects the English alphabet, it is recommended to use CW1 only for text using the Latin alphabet - everything else gets displayed as-is on clients that don't support `CW1`. + +`CW1` may appear multiple times in a message. + +Examples: + + hello\x01CW1 pehry\x01world\x01CW1 !\x01 + +On a client that supports CW1: + + hello [hidden text 1 (click to expand)] world [hidden text 2 (click to expand)] + +expands to: + + hello cruel world ! + +On a client that doesn't support CW1: + + hello CW1 pehry world CW1 ! + +(Some clients may display delimiter boxes around `CW1 pehry` and `CW1 !`, making it easier to distinguish where the hidden text ends from where non-hidden text starts) + +Rot13 is already used in many mental health channels to "hide" triggering content pending further action from the user, which makes it a reasonable choice for a content warning CTCP. diff --git a/hexchat-cw1.lua b/hexchat-cw1.lua new file mode 100644 index 0000000..abf5128 --- /dev/null +++ b/hexchat-cw1.lua @@ -0,0 +1,211 @@ +local hexchat = hexchat +hexchat.register("rot13", "3.0.1", "rot13") + +local rot13 = {} + +for i = string.byte('a'), string.byte('m') do + local a, b = string.char(i), string.char(i+13) + local A, B = a:upper(), b:upper() + rot13[a] = b + rot13[b] = a + rot13[A] = B + rot13[B] = A +end + +local function do_rot13(arg, arg_eol) + if not arg[3] then + hexchat.print("Usage: /" .. arg[1] .. " ") + return hexchat.EAT_ALL + end + hexchat.command("say " .. arg[2] .. "\x01CW1 " .. arg_eol[3]:gsub("[A-Za-z]", rot13) .. "\x01") + return hexchat.EAT_ALL +end + +hexchat.hook_command("cw-rot13", do_rot13) + +local function startswith(str1, str2) + return str1:sub(1, #str2) == str2 +end + +local function parsecw(msg) + local hidden = {} + msg = msg:gsub("()(\1([^\1]*)\1)()", function(start, ctcp, contents, finish) + if startswith(contents, "CW1 ") then + local cw = contents:sub(5) + hidden[#hidden + 1] = cw:gsub("[A-Za-z]", rot13) + return " [Hidden Text " .. #hidden .. "] " + end + return ctcp + end) + return msg, hidden +end + +local invert_notice = false +local last_notice = false + +local tunpack = unpack or table.unpack + +local skip = false +local function mkparse(event, pos) + return function(word, attributes) + if skip then return end + if event:find("Notice$") then + if invert_notice then + word[pos] = '\1'.. word[pos] + if last_notice then + word[pos] = word[pos] .. '\1' + end + end + end + invert_notice = false + local old_msg = word[pos] + local hidden + word[pos], hidden = parsecw(word[pos]) + if word[pos] ~= old_msg then + skip = true + hexchat.emit_print_attrs(attributes, event, tunpack(word)) + for i, v in ipairs(hidden) do + hexchat.print("\00326*\tHidden Text " .. i .. "\3 > \8"..v.."\8 < (Copy and paste to expand)") + end + skip = false + return hexchat.EAT_ALL + end + end +end + +local function hookparse(event, pos) + return hexchat.hook_print_attrs(event, mkparse(event, pos)) +end + +do + (function(f,...)return f(f,...) end)(function(f, a, b, ...) + if a then + hookparse(a, b) + return f(f, ...) + end + end, + "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, + nil) +end + +-- from here on: 3.0.1 bugfix + +-- TODO maybe move these to a separate plugin. (or integrate them in hexchat) + +local function tooct(n) + return string.format("%o", n) +end + +local function on_privmsg(word, word_eol, attrs) + -- fix up some PRIVMSGs, pass them on to our handlers above + local i_at = 0 + for i,v in ipairs(word) do + if v == "PRIVMSG" then + i_at = i + 2 + break + end + end + if i_at == 0 then return end + if word_eol[i_at]:sub(1, 2):find(":[-+]") and tooct(hexchat.props["flags"]):find("[4-7].$") then -- bleh + word_eol[i_at] = ":" .. word_eol[i_at]:sub(3) + end + if word_eol[i_at]:sub(1, 6) == ":\1CW1 " then + if not word_eol[i_at]:find("\1", 7, true) == #word_eol[i_at] then + return hexchat.EAT_NONE -- let hexchat handle it normally + end + local target = word[i_at - 1] + local source + if word[1]:sub(1,1) ~= "@" then + source = word[1] + else + source = word[2] + end + source = source:match("^:([^!]+)!") -- this seems easy, right? + local context = hexchat.find_context(hexchat.get_info("server"), target) + if context then + local old_ctx = hexchat.get_context() + local serv_id = hexchat.props["id"] + -- sanity check + if hexchat.set_context(context) and hexchat.props["id"] == serv_id then + if hexchat.props["type"] == 2 then + local prefix = "" + for user in hexchat.iterate("users") do + if user.nick == source then + prefix = user.prefix + end + end + -- TODO fix highlights + hexchat.emit_print_attrs(attrs, "Channel Message", source, word_eol[i_at]:sub(2), prefix) + return hexchat.EAT_HEXCHAT + elseif hexchat.props["type"] == 3 then + hexchat.emit_print_attrs(attrs, "Private Message to Dialog", source, word_eol[i_at]:sub(2)) + return hexchat.EAT_HEXCHAT + end + end + -- assume it's a query, because usually you only get channel messages if you're in the channel + if not hexchat.set_context(old_ctx) then return end -- how did we get this far without segfaulting? + end + if hexchat.prefs["gui_autoopen_dialog"] then + hexchat.command("query -nofocus " .. target) + local context = hexchat.find_context(hexchat.get_info("server"), target) + if context then + local old_ctx = hexchat.get_context() + local serv_id = hexchat.props["id"] + -- sanity check + if hexchat.set_context(context) and hexchat.props["id"] == serv_id then + -- meh, go hardmode on this (this shouldn't happen unless find_context returns the server context for some reason) + if hexchat.props["type"] ~= 3 then + local found = false + for chan in hexchat.iterate("channels") do + if chan.id == serv_id and chan.type == 3 and chan.channel == source then + found = true + hexchat.set_context(chan.context) + end + end + -- still not found? ragequit! + if not found then return hexchat.EAT_HEXCHAT end + end + hexchat.emit_print_attrs(attrs, "Private Message to Dialog", source, word_eol[i_at]:sub(2)) + end + end + return hexchat.EAT_HEXCHAT + end + hexchat.emit_print_attrs(attrs, "Private Message", source, word_eol[i_at]:sub(2)) + return hexchat.EAT_HEXCHAT + end +end + +local function on_notice(word, word_eol, attrs) -- this one is much simpler + local i_at = 0 + for i,v in ipairs(word) do + if v == "NOTICE" then + i_at = i + 2 + break + end + end + if i_at == 0 then return end + if word_eol[i_at]:sub(1, 2):find(":[-+]") and tooct(hexchat.props["flags"]):find("[4-7].$") then -- bleh + word_eol[i_at] = ":" .. word_eol[i_at]:sub(3) + end + if word_eol[i_at]:sub(1, 6) == ":\1CW1 " then + -- workaround + invert_notice = true + last_notice = word_eol[i_at]:sub(-1,-1) == "\1" + -- then just let hexchat handle it + end +end + + +hexchat.hook_server_attrs("PRIVMSG", on_privmsg) + +hexchat.hook_server_attrs("NOTICE", on_notice) + +hexchat.print("rot13 loaded") -- this was in 3.0.0 too