-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe -- -- SPDX-License-Identifier: LicenseRef-CCPL local tArgs = { ... } local function printUsage() local programName = arg[0] or fs.getName(shell.getRunningProgram()) print("Usages:") print(programName .. " host ") print(programName .. " join ") end local sOpenedModem = nil local function openModem() for _, sModem in ipairs(peripheral.getNames()) do if peripheral.getType(sModem) == "modem" then if not rednet.isOpen(sModem) then rednet.open(sModem) sOpenedModem = sModem end return true end end print("No modems found.") return false end local function closeModem() if sOpenedModem ~= nil then rednet.close(sOpenedModem) sOpenedModem = nil end end -- Colours local highlightColour, textColour if term.isColour() then textColour = colours.white highlightColour = colours.yellow else textColour = colours.white highlightColour = colours.white end local sCommand = tArgs[1] if sCommand == "host" then -- "chat host" -- Get hostname local sHostname = tArgs[2] if sHostname == nil then printUsage() return end -- Host server if not openModem() then return end rednet.host("chat", sHostname) print("0 users connected.") local tUsers = {} local nUsers = 0 local function send(sText, nUserID) if nUserID then local tUser = tUsers[nUserID] if tUser then rednet.send(tUser.nID, { sType = "text", nUserID = nUserID, sText = sText, }, "chat") end else for nUserID, tUser in pairs(tUsers) do rednet.send(tUser.nID, { sType = "text", nUserID = nUserID, sText = sText, }, "chat") end end end -- Setup ping pong local tPingPongTimer = {} local function ping(nUserID) local tUser = tUsers[nUserID] rednet.send(tUser.nID, { sType = "ping to client", nUserID = nUserID, }, "chat") local timer = os.startTimer(15) tUser.bPingPonged = false tPingPongTimer[timer] = nUserID end local function printUsers() local _, y = term.getCursorPos() term.setCursorPos(1, y - 1) term.clearLine() if nUsers == 1 then print(nUsers .. " user connected.") else print(nUsers .. " users connected.") end end -- Handle messages local ok, error = pcall(parallel.waitForAny, function() while true do local _, timer = os.pullEvent("timer") local nUserID = tPingPongTimer[timer] if nUserID and tUsers[nUserID] then local tUser = tUsers[nUserID] if tUser then if not tUser.bPingPonged then send("* " .. tUser.sUsername .. " has timed out") tUsers[nUserID] = nil nUsers = nUsers - 1 printUsers() else ping(nUserID) end end end end end, function() while true do local tCommands tCommands = { ["me"] = function(tUser, sContent) if #sContent > 0 then send("* " .. tUser.sUsername .. " " .. sContent) else send("* Usage: /me [words]", tUser.nUserID) end end, ["nick"] = function(tUser, sContent) if #sContent > 0 then local sOldName = tUser.sUsername tUser.sUsername = sContent send("* " .. sOldName .. " is now known as " .. tUser.sUsername) else send("* Usage: /nick [nickname]", tUser.nUserID) end end, ["users"] = function(tUser, sContent) send("* Connected Users:", tUser.nUserID) local sUsers = "*" for _, tUser in pairs(tUsers) do sUsers = sUsers .. " " .. tUser.sUsername end send(sUsers, tUser.nUserID) end, ["help"] = function(tUser, sContent) send("* Available commands:", tUser.nUserID) local sCommands = "*" for sCommand in pairs(tCommands) do sCommands = sCommands .. " /" .. sCommand end send(sCommands .. " /logout", tUser.nUserID) end, } local nSenderID, tMessage = rednet.receive("chat") if type(tMessage) == "table" then if tMessage.sType == "login" then -- Login from new client local nUserID = tMessage.nUserID local sUsername = tMessage.sUsername if nUserID and sUsername then tUsers[nUserID] = { nID = nSenderID, nUserID = nUserID, sUsername = sUsername, } nUsers = nUsers + 1 printUsers() send("* " .. sUsername .. " has joined the chat") ping(nUserID) end else -- Something else from existing client local nUserID = tMessage.nUserID local tUser = tUsers[nUserID] if tUser and tUser.nID == nSenderID then if tMessage.sType == "logout" then send("* " .. tUser.sUsername .. " has left the chat") tUsers[nUserID] = nil nUsers = nUsers - 1 printUsers() elseif tMessage.sType == "chat" then local sMessage = tMessage.sText if sMessage then local sCommand = string.match(sMessage, "^/([a-z]+)") if sCommand then local fnCommand = tCommands[sCommand] if fnCommand then local sContent = string.sub(sMessage, #sCommand + 3) fnCommand(tUser, sContent) else send("* Unrecognised command: /" .. sCommand, tUser.nUserID) end else send("<" .. tUser.sUsername .. "> " .. tMessage.sText) end end elseif tMessage.sType == "ping to server" then rednet.send(tUser.nID, { sType = "pong to client", nUserID = nUserID, }, "chat") elseif tMessage.sType == "pong to server" then tUser.bPingPonged = true end end end end end end ) if not ok then printError(error) end -- Unhost server for nUserID, tUser in pairs(tUsers) do rednet.send(tUser.nID, { sType = "kick", nUserID = nUserID, }, "chat") end rednet.unhost("chat") closeModem() elseif sCommand == "join" then -- "chat join" -- Get hostname and username local sHostname = tArgs[2] local sUsername = tArgs[3] if sHostname == nil or sUsername == nil then printUsage() return end -- Connect if not openModem() then return end write("Looking up " .. sHostname .. "... ") local nHostID = rednet.lookup("chat", sHostname) if nHostID == nil then print("Failed.") return else print("Success.") end -- Login local nUserID = math.random(1, 2147483647) rednet.send(nHostID, { sType = "login", nUserID = nUserID, sUsername = sUsername, }, "chat") -- Setup ping pong local bPingPonged = true local pingPongTimer = os.startTimer(0) local function ping() rednet.send(nHostID, { sType = "ping to server", nUserID = nUserID, }, "chat") bPingPonged = false pingPongTimer = os.startTimer(15) end -- Handle messages local w, h = term.getSize() local parentTerm = term.current() local titleWindow = window.create(parentTerm, 1, 1, w, 1, true) local historyWindow = window.create(parentTerm, 1, 2, w, h - 2, true) local promptWindow = window.create(parentTerm, 1, h, w, 1, true) historyWindow.setCursorPos(1, h - 2) term.clear() term.setTextColour(textColour) term.redirect(promptWindow) promptWindow.restoreCursor() local function drawTitle() local w = titleWindow.getSize() local sTitle = sUsername .. " on " .. sHostname titleWindow.setTextColour(highlightColour) titleWindow.setCursorPos(math.floor(w / 2 - #sTitle / 2), 1) titleWindow.clearLine() titleWindow.write(sTitle) promptWindow.restoreCursor() end local function printMessage(sMessage) term.redirect(historyWindow) print() if string.match(sMessage, "^%*") then -- Information term.setTextColour(highlightColour) write(sMessage) term.setTextColour(textColour) else -- Chat local sUsernameBit = string.match(sMessage, "^<[^>]*>") if sUsernameBit then term.setTextColour(highlightColour) write(sUsernameBit) term.setTextColour(textColour) write(string.sub(sMessage, #sUsernameBit + 1)) else write(sMessage) end end term.redirect(promptWindow) promptWindow.restoreCursor() end drawTitle() local ok, error = pcall(parallel.waitForAny, function() while true do local sEvent, timer = os.pullEvent() if sEvent == "timer" then if timer == pingPongTimer then if not bPingPonged then printMessage("Server timeout.") return else ping() end end elseif sEvent == "term_resize" then local w, h = parentTerm.getSize() titleWindow.reposition(1, 1, w, 1) historyWindow.reposition(1, 2, w, h - 2) promptWindow.reposition(1, h, w, 1) end end end, function() while true do local nSenderID, tMessage = rednet.receive("chat") if nSenderID == nHostID and type(tMessage) == "table" and tMessage.nUserID == nUserID then if tMessage.sType == "text" then local sText = tMessage.sText if sText then printMessage(sText) end elseif tMessage.sType == "ping to client" then rednet.send(nSenderID, { sType = "pong to server", nUserID = nUserID, }, "chat") elseif tMessage.sType == "pong to client" then bPingPonged = true elseif tMessage.sType == "kick" then return end end end end, function() local tSendHistory = {} while true do promptWindow.setCursorPos(1, 1) promptWindow.clearLine() promptWindow.setTextColor(highlightColour) promptWindow.write(": ") promptWindow.setTextColor(textColour) local sChat = read(nil, tSendHistory) if string.match(sChat, "^/logout") then break else rednet.send(nHostID, { sType = "chat", nUserID = nUserID, sText = sChat, }, "chat") table.insert(tSendHistory, sChat) end end end ) -- Close the windows term.redirect(parentTerm) -- Print error notice local _, h = term.getSize() term.setCursorPos(1, h) term.clearLine() term.setCursorBlink(false) if not ok then printError(error) end -- Logout rednet.send(nHostID, { sType = "logout", nUserID = nUserID, }, "chat") closeModem() -- Print disconnection notice print("Disconnected.") else -- "chat somethingelse" printUsage() end