From 3186f0c0d6590f9203c00be35bd1bbec5ef1198a Mon Sep 17 00:00:00 2001 From: ANAND Date: Mon, 27 Apr 2020 22:17:41 +0530 Subject: [PATCH] Implement an API; add support for custom filters --- filters.lua | 58 ++++++++++++++ init.lua | 218 ++++++++++++++++++++++++++++------------------------ readme.md | 59 ++++++++------ 3 files changed, 212 insertions(+), 123 deletions(-) create mode 100644 filters.lua diff --git a/filters.lua b/filters.lua new file mode 100644 index 0000000..c8eb53f --- /dev/null +++ b/filters.lua @@ -0,0 +1,58 @@ +-- Words filter +filter.register_filter("words", function(name, message) + for _, w in ipairs(filter.assets["words"].words) do + if string.find(message:lower(), "%f[%a]" .. w .. "%f[%A]") then + return "Watch your language!" + end + end +end) + +-- Initialize words list +filter.register_on_init(function() + local sw = s:get_string("words") + if sw and sw ~= "" then + filter.assets["words"].words = minetest.parse_json(sw) + end + + if #filter.assets["words"].words == 0 then + local words = {} + local file = io.open(filepath, "r") + if file then + for line in file:lines() do + line = line:trim() + if line ~= "" then + words[#words + 1] = line:trim() + end + end + filter.assets["words"].words = words + end + end +end) + +-- Words filter sub-command +filter.register_chatcommand("words", { + description = "Usage: /filter words []", + func = function(cmd) + local words = filter.assets["words"].words + if cmd == "list" then + return true, #words .. " words: " .. table.concat(words, ", ") + elseif cmd == "add" then + table.insert(filter.assets["words"].words, param) + filter.storage:set_string("words", minetest.write_json(words)) + filter.assets["words"].words = words + return true, "Added \"" .. val .. "\"." + elseif cmd == "remove" then + for i, w in ipairs(words) do + if w == param then + table.remove(words, i) + filter.storage:set_string("words", minetest.write_json(words)) + filter.assets["words"].words = words + return true, "Removed \"" .. param .. "\"." + end + end + return false, "\"" .. param .. "\" not found in list." + else + return false, "Invalid option. See /help filter" + end + end +}) diff --git a/init.lua b/init.lua index 2808d6f..58096b3 100644 --- a/init.lua +++ b/init.lua @@ -23,84 +23,98 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -]]-- +--]] -filter = { registered_on_violations = {} } -local words = {} +filter = { + assets = {}, + storage = minetest.get_mod_storage() +} local muted = {} local violations = {} -local s = minetest.get_mod_storage() +local registered_on_violations = {} +local registered_on_init = {} +local registered_filters = {} +local registered_chatcommands = {} -function filter.init() - local sw = s:get_string("words") - if sw and sw ~= "" then - words = minetest.parse_json(sw) - end +-- Run callback at startup. Can be used for data initialization +-- or loading assets from mod storage +function filter.register_on_init(func) + table.insert(registered_on_init, func) +end - if #words == 0 then - filter.import_file(minetest.get_modpath("filter") .. "/words.txt") +-- Filters return a warning string if triggered +function filter.register_filter(name, func) + assert(not registered_filters[name]) + registered_filters[name] = func + filter.assets["name"] = {} +end + +local help_str = "" +local function update_help() + help_str = "" + for name, def in pairs(registered_chatcommands) do + help_str = help_str .. "\n\t\"" .. name .."\": " .. def.description end end -function filter.import_file(filepath) - local file = io.open(filepath, "r") - if file then - for line in file:lines() do - line = line:trim() - if line ~= "" then - words[#words + 1] = line:trim() - end - end - return true - else - return false - end +-- /filter sub-command registration method +function filter.register_chatcommand(name, def) + assert(not registered_chatcommands[name]) + registered_chatcommands[name] = def + update_help() end -function filter.register_on_violation(func) - table.insert(filter.registered_on_violations, func) -end +-- check_message invokes all filters, and returns true and a '\n' +-- separated list of warnings if at least one filter is triggered +local function check_message(name, message) + local str + local ret_val = {} -function filter.check_message(name, message) - for _, w in ipairs(words) do - if string.find(message:lower(), "%f[%a]" .. w .. "%f[%A]") then - return false + for _, fn in pairs(filter.registered_filters) do + str = fn(name, message) + if str and str ~= "" then + table.insert(ret_val[name], str) end end - return true + local ret = table.concat(ret_val[name], "\n") + if ret and ret ~= "" then + ret_val[name] = {} + return true, ret + end + return false end -function filter.mute(name, duration) +local function mute(name, duration) do local privs = minetest.get_player_privs(name) privs.shout = nil minetest.set_player_privs(name, privs) end - minetest.chat_send_player(name, "Watch your language! You have been temporarily muted") + minetest.chat_send_player(name, "You have been temporarily muted for abusing the chat.") muted[name] = true - minetest.after(duration * 60, function() - privs = minetest.get_player_privs(name) + minetest.after(duration * 60, function(name) + local privs = minetest.get_player_privs(name) if privs.shout == true then return end muted[name] = nil - minetest.chat_send_player(name, "Chat privilege reinstated. Please do not abuse chat.") + minetest.chat_send_player(name, "Chat privilege reinstated. Please do not abuse the chat.") privs.shout = true minetest.set_player_privs(name, privs) - end) + end, name) end -function filter.show_warning_formspec(name) - local formspec = "size[7,3]bgcolor[#080808BB;true]" .. default.gui_bg .. default.gui_bg_img .. [[ - image[0,0;2,2;filter_warning.png] - label[2.3,0.5;Please watch your language!] - ]] +local function show_warning_formspec(name, warnings) + local formspec = "size[7,3]bgcolor[#080808BB;true]" + .. "default.gui_bg" .. "default.gui_bg_img" + .. "image[0,0;2,2;filter_warning.png]" + .. "label[2.3,0.5;" .. warnings .. "]" if minetest.global_exists("rules") and rules.show then formspec = formspec .. [[ @@ -115,7 +129,7 @@ function filter.show_warning_formspec(name) minetest.show_formspec(name, "filter:warning", formspec) end -function filter.on_violation(name, message) +local function on_violation(name, message, warnings) violations[name] = (violations[name] or 0) + 1 local resolution @@ -129,13 +143,13 @@ function filter.on_violation(name, message) if not resolution then if violations[name] == 1 and minetest.get_player_by_name(name) then resolution = "warned" - filter.show_warning_formspec(name) + show_warning_formspec(name, warnings) elseif violations[name] <= 3 then resolution = "muted" - filter.mute(name, 1) + mute(name, 1) else resolution = "kicked" - minetest.kick_player(name, "Please mind your language!") + minetest.kick_player(name, "Kicked for abusing the chat.") end end @@ -148,6 +162,29 @@ function filter.on_violation(name, message) end end +local function make_checker(old_func) + return function(name, param) + if not check_message(name, param) then + on_violation(name, param) + return false + end + + return old_func(name, param) + end +end + +local old_register_chatcommand = minetest.register_chatcommand +function minetest.register_chatcommand(name, def) + if def.privs and def.privs.shout then + def.func = make_checker(def.func) + end + return old_register_chatcommand(name, def) +end + +function filter.register_on_violation(func) + table.insert(registered_on_violations, func) +end + table.insert(minetest.registered_on_chat_messages, 1, function(name, message) if message:sub(1, 1) == "/" then return @@ -159,39 +196,19 @@ table.insert(minetest.registered_on_chat_messages, 1, function(name, message) return true end - if not filter.check_message(name, message) then - filter.on_violation(name, message) + local dirty, warnings = check_message(name, message) + if dirty then + on_violation(name, message, warnings) return true end end) - -local function make_checker(old_func) - return function(name, param) - if not filter.check_message(name, param) then - filter.on_violation(name, param) - return false - end - - return old_func(name, param) - end -end - for name, def in pairs(minetest.registered_chatcommands) do if def.privs and def.privs.shout then def.func = make_checker(def.func) end end -local old_register_chatcommand = minetest.register_chatcommand -function minetest.register_chatcommand(name, def) - if def.privs and def.privs.shout then - def.func = make_checker(def.func) - end - return old_register_chatcommand(name, def) -end - - local function step() for name, v in pairs(violations) do violations[name] = math.floor(v * 0.5) @@ -204,39 +221,25 @@ end minetest.after(10*60, step) minetest.register_chatcommand("filter", { - params = "filter server", - description = "manage swear word filter", + params = " []", + description = "List of possible sub-commands:" .. help_str, privs = {server = true}, func = function(name, param) - local cmd, val = param:match("(%w+) (.+)") - if param == "list" then - return true, #words .. " words: " .. table.concat(words, ", ") - elseif cmd == "add" then - table.insert(words, val) - s:set_string("words", minetest.write_json(words)) - return true, "Added \"" .. val .. "\"." - elseif cmd == "remove" then - for i, w in ipairs(words) do - if w == val then - table.remove(words, i) - s:set_string("words", minetest.write_json(words)) - return true, "Removed \"" .. val .. "\"." - end - end - return true, "\"" .. val .. "\" not found in list." - else - return true, "I know " .. #words .. " words.\nUsage: /filter []" + local fn, cmd = param:match("(%w+) (.+)") + if fn then + minetest.chat_send_all("fn " .. fn) + end + if cmd then + minetest.chat_send_all("cmd " .. cmd) end - end, -}) -if minetest.global_exists("rules") and rules.show then - minetest.register_on_player_receive_fields(function(player, formname, fields) - if formname == "filter:warning" and fields.rules then - rules.show(player) + if not registered_chatcommands[fn] then + return false, "Invalid sub-command. See /help filter" end - end) -end + + return registered_chatcommands[fn].func(cmd) + end +}) minetest.register_on_shutdown(function() for name, _ in pairs(muted) do @@ -246,4 +249,21 @@ minetest.register_on_shutdown(function() end end) -filter.init() +if minetest.global_exists("rules") and rules.show then + minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "filter:warning" and fields.rules then + rules.show(player) + end + end) +end + +-------------------------------------------------------------------------------- +-- Parse filters +local modpath = minetest.get_modpath("filter") .. "/" +dofile(modpath .. "filters.lua") + +-------------------------------------------------------------------------------- +-- Run all registered_on_init callbacks +for _, init in pairs(registered_on_init) do + init() +end diff --git a/readme.md b/readme.md index 27293e2..89ff817 100644 --- a/readme.md +++ b/readme.md @@ -1,38 +1,49 @@ # filter mod -This mod adds a simple chat filter. There is no default word list, -and adding words to the filter list is done through the `/filter` -chat command. You need the `server` priv to use the chat command. +This mod adds a chat filter API that can be used to register custom +chat filters. As of now a bad words filter has been implemented. There +is no default word list, and adding words to the filter list is done +through the `/word_filter` chat command, which requires `server` priv. -The `/filter` chat command can `add`, `remove` or `list` words. The +The `/word_filter` chat command can `add`, `remove` or `list` words. The words are stored in `mod_storage`, which means that this mod requires 0.4.16 or above to function. -If a player speaks a word that is listed in the filter list, they are -muted for 1 minute. After that, their `shout` privilege is restored. -If they leave, their `shout` privilege is still restored, but only after -the time expires, not before. +If a player triggers a filter, they are muted for 1 minute. After that, +their `shout` privilege is restored. If they leave, their `shout` +privilege is still restored, but only after the time expires, not before. ## API +### Custom filter registration + +* `filter.register_filter(name, func(playername, message))` + * Takes a name and a two-parameter function + * `name` is the name of the filter, which is currently unused, except + for indexing. + * `playername` and `message` hold the name of the player and the + message they sent, respectively. + * `func` should return a relevant warning when triggered. e.g. + "Watch your language!", and `nil` when message has passed the check. + ### Callbacks -* filter.register_on_violation(func(name, message, violations)) - * Violations is the value of the player's violation counter - which is - incremented on a violation, and halved every 10 minutes. - * Return true if you've handled the violation. No more callbacks will be - executation, and the default behaviour (warning/mute/kick) on violation - will be skipped. +* `filter.register_on_violation(func(name, message, violations))` + * `violations` is the value of the player's violation counter - which is + incremented on a violation, and halved every 10 minutes. + * Return `true` if you've handled the violation. No more callbacks will be + executation, and the default behaviour (warning/mute/kick) on violation + will be skipped. ### Methods -* filter.import_file(path) - * Input bad words from a file (`path`) where each line is a new word. -* filter.check_message(name, message) - * Checks message for violation. Returns true if okay, false if bad. - If it returns false, you should cancel the sending of the message and - call filter.on_violation() -* filter.on_violation(name, message) - * Increments violation count, runs callbacks, and punishes the players. -* filter.mute(name, duration) -* filter.show_warning_formspec(name) +* `filter.import_file(path)` + * Input bad words from a file (`path`) where each line is a new word. +* `filter.check_message(name, message)` + * Checks message for violation. Returns `true` if bad, `false` if ok. + If it returns true, the message is not sent `filter.on_violation` is + called. +* `filter.on_violation(name, message)` + * Increments violation count, runs callbacks, and punishes the players. +* `filter.mute(name, duration)` +* `filter.show_warning_formspec(name)`