--[[ _ _ /~` _ _ _ _ | _ _|_ _ (~ _ _. _ _|_ | | |\_,(_)| | ||_)|(/_ | (/__)(_| ||_) | 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