Skip to content

Instantly share code, notes, and snippets.

@sztanpet
Last active October 19, 2015 12:26
Show Gist options
  • Save sztanpet/fb5adc38f54863f3ea0f to your computer and use it in GitHub Desktop.
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 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