Compare commits

..

No commits in common. "master" and "cw1-plugin" have entirely different histories.

2 changed files with 244 additions and 6 deletions

View File

@ -1,10 +1,37 @@
hexchat-plugins
===============
Hexchat CW1 plugin
==================
Assorted collection of hexchat plugins.
Implements the CW1 CTCP.
Plugins are their own branches.
CW1 CTCP
========
Current plugins:
Syntax:
- [CW1 plugin](https://cybre.tech/SoniEx2/hexchat-plugins/src/branch/cw1-plugin)
CW1 <rot13 text>
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.

211
hexchat-cw1.lua Normal file
View File

@ -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] .. " <reason> <content>")
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