Last active
October 19, 2015 12:26
-
-
Save sztanpet/fb5adc38f54863f3ea0f to your computer and use it in GitHub Desktop.
A hexchat lua (https://github.com/mniip/hexchat-lua) plugin to ignore highlights from users
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
This is free and unencumbered software released into the public domain. | |
Anyone is free to copy, modify, publish, use, compile, sell, or | |
distribute this software, either in source code form or as a compiled | |
binary, for any purpose, commercial or non-commercial, and by any | |
means. | |
In jurisdictions that recognize copyright laws, the author or authors | |
of this software dedicate any and all copyright interest in the | |
software to the public domain. We make this dedication for the benefit | |
of the public at large and to the detriment of our heirs and | |
successors. We intend this dedication to be an overt act of | |
relinquishment in perpetuity of all present and future rights to this | |
software under copyright law. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
OTHER DEALINGS IN THE SOFTWARE. | |
For more information, please refer to <http://unlicense.org/> | |
]] | |
hexchat.register("antihl", "1.0", "Prevent certain nicks from highlighting you") | |
-- whether we are in debug mode or not | |
local debugMode = false | |
local function p(...) | |
hexchat.print("AntiHL", ...) | |
end | |
local function handleDebug(source, ...) | |
if not debugMode then return end | |
if source == "INC" then | |
local w, we = ... | |
p("debug - highlight from " .. w[1]:sub(2)) | |
elseif source == "IGN" then | |
p("debug - highlight ignored") | |
elseif source == "GHL" then | |
local count = ... | |
p("debug - globalhighlights " .. count) | |
elseif source == "UHL" then | |
local from, count, globalCount = ... | |
p("debug - userhighlights from: " .. from .. " count: " .. count) | |
end | |
end | |
local duration = { | |
_units = { | |
s = 1, | |
sec = 1, | |
secs = 1, | |
second = 1, | |
seconds = 1, | |
m = 60, | |
min = 60, | |
mins = 60, | |
minute = 60, | |
minutes = 60, | |
h = 3600, | |
hr = 3600, | |
hrs = 3600, | |
hour = 3600, | |
hours = 3600, | |
d = 86400, | |
day = 86400, | |
days = 86400, | |
}, | |
parse = function(self, str) | |
local number, unit = str:match("^([%d%.]+) *(%a*)$") | |
if not number or not tonumber(number) then return 0, "invalid" end | |
if not unit or unit == "" then unit = "s" end | |
number = tonumber(number) * (self._units[unit:lower()] or self._units.s); | |
return number, nil | |
end, | |
humanize = function(self, seconds) | |
local str = "" | |
local hours = math.floor(seconds / 3600) | |
if hours > 0 then | |
str = str .. hours .. " hours " | |
seconds = seconds - (hours * 3600) | |
end | |
local minutes = math.floor(seconds / 60) | |
if minutes > 0 then | |
str = str .. minutes .. " minutes " | |
seconds = seconds - (minutes * 60) | |
end | |
if seconds > 0 or str == "" then | |
str = str .. seconds .. " seconds " | |
end | |
-- TODO trim? what for who cares | |
return str | |
end, | |
} | |
local ignores = { | |
-- table of ignored people | |
-- the key is a pattern to match the user with | |
-- the value is the time until the user is ignored in seconds | |
-- if the value is 0 the ignore is permanent | |
_state = {}, | |
init = function(self) | |
for k, v in pairs(hexchat.pluginprefs) do | |
if k:sub(1, 7) == "ignore-" then | |
local source = k:sub(8) | |
local endTime = tonumber(v) | |
self:add(source, endTime, true) | |
end | |
end | |
self:expire() | |
end, | |
add = function(self, source, endTime, skipPref) | |
skipPref = skipPref == nil or skipPref and true | |
self._state[source] = endTime | |
if skipPref ~= true then | |
hexchat.pluginprefs["ignore-" .. source] = endTime | |
end | |
end, | |
del = function(self, source) | |
if not self._state[source] then | |
return false | |
end | |
self:add(source, nil, false) | |
end, | |
expire = function(self) | |
local now = os.time() | |
for source, endTime in pairs(self._state) do | |
if endTime ~= 0 and endTime < now then | |
self._state[source] = nil | |
hexchat.pluginprefs["ignore-" .. source] = nil | |
end | |
end | |
return true -- must return true to reexecute via hexchat.timer | |
end, | |
each = function(self) | |
return pairs(self._state) | |
end, | |
match = function(self, source) | |
for p, _ in pairs(self._state) do | |
if source:match(p) then | |
return true | |
end | |
end | |
return false | |
end, | |
} | |
local commands = { | |
_invalidLength = function(self, str) | |
-- hardcoded argument length TODO | |
-- to check if the things we put into the plugin preferences are not excessive | |
-- cant honestly see hexchat changing, so hardcoded for now | |
if #str > 512 then | |
p("- arguments too long :(") | |
return true | |
end | |
return false | |
end, | |
_invalidNumArgs = function(self, args, minLen, maxLen) | |
if #args < minLen then | |
p("- too few arguments, see /help antihl") | |
return true | |
end | |
if #args > maxLen then | |
p("- too many arguments, arguments cannot contain whitespace, see /help antihl") | |
return true | |
end | |
return false | |
end, | |
_handle = function(self, w, we) | |
-- convenience method, accept commands both with and without a - infront | |
if not w[2] or (not self[ w[2] ] and not self[ w[2]:sub(2) ]) then | |
return p("- unknown command, see /help antihl") | |
end | |
local fn = self[ w[2] ] or self[ w[2]:sub(2) ] | |
fn(self, w, we) | |
return hexchat.EAT_ALL | |
end, | |
list = function(self, w, we) | |
if self:_invalidNumArgs(w, 2, 2) then return end | |
local len = 0 | |
for _, _ in ignores:each() do | |
len = len + 1 | |
end | |
p("- note: order of things printed is not defined, printing " .. len .. " ignored people:") | |
local now = os.time() | |
for source, endTime in ignores:each() do | |
local postfix = "" | |
if endTime == 0 then | |
postfix = "permanently" | |
else | |
local seconds = endTime - now | |
postfix = "for another " .. duration:humanize(seconds) | |
end | |
p("- ignoring " .. source .. " " .. postfix) | |
end | |
end, | |
ignore = function(self, w, we) -- w[3] the source to ignore, w[4] the duration | |
if self:_invalidNumArgs(w, 3, 4) or self:_invalidLength(we[3]) then return end | |
local duration = 0 -- default = permanent | |
if w[4] and #w[4] ~= 0 then | |
local err | |
duration, err = duration:parse(w[4]) | |
if err ~= nil then | |
return p("- could not parse duration, examples: 60s, 60m, 60days") | |
end | |
end | |
local endTime = 0 | |
if duration ~= 0 then | |
endTime = os.time() + duration | |
end | |
local source = w[3] | |
ignores:add(source, endTime, false) | |
p("- successfully ignored " .. source) | |
end, | |
unignore = function(self, w, we) | |
if self:_invalidNumArgs(w, 3, 3) or self:_invalidLength(we[3]) then return end | |
local source = w[3] | |
local deleted = ignores:del(source) | |
if not deleted then | |
p("- could not find " .. source .. " to unignore") | |
else | |
p("- unignored the highlights of " .. source .. " successfully") | |
end | |
end, | |
debug = function(self, w, we) | |
if self:_invalidNumArgs(w, 2, 2) then return end | |
local action = "" | |
if debugMode then | |
action = "disabled" | |
debugMode = false | |
else | |
action = "enabled" | |
debugMode = true | |
end | |
p("- debugging " .. action) | |
end, | |
set = function(self, w, we) | |
if self:_invalidNumArgs(w, 4, 4) or self:_invalidLength(we[3]) then return end | |
local varname = w[3]:lower() | |
local varvalue = w[4] | |
if varname ~= "globalhighlights" and | |
varname ~= "userhighlights" and | |
varname ~= "highlightdelta" and | |
varname ~= "autoignore" and | |
varname ~= "autoignoreduration" then | |
return p("- unknown variable name, see /help antihl") | |
end | |
if varname == "globalhighlights" or varname == "userhighlights" then | |
varvalue = tonumber(varvalue) | |
if not varvalue or varvalue < 0 then | |
return p("- argument is not a valid number, must be bigger than 0") | |
end | |
end | |
if varname == "highlightdelta" or varname == "autoignoreduration" then | |
local err | |
varvalue, err = duration:parse(varvalue) | |
if err ~= nil then | |
return p("- argument is not a valid duration, see /help antihl") | |
end | |
end | |
if varname == "highlightdelta" then | |
highlights:set("highlightDelta", varvalue) | |
elseif varname == "globalhighlights" then | |
highlights:set("globalHighlightLimit", varvalue) | |
elseif varname == "userhighlights" then | |
highlights:set("userHighlightLimit", varvalue) | |
elseif varname == "autoignore" then | |
if varvalue:match("^ye?s?$") or varvalue == "1" then | |
highlights:set("autoIgnore", 1) | |
elseif varvalue:match("^no?$") or varvalue == "0" then | |
highlights:set("autoIgnore", 0) | |
else | |
return p("- argument is not a valid boolean, see /help antihl") | |
end | |
elseif varname == "autoignoreduration" then | |
highlights:set("autoIgnoreDuration", varvalue) | |
end | |
p("- variable successfully set") | |
end, | |
} | |
local highlights = { | |
_state = {}, | |
_quotepattern = "([" .. ("%^$().[]*+-?"):gsub("(.)", "%%%1") .. "])", | |
-- the duration in the past to look back to to trigger user/global highlight | |
-- avoidance messages | |
_highlightDelta = 10 * 60, | |
-- the number of highlights above which we print a helpful message to ignore | |
-- the highlights of the user | |
_userHighlightLimit = 2, | |
-- the number of global highlights to trigger printing of a helpful message | |
-- to ignore every highlight | |
_globalHighlightLimit = 5, | |
-- whether to auto-ignore the user for 30m when the userHighlightLimit is hit | |
_autoIgnore = 1, | |
-- the duration to auto-ignore the user for | |
_autoIgnoreDuration = 30 * 60, | |
init = function(self) | |
local fields = { | |
"globalHighlightLimit", | |
"userHighlightLimit", | |
"highlightDelta", | |
"autoIgnore", | |
"autoIgnoreDuration", | |
} | |
for _, field in pairs(fields) do | |
local v = hexchat.pluginprefs[field] | |
if v and v ~= "" then | |
self:set(field, tonumber(v)) | |
end | |
end | |
end, | |
add = function(self, source) | |
table.insert(self._state, { | |
from = source:match("^[^!]+!(.*@.*)$"), -- ignore the nick | |
at = os.time(), | |
}) | |
end, | |
check = function(self, nick, message) -- whether the message is a highlight | |
local nick, _ = nick:gsub(self._quotepattern, "%%%1") | |
local words = { | |
nick | |
} | |
local extraHighlight = hexchat.prefs.irc_nick_hilight | |
if extraHighlight and extraHighlight ~= "" then | |
extraHighlight:gsub("([^ ,]+)", function(c) | |
words[#words+1] = c:gsub(self._quotepattern, "%%%1") | |
end) | |
end | |
-- just make sure we terminate the message so the pattern message even if | |
-- at the end - HACK | |
message = message .. ":" | |
for _, w in pairs(words) do | |
if message:match("[:, \001]" .. w .. "[:, \001]") then | |
return true | |
end | |
end | |
return false | |
end, | |
handle = function(self) | |
local epoch = os.time() - self._highlightDelta | |
local globalHighlights = 0 | |
local userHighlights = {} | |
for k, v in pairs(self._state) do | |
if v.at < epoch then | |
table.remove(self._state, k) | |
else | |
if not userHighlights[ v.from ] then | |
userHighlights[ v.from ] = 0 | |
end | |
userHighlights[ v.from ] = userHighlights[ v.from ] + 1 | |
globalHighlights = globalHighlights + 1 | |
end | |
end | |
handleDebug("GHL", globalHighlights) | |
local humanDuration = "" | |
local now | |
if self._autoIgnore == 1 then | |
humanDuration = duration:humanize(self._autoIgnoreDuration) | |
now = os.time() | |
end | |
-- only trigger messages once, when the limit is passed by one | |
-- and only trigger the global message if multiple users are highlighting us | |
for from, hlCount in pairs(userHighlights) do | |
handleDebug("UHL", from, hlCount) | |
if hlCount == self._userHighlightLimit + 1 then | |
if self._autoIgnore == 1 then | |
local pat = ".*!" .. from | |
ignores:add(pat, now + self._autoIgnoreDuration, false) | |
return p("- auto-ignoring user " .. pat .. " for " .. humanDuration .. ", type /antihl -unignore " .. pat .. " to undo, /antihl set autoignore no to disable") | |
end | |
return p("- to ignore all further userhighlights from this user, type: /antihl -ignore .*!" .. from .. " 30m") | |
elseif hlCount > self._userHighlightLimit then | |
globalHighlights = globalHighlights - hlCount | |
end | |
end | |
if globalHighlights == self._globalHighlightLimit + 1 then | |
if self._autoIgnore == 1 then | |
ignores:add(".*", now + self._autoIgnoreDuration, false) | |
return p("- auto-ignoring all highlights for " .. humanDuration .. ", type /antihl -unignore .* to undo, /antihl set autoignore no to disable") | |
end | |
return p("- to ignore all further globalhighlights for 30 minutes, type: /antihl -ignore .* 30m") | |
end | |
end, | |
set = function(self, name, value) | |
local key = "_" .. name | |
if not self[key] then | |
return false | |
end | |
self[key] = value | |
hexchat.pluginprefs[name] = value | |
end | |
} | |
-- try to expire the ignores every 5 seconds, the argument is in milisecs | |
hexchat.hook_timer(5000, function() ignores:expire() end) | |
hexchat.hook_server("PRIVMSG", function(w, we) | |
local nick = hexchat.get_info("nick") | |
if highlights:check(nick, we[4]) then | |
handleDebug("INC", w, we) | |
-- whether the user is ignored or not | |
local source = w[1]:sub(2) | |
local ignore = ignores:match(source) | |
local dialogName = source:match("^[^!]*") | |
local msg = we[4]:sub(2) | |
local ctx | |
local eventName | |
if w[3]:sub(1, 1) == "#" then -- TODO not all channels begin with a # | |
eventName = "Channel Message" | |
ctx = hexchat.find_context(hexchat.get_info("network"), w[3]) | |
else | |
eventName = "Private Message to Dialog" | |
ctx = hexchat.find_context(nil, dialogName) | |
if not ctx then -- no open dialog window, open it | |
hexchat.command("query " .. dialogName) | |
ctx = hexchat.find_context(nil, dialogName) | |
end | |
end | |
if w[4] == ":\001ACTION" then | |
if eventName == "Channel Message" then | |
eventName = "Channel Action" | |
elseif eventName == "Private Message to Dialog" then | |
eventName = "Private Action" | |
end | |
msg = msg:sub(9, #msg - 1) | |
end | |
if ignore and ctx then | |
handleDebug("IGN", w, we) | |
ctx:emit_print(eventName, dialogName, msg) | |
return hexchat.EAT_HEXCHAT | |
end | |
highlights:add(source) | |
highlights:handle() | |
end | |
end) | |
-- cant use tabs seemingly for the help text | |
hexchat.hook_command("antihl", function(w, we) commands:_handle(w, we) end, [[Usage: | |
/antihl -list | |
/antihl -ignore <lua pattern> [<duration (example: 60s, default: permanent aka 0 if left out)>] | |
/antihl -unignore <lua pattern> | |
/antihl -set <var> <value> where var can be: | |
globalhighlights or userhighlights with a number argument | |
highlightdelta with a duration argument (default: 10m) | |
autoignore with a boolean argument (yes, no, default: yes) | |
autoignoreduration with a duration argument (default: 30m) | |
/antihl -debug | |
Lua patterns: lua.org/manual/5.3/manual.html#6.4.1]] | |
) | |
ignores:init() | |
highlights:init() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment