diff --git a/README.md b/README.md new file mode 100644 index 0000000..235903b --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# mcompletescript + +![](https://img.shields.io/badge/written%20in-Lua-blue) + +A PtokaX extension adding several features. + +User features: +- Catchup chat history +- Full chat archive +- User registration and password change support +- Block search terms +- Block invisible space characters in user nicks +- Group chat support with UserCommand integration + +Administrator features: +- Manage ptokax scripts +- Retrieve hub statistics +- Impersonate users + +Tags: nmdc + + +## Download + +- [⬇️ mCompleteScript_v20.lua](dist-archive/mCompleteScript_v20.lua) *(21.02 KiB)* diff --git a/dist-archive/mCompleteScript_v20.lua b/dist-archive/mCompleteScript_v20.lua new file mode 100644 index 0000000..16c185a --- /dev/null +++ b/dist-archive/mCompleteScript_v20.lua @@ -0,0 +1,699 @@ +--[[ _ _ /~` _ _ _ _ | _ _|_ _ (~ _ _. _ _|_ + | | |\_,(_)| | ||_)|(/_ | (/__)(_| ||_) | v21 + | | +History: Mappy v1..17 2008-2009, v18 Jan 2010, v19 Dec 2012, v20 Jan 2013, v21 Aug 2013 +For time/date formatting, see lua.org/pil/22.1.html +]] + +-----------------------------------------------------------[ options ] + +-- Configure features +bBlockInvisibleCharacters = false +bBlockIllegalSearches = false +bEnableCatchup = true +bEnableRegistration = true +bEnableUserLocations = false +bEnableImpersonate = false +bEnableChatLogging = true +bEnableGroupChat = true + +il = 15 -- Number of chat log messages to keep in memory +tf = "[%H.%M]" -- Format of chat log timestamps +AdminLevel = 0 -- Permission required for admin features (by default 0 is 'Master') +RegLevel = 3 -- Permission given upon registration (by default 3 is 'Registered') +PLL = 5 -- Minimum password length (must be at least one) + +-- Words that flag searches; +badwords = {"clop"} + +-- Settings for chat logs +chatLogPath = Core.GetPtokaXPath().."logs/%B - %Y - 127.0.0.1.txt" +chatLogTimeFormat = "%Y-%m-%d %H:%M:%S" + +-- Settings for group chats +groupChatPruneCheckEvery = 30 +groupChatTimeoutAfter = 15*60 + +-- Strings +SZ_REG_CONNECTED = "Thank you for using a registered nickname!" +SZ_UNREG_CONNECTED = "Want to secure your nickname? Register it." +SZ_REG_COMPLETED = "Your nickname is now registered. In future, log on with your password." +SZ_INVISIBLE_CHAR = "Sorry, remove all invisible characters from your name to reconnect." +SZ_REDIR_REASON = "Rearranging network, please be patient..." +SZ_E_DISABLED = "* Feature disabled." +SZ_E_SYNTAX_ERROR = "* Syntax error." +SZ_E_UNAUTHORISED = "* Unauthorised." +SZ_E_SCRIPT_ERROR = "* Error performing script operation." + +SZ_GCHAT_CLOSING = "## Closing group chat..." +SZ_GCHAT_AWAYMSG = "Away" + +-----------------------------------------------------------[ data storage ] + +groupchats = {} +groupChatTimer = nil +ml = {""} +mp = -1 +hall = { + -- 1-indexed, + "Rochester and Rutherford", -- 8..15 + "Ilam Apartments", -- 16..31 + "Ilam Apartments", + "Bishop Julius", -- 32..39 + "College House", -- 40..47 + "University Hall", -- 48..55 + "Sonada" -- 56..63 +} +-----------------------------------------------------------[ connections ] + +function OpConnected(user) SC(user) end +function RegConnected(user) SC(user) end + +function UserConnected(user) + if bBlockInvisibleCharacters then + -- Only filters unregistered users + if string.find(user.sNick, "") then + Core.SendToUser(user, "* "..SZ_INVISIBLE_CHAR) + BanMan.TempBanNick(user.sNick, 1, "Use of invisible character", Core.GetHubSecAlias) + Core.SendToOps("<"..Core.GetHubSecAlias.."> "..user.sNick.." tried to use an invisible character!") + end + end + SC(user) +end + +function SC(user, sendMessages) + -- CONTEXT_HUB = 0x01 + -- CONTEXT_USER = 0x02 + if sendMessages == nil then sendMessages = true end + if not user then return end + + -- Clear existing + AddUSC(user, "255 3", "", "") + + -- Locations + if (bEnableUserLocations) then + AddUSC(user, "1 2", "Get User Location", "!locate %[nick]") + end + + -- Registration + if (bEnableRegistration) then + if user.iProfile == RegLevel then + if sendMessages then Core.SendToUser(user, "* "..SZ_REG_CONNECTED) end + AddUSC(user, "1 1", "Change your password...", "!reg %[line:Enter a password]") + else + if sendMessages then Core.SendToUser(user, "* "..SZ_UNREG_CONNECTED) end + AddUSC(user, "1 1", "Register your nick...", "!reg %[line:Enter a password]") + end + end + + -- Catchup + if (bEnableCatchup) then + if sendMessages then SendCatchup(user) end + AddUSC(user, "1 1", "Get chat history", "!catchup") + end + + -- Impersonation + if (bEnableImpersonate or user.iProfile == AdminLevel) then + AddUSC(user, "1 2", "Impersonate\\Say as user...", "!yuo %[nick] %[line:Enter text]") + AddUSC(user, "1 2", "Impersonate\\Action as user [normal]...", "!yuo %[nick] /me %[line:Enter text]") + AddUSC(user, "1 2", "Impersonate\\Action as user [system]...", "!you %[nick] %[line:Enter text]") + AddUSC(user, "0 2", "", "") + AddUSC(user, "1 3", "Impersonate\\Send system message...", "!you %[line:Enter text]") + end + + -- Group chats + if (bEnableGroupChat) then + AddUSC(user, "1 1", "Group chat\\New group chat", "!groupchat_new") + local foundAnyChats = false + for botnick, grinfo in pairs(groupchats) do + if grinfo["owner"] == user.sNick then + if not foundAnyChats then + AddUSC(user, "1 1", "Group chat\\-", "") + foundAnyChats = true + end + AddUSC(user, "1 1", "Group chat\\Close chat ["..botnick.."]", "!groupchat_close "..botnick) + AddUSC(user, "1 2", "Invite user to join ["..botnick.."]", "!groupchat_invite "..botnick.." %[nick]") + end + end + end + + -- Admin tasks + if (user.iProfile == AdminLevel) then + + AddUSC(user, "1 1", "Admin\\Restart PtokaX", "!restart") + AddUSC(user, "1 1", "Admin\\Redirect all users...", "!redirectallusers %[line:Redirect address]") + AddUSC(user, "1 1", "Admin\\Set hub title...", "!sethubtitle %[line:Hub title]") + AddUSC(user, "1 1", "Admin\\Reload MOTD from disk", "!reload-motd") + AddUSC(user, "0 1", "Admin\\-", "") + AddUSC(user, "1 2", "Admin\\Look up user password", "!get-password %[nick]") + if bEnableCatchup then + AddUSC(user, "1 1", "Admin\\Flush catchup ring buffer", "!catchup-flush") + end + AddUSC(user, "1 1", "Admin\\Chat as "..Core.GetHubSecAlias().."...", "!yuo "..Core.GetHubSecAlias().." %[line:Enter text]") + AddUSC(user, "1 1", "Admin\\Get hub stats", "!get-stats") + AddUSC(user, "1 1", "Admin\\Refresh menu", "!refreshmenu") + + -- Scripts menu + + AddUSC(user, "0 1", "Admin\\-", "") + ScriptMan.Refresh() + for i,v in ipairs(ScriptMan.GetScripts()) do + local title = "Admin\\Scripts\\["..(v.bEnabled and "X" or " ").."] "..v.sName.."\\" + if (v.bEnabled) then + AddUSC(user, "1 1", title.."Disable", "!stop-script "..v.sName) + else + AddUSC(user, "1 1", title.."Enable", "!start-script "..v.sName) + end + AddUSC(user, "1 1", title.."Restart", "!restart-script "..v.sName) + end + + end +end + +-----------------------------------------------------------[ main parsing ] + +triggers = { + + -- Usage: !locate username + ["!locate"] = function(user, data, t) + if not bEnableUserLocations then return Core.SendToUser(user, SZ_E_DISABLED) end + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + + Core.SendToUser(user, GetHallFromIP(t[2])) + end, + + -- Usage: !reg password + ["!reg"] = function(user, data, t) + if not bEnableRegistration then return Core.SendToUser(user, SZ_E_DISABLED) end + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + + if string.len(t[2])>=PLL then + if RegMan.GetReg(user.sNick)==nil then + RegMan.AddReg(user.sNick, t[2], RegLevel) + Core.SendToUser(user, "* "..SZ_REG_COMPLETED) + else + RegMan.ChangeReg(user.sNick, t[2], RegLevel) + Core.SendToUser(user, "* Password changed.") + end + RegMan.Save() + else + Core.SendToUser(user, "* Passwords must be at least "..PLL.." characters long.") + end + end, + + -- Usage: !catchup + ["!catchup"] = function(user, data, t) + if not bEnableCatchup then return Core.SendToUser(user, SZ_E_DISABLED) end + + SendCatchup(user) + end, + + -- Action as user + -- Usage: !you their-nick message goes here + ["!you"] = function(user, data, t) + if bEnableImpersonate or user.iProfile == AdminLevel then + local m + _,m=s2(data) + _,m=s2(m) + Core.SendToAll("* "..m) + logCatchup("* "..m) + else + Core.SendToUser(user, SZ_E_DISABLED) + end + end, + + -- Say as user + -- Usage: !yuo their-nick message goes here + ["!yuo"] = function(user, data, t) + if bEnableImpersonate or user.iProfile == AdminLevel then + local m, u, mg + _,m=s2(data) + _,m=s2(m) + u,mg=s2(m) + Core.SendToAll("<"..u.."> "..mg) + logCatchup("<"..u.."> "..mg) + else + Core.SendToUser(user, Z_E_DISABLED) + end + end, + + -- Initiate a group chat + -- Usage: !groupchat_new + ["!groupchat_new"] = function(user, data, t) + if bEnableGroupChat or user.iProfile == AdminLevel then + if #t ~= 1 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + groupchat_init(user.sNick) + else + Core.SendToUser(user, Z_E_DISABLED) + end + end, + + -- Invite a user to a group chat under your control + -- Usage: !groupchat_invite botnick friendnick + ["!groupchat_invite"] = function(user, data, t) + if bEnableGroupChat or user.iProfile == AdminLevel then + if #t ~= 3 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + groupchat_invite(user, t[2], t[3]) + else + Core.SendToUser(user, Z_E_DISABLED) + end + end, + + -- Close a group chat under your control + -- Usage: !groupchat_close botnick + ["!groupchat_close"] = function(user, data, t) + if bEnableGroupChat or user.iProfile == AdminLevel then + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + groupchat_end(user, t[2]) + else + Core.SendToUser(user, Z_E_DISABLED) + end + end + +} +adminTriggers = { + + -- Usage: !get-password their-nick + ["!get-password"] = function(user, data, t) + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + + local reguser = RegMan.GetReg(t[2]) + if reguser then + Core.SendToUser(user, "User "..reguser.sNick.." has password '"..reguser.sPassword.."'") + else + Core.SendToUser(user, "User "..t[2].." not registered.") + end + end, + + -- Usage: !reload-motd + ["!reload-motd"] = function(user, data, t) + local motd = readFile(Core.GetPtokaXPath().."cfg/Motd.txt", "rb") + if motd then + SetMan.SetMOTD(motd) + SetMan.Save() + Core.SendToUser(user, "<"..Core.GetHubSecAlias().."> "..SetMan.GetMOTD()) + else + Core.SendToUser(user, "* Error opening '"..Core.GetPtokaXPath().."cfg/Motd.txt'") + end + end, + + -- Usage: !redirectallusers target-address + ["!redirectallusers"] = function(user, data, t) + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + + local allUsers = Core.GetOnlineUsers() + for _, u in pairs(allUsers) do + Core.SendToUser(user, "* Redirecting ".. u.sNick .." to "..t[2]) + Core.Redirect(u, t[2], SZ_REDIR_REASON) + end + end, + + -- Usage: !refreshmenu + ["!refreshmenu"] = function(user, data, t) + SC(user, false) + end, + + -- Usage: !start-script filename.lua + ["!start-script"] = function(user, data, t) + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + if (ScriptMan.StartScript(t[2]) == nil) then Core.SendToUser(user, SZ_E_SCRIPT_ERROR) end + + Core.SendToUser(user, "* Script "..t[2].." started."); + SetMan.Save() + SC(user, false) + end, + + -- Usage: !stop-script filename.lua + ["!stop-script"] = function(user, data, t) + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + if (ScriptMan.StopScript(t[2]) == nil) then Core.SendToUser(user, SZ_E_SCRIPT_ERROR) end + + Core.SendToUser(user, "* Script "..t[2].." stopped."); + SetMan.Save() + SC(user, false) + end, + + -- Usage: !restart-script filename.lua + ["!restart-script"] = function(user, data, t) + if #t ~= 2 then return Core.SendToUser(user, SZ_E_SYNTAX_ERROR) end + if (ScriptMan.RestartScript(t[2]) == nil) then Core.SendToUser(user, SZ_E_SCRIPT_ERROR) end + + Core.SendToUser(user, "* Script "..t[2].." restarted."); + SetMan.Save() + SC(user, false) + end, + + -- Usage: !get-stats + ["!get-stats"] = function(user, data, t) + Core.SendToUser(user, "* Peak users this boot: "..Core.GetActualUsersPeak()) + Core.SendToUser(user, "* Peak users all time: "..Core.GetMaxUsersPeak()) + Core.SendToUser(user, "* Current share: "..Core.GetCurrentSharedSize()) + Core.SendToUser(user, "* Uptime: "..Core.GetUpTime().." seconds") + end, + + -- Usage: !catchup-flush + ["!catchup-flush"] = function(user, data, t) + flushCatchup() + Core.SendToUser(user, "* Catchup flushed.") + end, + + -- Usage: !sethubtitle title goes here + ["!sethubtitle"] = function(user, data, t) + SetMan.SetString(0, table.concat(t, " ", 2)) + SetMan.Save() + end, + +} + +function ChatArrival(user, data) + local validcommand=false + + if (string.sub(data, 1, 1) == "<" ) then + data=string.sub(data, 1, string.len(data) -1) + t = {} + for k in string.gmatch(data, "%s+([^%s]+)") do + table.insert(t, k) + end + + if (#t>=1 and triggers[t[1]]) then + triggers[t[1]](user, data, t) + return true + + elseif (#t>=1 and user.iProfile == AdminLevel and adminTriggers[t[1]]) then + adminTriggers[t[1]](user, data, t) + return true + + elseif t[1] and string.sub(t[1], 1, 1) == "!" then + -- Don't add potential system commands to catchup/logs + + else + logCatchup(data:sub(1)) --, -2)) --string.len(data))) + logChat(nmdc_unescape(data:sub(1))) --, -2))) + end + + end + +end + +function ToArrival(user, data) + -- Parse data and determine whether it's intended for any groupchat bots + local recipient = data:gsub("%$To: ([^%s]+) From: [^%s]+ %$<[^>]+> (.*)|", "%1") + local body = data:gsub("%$To: ([^%s]+) From: [^%s]+ %$<[^>]+> (.*)|", "%2") + + -- Core.SendToUser(user, nmdc_escape(data)) + + -- Core.SendToUser(user, "got recipient ["..recipient.."] and body ["..body.."]") + + if groupchats[recipient] then + groupchat_sayas(recipient, user.sNick, body) + return true -- abort + else + return false -- process normally + end +end + +function OnStartup() + -- Create groupchat pruner + groupChatTimer = TmrMan.AddTimer(groupChatPruneCheckEvery * 1000, "groupchat_prune") +end + +-----------------------------------------------------------[ impersonation ] + +function s2(inp) + sp = string.find(inp, " ") + if sp==nil then sp=0 end + return string.sub(inp, 1, sp-1), string.sub(inp, sp+1) +end + +-----------------------------------------------------------[ group chat ] + +function groupchat_init(ownernick) + -- Generate a nick for the bot by adding numbers to the user's nick until + -- one is not currently in use + local suffix = 1 + while true do + -- Core.GetUser doesn't include script bots + if Core.GetUser(ownernick .. suffix) == nil and not groupchats[ownernick..suffix] then + break + end + + suffix = suffix + 1 + if suffix > 100 then + Core.SendToNick(ownernick, "Error: Couldn't pick a group chat username") + return + end + end + local botnick = ownernick..suffix + + -- Register bot + if not Core.RegBot(botnick, "", "", false) then + Core.SendToNick(ownernick, "Error: Couldn't start group chat") + return + end + + -- Save all data + groupchats[botnick] = { + ["owner"] = ownernick, + ["last"] = os.time(), + ["users"] = { + [ownernick] = true + } + } + + -- Refresh usercommands + SC( Core.GetUser(ownernick), false ) + + groupchat_say(botnick, "## Joins: "..ownernick) +end + +-- Owner invites a user into an existing group chat +function groupchat_invite(user, botnick, friendnick) + if groupchats[botnick] and groupchats[botnick]["owner"] == user.sNick then -- only owner can invite + if Core.GetUser(friendnick) then -- Check if user's online + + -- Add user and notify participants + groupchats[botnick]["users"][friendnick] = true + + -- Notify self of existing participants + for peer,_ in pairs(groupchats[botnick]["users"]) do + local isHost = "" + if (groupchats[botnick]["owner"] == peer) then + isHost = " (host)" + end + if peer ~= friendnick then + -- we go last + Core.SendPmToNick(friendnick, botnick, "## Joins: "..peer..isHost) + end + end + + -- Notify existing participants (and self) of joining + groupchat_say(botnick, "## Joins: "..friendnick) -- updates mtime + + else + Core.SendToUser(user, "Error: User not online") + end + else + Core.SendToUser(user, SZ_E_UNAUTHORISED) -- nonexistent or not yours + end +end + +-- Pass user as well, so that administrators can prune groupchat bots. +function groupchat_end(user, botnick) + if groupchats[botnick] then + if groupchats[botnick]["owner"] == user.sNick or user.iProfile == AdminLevel then + -- End chat + groupchat_remove(botnick) + else + -- Not allowed + Core.SendToUser(user, SZ_E_UNAUTHORISED) + end + end +end + +-- Every minute or so, prune group chats that havn't had any replies in a while. +function groupchat_prune() + if #groupchats then + for botnick, gcdata in pairs(groupchats) do + if (os.time() - gcdata["last"]) > groupChatTimeoutAfter then + groupchat_remove(botnick) + end + end + end +end + +-- Explicitly remove a group chat. Only called by other groupchat_ helper +-- functions +function groupchat_remove(botnick) + if groupchats[botnick] then + -- Send leaving message to any still-connected users + groupchat_say(botnick, SZ_GCHAT_CLOSING) + + -- Part the bot + Core.UnregBot(botnick) + + local owner = groupchats[botnick]["owner"] + + -- Remove from groupchat registry + groupchats[botnick] = nil + + -- Refresh usercommands for the host + SC( Core.GetUser(owner), false ) + end +end + +-- Explicitly say a message into a group chat. Only called by other groupchat_ +-- helper functions +function groupchat_say(botnick, message) + if groupchats[botnick] then + for destnick, _ in pairs(groupchats[botnick]["users"]) do + Core.SendPmToNick( destnick, botnick, message ) + end + groupchats[botnick]["last"] = os.time() + end +end + +-- Say a message from a user (i.e. don't echo it back to them) +function groupchat_sayas(botnick, selfnick, message) + if groupchats[botnick] then + local valid = false; + for destnick, _ in pairs(groupchats[botnick]["users"]) do + if destnick == selfnick then + valid = true + break + end + end + if valid then + for destnick, _ in pairs(groupchats[botnick]["users"]) do + if destnick ~= selfnick then + Core.SendPmToNick(destnick, botnick, "<"..selfnick.."> "..message) + end + end + else + Core.SendPmToNick(selfnick, botnick, SZ_GCHAT_AWAYMSG) -- pretend we're not here + end + end +end + +-----------------------------------------------------------[ search parsing ] + +function SearchArrival(user, data) + if bBlockIllegalSearches then + sstr=data + while string.find(sstr, "?") do + sstr=string.sub(sstr, string.find(sstr,"?")+1) + end + sstr="$"..string.lower( string.sub(sstr,1,string.len(sstr)-1) ) + + t = {} + for k in string.gmatch(sstr, "%$([^%$]+)") do + table.insert(t, k) + end + sstr=table.concat(t, " ") + + if string.sub(sstr, 1, 4) ~= "tth:" then + local showbanner=false + for cnt=1,#badwords do + if string.find(sstr, badwords[cnt]) then + showbanner=true + break + end + end + if showbanner then + Core.SendToAll("<"..Core.GetHubSecAlias().."> "..user.sNick.." just searched for '"..sstr.."'!") + return true -- disable the search + end + end + end +end + +-----------------------------------------------------------[ location ] + +function GetHallFromIP(userNick) + if Core.GetUser(userNick) == nil then + return "* User "..userNick.." doesn't have a location." + else + -- Dumb check of third digit in IP + subnet = string.gsub(Core.GetUser(userNick).sIP, "%d+%D%d+%D(%d+)%D%d+", "%1") + subnet = math.floor(subnet / 8) + if hall[subnet]==nil then + return "* User "..userNick.." is located somewhere unknown" + else + return "* User "..userNick.." is located at "..hall[subnet] + end + end +end + +-----------------------------------------------------------[ catchup ] + +function SendCatchup(user) + local sz = getRange(ml, isz(mp), il)..getRange(ml, 0, isz(mp)) + Core.SendToNick(user.sNick, "* Showing up to "..il.." messages.."..sz) + Core.SendToNick(user.sNick, "* Catchup completed.\n") +end + +function isz(n) return (n + 1 + il) % il end + +function logCatchup(text) + mp = isz(mp) + ml[mp] = os.date(tf).." "..text +end + +function flushCatchup() + ml = {""} + mp = -1 +end + +-----------------------------------------------------------[ chatlogs ] + +function logChat(text) + local logfile = os.date(chatLogPath) + local f = io.open(logfile, "a+") + f:write("["..os.date(chatLogTimeFormat).."] "..text.."\n") + f:close() +end + +-----------------------------------------------------------[ general ] + +function onError(sErrMsg) + Core.SendPmToOps(Core.GetHubSecAlias, "The hub script has broken down!\n"..sErrMsg) +end + +function AddUSC(user, mode, menu, cstr) + Core.SendToUser(user, "$UserCommand "..mode.." "..menu.."$<%[mynick]> "..cstr.."|") +end + +-- @return nil|string +function readFile(path) + local f = io.open(path, "rb") + if (f) then + f:close() + + lines = {} + for line in io.lines(path) do + lines[#lines + 1] = line + end + return table.concat(lines, "\n") + end + return nil +end + +-- @return string +function getRange(array, first, last) + local c = first + local s = "" + while not (c >= last or array[c] == nil) do + s = s.."\n"..array[c] + c = c + 1 + end + return s +end + +function nmdc_escape(message) + local r = message:gsub("&", "&"):gsub("|", "|"):gsub("%$", "$") -- escape $ sign + return r -- otherwise there are two arguments +end + +function nmdc_unescape(message) + local r = message:gsub("$", "$"):gsub("|", "|"):gsub("&", "&") + return r +end diff --git a/doc/mcs20_redacted.jpg b/doc/mcs20_redacted.jpg new file mode 100644 index 0000000..dfab932 Binary files /dev/null and b/doc/mcs20_redacted.jpg differ