minetest-chat_highlights/init.lua

902 lines
25 KiB
Lua

local register_on_receive = minetest.register_on_receiving_chat_message or minetest.register_on_receiving_chat_messages
if not register_on_receive then
return
end
local colorize = minetest.colorize
local mod_name = minetest.get_current_modname()
local function log(level, messagefmt, ...)
minetest.log(level, ("[%s] %s"):format(mod_name, messagefmt:format(...)))
end
log("action", "CSM loading...")
-- configurable values --
local ignore_messages = {
"Not chiselable",
"You are now a human",
"You are now a werewolf",
--"Werewolves only can eat raw meat!",
"You missed the snake",
"Nothing to replace.",
"Node replacement tool set to:",
"Your hit glanced off of the protection and turned you around. The protection deals you 1 damage.",
"Error: \"nothing\" is not a node.",
">>> You missed <<<",
}
local function escape_regex(x)
return (x:gsub("%%", "%%%%")
:gsub("^%^", "%%^")
:gsub("%$$", "%%$")
:gsub("%(", "%%(")
:gsub("%)", "%%)")
:gsub("%.", "%%.")
:gsub("%[", "%%[")
:gsub("%]", "%%]")
:gsub("%*", "%%*")
:gsub("%+", "%%+")
:gsub("%-", "%%-")
:gsub("%?", "%%?"))
end
local function should_ignore(text)
for _, ignore_message in ipairs(ignore_messages) do
local match, _ = text:match("(" .. escape_regex(ignore_message) .. ")")
if match and match ~= "" then
return true
end
end
return false
end
local PER_SERVER = true -- set to false if you want to use the same player statuses on all servers
local AUTO_ALERT_ON_NAME = true -- set to false if you don't want messages that mention you to highlight automatically
local COLOR_BY_STATUS = {
default="#888888", -- don't remove or change the name of this status!
server="#FF9900", -- don't remove or change the name of this status!
self="#FF8888", -- don't remove or change the name of this status!
-- these can be changed to your liking.
-- TODO: make these configurable in game?
secretz="#000000",
admin="#88FFFF",
privileged="#00FFFF",
poweruser="#55FFAA",
ally="#00FF55",
friend="#00FF00",
acquaintance="#55FF00",
contact="#AAFF00",
noob="#FFFF00",
trouble="#FF0000",
rival="#FF0088",
other="#FF00FF",
}
local LIGHTEN_TEXT_BY = .8 -- 0 == same color as status; 1 == pure white.
local DATE_FORMAT = "%Y%m%dT%H%M%S"
-- END configurable values --
-- general functions --
local function safe(func)
-- wrap a function w/ logic to avoid crashing the game
local f = function(...)
local status, out = pcall(func, ...)
if status then
return out
else
log("warning", "Error (func): " .. out)
return nil
end
end
return f
end
local function lc_cmp(a, b)
return a:lower() < b:lower()
end
local function pairsByKeys(t, f)
local a = {}
for n in pairs(t) do
table.insert(a, n)
end
table.sort(a, f)
local i = 0
return function()
i = i + 1
if a[i] == nil then
return nil
else
return a[i], t[a[i]]
end
end
end
local function round(x)
-- approved by kahan
if x % 2 ~= 0.5 then
return math.floor(x+0.5)
else
return x - 0.5
end
end
local function bound(min, val, max)
return math.min(max, math.max(min, val))
end
local function lighten(hex_color, percent)
-- lighten a hexcolor (#XXXXXX) by a percent (0.0=none, 1.0=full white)
local r = tonumber(hex_color:sub(2,3), 16)
local g = tonumber(hex_color:sub(4,5), 16)
local b = tonumber(hex_color:sub(6,7), 16)
r = bound(0, round(((1 - percent) * r) + (percent * 255)), 255)
g = bound(0, round(((1 - percent) * g) + (percent * 255)), 255)
b = bound(0, round(((1 - percent) * b) + (percent * 255)), 255)
return ("#%02x%02x%02x"):format(r, g, b)
end
local function get_date_string()
return os.date(DATE_FORMAT, os.time())
end
-- END general functions --
-- mod_storage access --
local mod_storage = minetest.get_mod_storage()
local server_id
if PER_SERVER then
local server_info = minetest.get_server_info()
server_id = server_info.address .. ":" .. server_info.port
else
server_id = ""
end
-- -- mod_storage: status_by_name -- --
local status_by_name
local function load_status_by_name()
local serialized_storage = mod_storage:get_string(server_id)
if string.find(serialized_storage, "return") then
return minetest.deserialize(serialized_storage)
else
mod_storage:set_string(server_id, minetest.serialize({}))
return {}
end
end
local function save_status_by_name()
mod_storage:set_string(server_id, minetest.serialize(status_by_name))
end
status_by_name = load_status_by_name()
local function get_name_status(name)
return status_by_name[name] or "default"
end
local function set_name_status(name, status)
status_by_name = load_status_by_name()
status_by_name[name] = status
save_status_by_name()
end
-- -- END mod_storage: status_by_name -- --
-- -- mod_storage: alert_patterns -- --
local alert_patterns
local function load_alert_patterns()
local serialized_storage = mod_storage:get_string(("%s:alert_patterns"):format(server_id))
if string.find(serialized_storage, "return") then
return minetest.deserialize(serialized_storage)
else
mod_storage:set_string(("%s:alert_patterns"):format(server_id), minetest.serialize({}))
return {}
end
end
local function save_alert_patterns()
mod_storage:set_string(("%s:alert_patterns"):format(server_id), minetest.serialize(alert_patterns))
end
alert_patterns = load_alert_patterns()
local function add_alert_pattern(pattern)
alert_patterns = load_alert_patterns()
alert_patterns[pattern] = true
save_alert_patterns()
end
local function remove_alert_pattern(pattern)
alert_patterns = load_alert_patterns()
alert_patterns[pattern] = nil
save_alert_patterns()
end
-- -- END mod_storage: alert_patterns -- --
-- -- mod_storage: disabled_servers -- --
local disabled_servers
local function load_disabled_servers()
local serialized_storage = mod_storage:get_string("disabled_servers")
if string.find(serialized_storage, "return") then
return minetest.deserialize(serialized_storage)
else
local ds = {["94.16.121.151:2500"] = true } -- disable on IFS by default
mod_storage:set_string("disabled_servers", minetest.serialize(ds))
return ds
end
end
local function save_disabled_servers()
mod_storage:set_string("disabled_servers", minetest.serialize(disabled_servers))
end
disabled_servers = load_disabled_servers()
local function toggle_disable_this_server()
local current_status
disabled_servers = load_disabled_servers()
if disabled_servers[server_id] then
disabled_servers[server_id] = nil
current_status = false
else
disabled_servers[server_id] = true
current_status = true
end
save_disabled_servers()
return current_status
end
-- -- END mod_storage: disabled_servers -- --
-- END mod_storage access --
-- initalization --
local set_my_name_tries = 0
local function set_my_name()
local name
if minetest.localplayer then
name = minetest.localplayer:get_name()
log("action", ("you are %s"):format(name))
set_name_status(name, "self")
if AUTO_ALERT_ON_NAME then
add_alert_pattern(name)
end
elseif set_my_name_tries < 20 then
set_my_name_tries = set_my_name_tries + 1
minetest.after(1, set_my_name)
else
log("warning", "could not determine name!")
end
end
if minetest.register_on_connect then
minetest.register_on_connect(set_my_name)
elseif minetest.register_on_mods_loaded then
minetest.register_on_mods_loaded(set_my_name)
else
minetest.after(1, set_my_name)
end
-- END initalization --
-- chat commands --
minetest.register_chatcommand("ch_toggle", {
description = ("turn %s on/off for this server"):format(mod_name),
func = safe(function()
local current_status = toggle_disable_this_server()
if current_status then
current_status = "off"
else
current_status = "on"
end
minetest.display_chat_message(("%s is now %s for server "%s""):format(mod_name, current_status, server_id))
end),
})
minetest.register_chatcommand("ch_statuses", {
description = "list statuses",
func = safe(function()
for name, color in pairsByKeys(COLOR_BY_STATUS) do
if name and color then
minetest.display_chat_message(colorize(color, ("%s: %s"):format(name, color)))
end
end
end),
})
minetest.register_chatcommand("ch_set", {
params = "<name> <status>",
description = "associate a name w/ a status",
func = safe(function(param)
local name, status = param:match("^(%S+)%s+(%S+)$")
if name ~= nil then
if not COLOR_BY_STATUS[status] then
minetest.display_chat_message(colorize("#FF0000", ("unknown status \"%s\""):format(status)))
return false
end
set_name_status(name, status)
minetest.display_chat_message(colorize(COLOR_BY_STATUS[status], ("%s is now %s"):format(name, status)))
return true
else
minetest.display_chat_message(colorize("#FF0000", "invalid syntax"))
return false
end
end),
})
minetest.register_chatcommand("ch_unset", {
params = "<name>",
description = "unregister a name",
func = safe(function(name)
set_name_status(name, nil)
minetest.display_chat_message(colorize(COLOR_BY_STATUS.server, ("unregistered %s"):format(name)))
end),
})
minetest.register_chatcommand("ch_list", {
description = "list all statuses",
func = safe(function()
for name, status in pairsByKeys(status_by_name, lc_cmp) do
local color = COLOR_BY_STATUS[status] or COLOR_BY_STATUS.default
minetest.display_chat_message(colorize(color, ("%s: %s"):format(name, status)))
end
end),
})
minetest.register_chatcommand("ch_alert_list", {
description = "list all alert patterns",
func = safe(function()
for pattern, _ in pairsByKeys(alert_patterns, lc_cmp) do
minetest.display_chat_message(colorize(COLOR_BY_STATUS.server, pattern))
end
end),
})
minetest.register_chatcommand("ch_alert_set", {
params = "<pattern>",
description = "alert on a given pattern",
func = safe(function(pattern)
add_alert_pattern(pattern)
end),
})
minetest.register_chatcommand("ch_alert_unset", {
params = "<pattern>",
description = "no longer alert on a given pattern",
func = safe(function(pattern)
remove_alert_pattern(pattern)
end),
})
-- END chat commands --
local function clean_android(msg)
-- supposedly, android surrounds messages with (c@#ffffff)
if msg:sub(1, 4) == "(c@#" then -- strip preceeding
msg = msg:sub(msg:find(")") + 1, -1)
if msg:sub(-11, -8) == "(c@#" then -- strip trailing
msg = msg:sub(-11)
end
end
return msg
end
local function clean_weird_crap(msg)
-- client side translation stuff in 5.5?
msg = msg:gsub("\27%(T@[^%)]+%)", "")
msg = msg:gsub("\27.", "")
return msg
end
local function get_color_by_name(name)
local _
name, _ = name:match("^([^@]+).*$") -- strip @... from IRC users
name, _ = name:match("^([^[]+).*$") -- strip [m] from matrix users
local status = get_name_status(name)
return COLOR_BY_STATUS[status] or COLOR_BY_STATUS.default
end
local function color_name(name)
local color = get_color_by_name(name)
return colorize(color, name)
end
local function color_names(names, delim)
local sorted_names = {}
for name in names:gmatch("[%w_%-]+") do
table.insert(sorted_names, name)
end
table.sort(sorted_names, lc_cmp)
for i, name in ipairs(sorted_names) do
sorted_names[i] = color_name(name)
end
return table.concat(sorted_names, delim)
end
local function color_text(name, text)
for pattern, _ in pairs(alert_patterns) do
if text:lower():match(pattern:lower()) then
minetest.sound_play("default_dug_metal")
return colorize(COLOR_BY_STATUS.self, text)
end
end
local color = get_color_by_name(name)
if color == COLOR_BY_STATUS.default then
return colorize(COLOR_BY_STATUS.default, text)
else
color = lighten(color, LIGHTEN_TEXT_BY)
return colorize(color, text)
end
end
local function idiv(a, b)
return (a - (a % b)) / b
end
local function seconds_to_interval(time)
local s = time % 60; time = idiv(time, 60)
local m = time % 60; time = idiv(time, 60)
local h = time % 24; time = idiv(time, 24)
if time ~= 0 then
return ("%d days %02d:%02d:%02d"):format(time, h, m, s)
elseif h ~= 0 then
return ("%02d:%02d:%02d"):format(h, m, s)
elseif m ~= 0 then
return ("%02d:%02d"):format(m, s)
else
return ("%d seconds"):format(s)
end
end
local function sort_privs(text)
local sorted_privs = {}
for priv in text:gmatch("[%w_%-]+") do
table.insert(sorted_privs, priv)
end
table.sort(sorted_privs, lc_cmp)
return table.concat(sorted_privs, ", ")
end
local t = {
-- SORT PRIVILEGES
{"^Privileges of ([^:]+): (.*)$", function(name, text)
return ("%s%s%s%s"):format(
color_text(name, "Privileges of "),
color_name(name),
color_text(name, ": "),
color_text(name, sort_privs(text))
)
end},
-- join/part messages
{"^%*%*%* (%S+) (.*)$", function(name, text)
return ("%s %s %s"):format(
color_text(name, "***"),
color_name(name),
color_text(name, text)
)
end},
-- yl discord messages
{"^<([^|%s]+)|([^>%s]+)>%s+(.*)$", function(source, name, text)
return ("%s%s%s%s%s %s"):format(
color_text(name, "<"),
color_text(name, source),
color_text(name, "|"),
color_name(name),
color_text(name, ">"),
color_text(name, text)
)
end},
-- normal messages
{"^<([^>%s]+)>%s+(.*)$", function(name, text)
return ("%s%s%s %s"):format(
color_text(name, "<"),
color_name(name),
color_text(name, ">"),
color_text(name, text)
)
end},
-- YL chatroom stuff
-- {"^\[([^@]+}@([^\]]+)\] (.*)$", function(name, channel, text)
-- return ("%s%s%s %s"):format(
-- color_text(name, "["),
-- color_name(name),
-- color_text(name, "@"),
-- color_name(channel),
-- color_text(name, "]"),
-- color_text(name, text)
-- )
-- end},
-- YL announce
{"^(%[[^%]]+%])%s(.*)$", function(t1, t2)
return ("%s %s"):format(
colorize(COLOR_BY_STATUS.server, t1),
colorize(COLOR_BY_STATUS.server, t2)
)
end},
-- prefixed messages
{"^(%S+)%s+<([^>]+)>%s+(.*)$", function(prefix, name, text)
return ("%s %s%s%s %s"):format(
color_text(name, prefix),
color_text(name, "<"),
color_name(name),
color_text(name, ">"),
color_text(name, text)
)
end},
-- Empire of Legends messages
{"^<(%S+)%s+([^>]+)>%s+(.*)$", function(prefix, name, text)
return ("%s%s %s%s %s"):format(
color_text(name, "<"),
color_text(name, prefix),
color_name(name),
color_text(name, ">"),
color_text(name, text)
)
end},
-- /me messages
{"^%* (%S+) (.*)$", function(name, text)
return ("%s %s %s"):format(
color_text(name, "*"),
color_name(name),
color_text(name, text)
)
end},
-- /msg messages
{"^[DP]M from (%S+): (.*)$", function(name, text)
minetest.sound_play("default_place_node_metal")
return ("%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "DM from "),
color_name(name),
colorize(COLOR_BY_STATUS.server, ": "),
colorize(COLOR_BY_STATUS.self, text)
)
end},
-- /tell messages
{"^(%S+) whispers: (.*)$", function(name, text)
minetest.sound_play("default_place_node_metal")
return ("%s%s%s%s"):format(
color_name(name),
colorize(COLOR_BY_STATUS.server, " whispers: "),
colorize(COLOR_BY_STATUS.self, text)
)
end},
-- /who
{"^Players in channel: (.*)$", function(names)
return ("%s%s"):format(
colorize(COLOR_BY_STATUS.server, "Players in channel: "),
color_names(names, ", ")
)
end},
-- /status
{"^# Server: (.*) clients={([^}]*)}(.*)", function(text, names, lastbit)
return ("%s%s%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "# Server: "),
colorize(COLOR_BY_STATUS.server, text),
colorize(COLOR_BY_STATUS.server, " clients={"),
color_names(names, ", "),
colorize(COLOR_BY_STATUS.server, "}"),
colorize(COLOR_BY_STATUS.server, lastbit)
)
end},
-- /status on YL
{"^# Server: (.*) clients: (.*)", function(text, names)
return ("%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "# Server: "),
colorize(COLOR_BY_STATUS.server, text),
colorize(COLOR_BY_STATUS.server, " clients: "),
color_names(names, ", ")
)
end},
-- IRC join messages
{"^%-!%- ([%w_%-]+) joined (.*)$", function(name, rest)
return ("%s%s%s%s"):format(
color_text(name, "-!- "),
color_name(name),
color_text(name, " joined "),
color_text(name, rest)
)
end},
-- IRC part messages
{"^%-!%- ([%w_%-]+) has quit (.*)$", function(name, rest)
return ("%s%s%s%s"):format(
color_text(name, "-!- "),
color_name(name),
color_text(name, " has quit "),
color_text(name, rest)
)
end},
-- IRC part messages
{"^%-!%- ([%w_%-]+) has left (.*)$", function(name, rest)
return ("%s%s%s%s"):format(
color_text(name, "-!- "),
color_name(name),
color_text(name, " has left "),
color_text(name, rest)
)
end},
-- IRC mode messages
{"^%-!%- mode/(.*)$", function(rest)
return colorize(COLOR_BY_STATUS.default, ("^%-!%- mode/%s$"):format(rest))
end},
-- IRC /nick messages
{"^%-!%- (.*) is now known as (.*)$", function(name1, name2)
return ("%s%s%s%s"):format(
color_text(name1, "-!- "),
color_name(name1),
color_text(name2, " is now know as "),
color_name(name2)
)
end},
-- DM sent
{"^[DP]M to (%S+): (.*)$", function(name, text)
return ("%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "DM to "),
color_name(name),
colorize(COLOR_BY_STATUS.server, ": "),
colorize(COLOR_BY_STATUS.server, text)
)
end},
-- BlS moderator PM snooping
{"^([%w_%-]+) to ([%w_%-]+): (.*)$", function(name1, name2, text)
return ("%s%s%s%s%s"):format(
color_name(name1),
colorize(COLOR_BY_STATUS.server, " to "),
color_name(name2),
colorize(COLOR_BY_STATUS.server, ": "),
colorize(COLOR_BY_STATUS.server, text)
)
end},
-- BlS unverified player notice
{"^Player ([%w_%-]+) is unverified%.$", function(name)
minetest.sound_play("default_dug_metal")
return colorize("#FF0000", ("Player %s is unverified."):format(name))
end},
-- BlS unverified player chat
{"^%[unverified] <([^>]+)>%s+(.*)$", function(name, text)
minetest.sound_play("default_dug_metal")
return colorize("#FF0000", ("[unverified] <%s> (%s)$"):format(name, text))
end},
-- BlS cloaked chat
{"^%-Cloaked%-%s+<([^>]+)>%s+(.*)$", function(name, text)
return ("%s%s%s%s %s"):format(
colorize(COLOR_BY_STATUS.server, "-Cloaked- "),
color_text(name, "<"),
color_name(name),
color_text(name, ">"),
color_text(name, text)
)
end},
-- death messages
{"^(%S+) was killed by (%S+), using (.+), near (.+)$", function(victim, killer, weapon, location)
return ("%s%s%s%s%s%s%s"):format(
color_name(victim),
color_text(victim, " was killed by "),
color_name(killer),
color_text(killer, ", using "),
color_text(killer, weapon),
color_text(victim, ", near "),
color_text(victim, location)
)
end},
{"^(%S+) was killed by (.*)$", function(name, text)
return ("%s%s%s"):format(
color_name(name),
color_text(name, " was killed by "),
color_text(name, text)
)
end},
{"^(%S+) should not play with ([^,]+), near ([^%.]+)%.", function(name, what, where)
return ("%s%s%s%s%s%s"):format(
color_name(name),
color_text(name, " should not play with "),
color_text(name, what),
color_text(name, ", near "),
color_text(name, where),
color_text(name, ".")
)
end},
{"^(%S+) shouldn't play with (.*)$", function(name, text)
return ("%s%s%s"):format(
color_name(name),
color_text(name, " shouldn't play with "),
color_text(name, text)
)
end},
{"^(%S+) was killed near (.*)$", function(name, text)
return ("%s%s%s"):format(
color_name(name),
color_text(name, " was killed near "),
color_text(name, text)
)
end},
{"^(%S+) has fallen near (.*)$", function(name, text)
return ("%s%s%s"):format(
color_name(name),
color_text(name, " has fallen near "),
color_text(name, text)
)
end},
{"^(%S+) has drown in ([^,]+), near ([^%.]+)%.", function(name, what, where)
return ("%s%s%s%s%s%s"):format(
color_name(name),
color_text(name, " has drown in "),
color_text(name, what),
color_text(name, ", near "),
color_text(name, where),
color_text(name, ".")
)
end},
{"^(%S+) has drown in (.*)", function(name, what)
return ("%s%s%s%s%s%s"):format(
color_name(name),
color_text(name, " has drown in "),
color_text(name, what),
color_text(name, ", near "),
color_text(name, where),
color_text(name, ".")
)
end},
-- rollback_check messages
{"%((%-?%d+,%-?%d+,%-?%d+)%) player:(%S+) (%S*) %-> (%S*) (%d+) seconds ago%.", function(pos, name, item1, item2, time)
if item1 == "air" then
item1 = colorize("#FF0000", item1)
else
item1 = colorize(COLOR_BY_STATUS.server, item1)
end
if item2 == "air" then
item2 = colorize("#FF0000", item2)
else
item2 = colorize(COLOR_BY_STATUS.server, item2)
end
return ("(%s) player:%s %s -> %s %s ago."):format(
colorize(COLOR_BY_STATUS.server, pos),
color_name(name),
item1,
item2,
seconds_to_interval(tonumber(time))
)
end},
-- YL thankyous
{"^Adventurer (%S+) received a 'Thank you' from (%S+)$", function(name1, name2)
return ("%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "Adventurer "),
color_name(name1),
colorize(COLOR_BY_STATUS.server, " received a 'Thank you' from "),
color_name(name2)
)
end},
-- YL levels
{"^Congratulations, (%S+) reached L(%d+)$", function(name, level)
return ("%s%s%s%s"):format(
colorize(COLOR_BY_STATUS.server, "Congratulations "),
color_name(name),
colorize(COLOR_BY_STATUS.server, " reached L"),
color_text(name, level)
)
end},
}
local last_message = ""
register_on_receive(safe(function(message)
if disabled_servers[server_id] then
return false
end
if message == last_message then
return true
else
last_message = message
end
local msg = minetest.gettext(message)
msg = minetest.strip_colors(msg)
msg = clean_android(msg)
msg = clean_weird_crap(msg)
--log("action", "%q", msg)
if should_ignore(msg) then
return true
end
local date = get_date_string()
for _, stuff in ipairs(t) do
local key, fun = unpack(stuff)
local parts = {msg:match(key)}
if #parts > 0 then
local fmsg = fun(unpack(parts))
if fmsg then
minetest.display_chat_message(("%s %s"):format(date, fmsg))
return true
end
end
end
end))