Advertisement
nonogamer9

OC-IRC: An IRC Client For OpenComputers

Oct 3rd, 2024 (edited)
121
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 17.58 KB | Software | 0 0
  1. local component = require("component")
  2. local term = require("term")
  3. local event = require("event")
  4. local keyboard = require("keyboard")
  5. local gpu = component.gpu
  6. local internet = component.internet
  7. local filesystem = require("filesystem")
  8. local computer = require("computer")
  9.  
  10. local function getGPUTier()
  11.   local w, h = gpu.maxResolution()
  12.   if w == 50 and h == 16 then return 1
  13.   elseif w == 80 and h == 25 then return 2
  14.   else return 3 end
  15. end
  16.  
  17. local isColorSupported = false
  18. if gpu and gpu.maxDepth then isColorSupported = gpu.maxDepth() > 1 end
  19.  
  20. local servers = {
  21.   {name = "Libera.Chat", address = "irc.libera.chat", port = 6667},
  22.   {name = "OFTC", address = "irc.oftc.net", port = 6667},
  23.   {name = "EFnet", address = "irc.efnet.org", port = 6667},
  24. }
  25.  
  26. local selectedServerIndex = 1
  27. local username = "User"
  28. local socket = nil
  29. local currentChannel = nil
  30. local messageBuffer = {}
  31. local maxBufferSize = 49
  32. local currentBufferIndex = 1
  33. local userList = {}
  34.  
  35. local function saveServers()
  36.   local file = io.open("/home/oc_irc_servers.txt", "w")
  37.   if file then
  38.     for _, server in ipairs(servers) do
  39.       file:write(string.format("%s,%s,%d\n", server.name, server.address, server.port))
  40.     end
  41.     file:close()
  42.   end
  43. end
  44.  
  45. local function loadServers()
  46.   local file = io.open("/home/oc_irc_servers.txt", "r")
  47.   if file then
  48.     servers = {}
  49.     for line in file:lines() do
  50.       local name, address, port = line:match("([^,]+),([^,]+),(%d+)")
  51.       if name and address and port then
  52.         table.insert(servers, {name = name, address = address, port = tonumber(port)})
  53.       end
  54.     end
  55.     file:close()
  56.   end
  57. end
  58.  
  59. local function saveUsername()
  60.   local file = io.open("/home/oc_irc_username.txt", "w")
  61.   if file then file:write(username) file:close() end
  62. end
  63.  
  64. local function loadUsername()
  65.   local file = io.open("/home/oc_irc_username.txt", "r")
  66.   if file then username = file:read("*all") file:close() end
  67. end
  68.  
  69. loadServers()
  70. loadUsername()
  71.  
  72. local function drawLogo()
  73.   if getGPUTier() == 1 then return end
  74.   local logo = {
  75.     " ________  ________                ___  ________  ________     ",
  76.     "|\\   __  \\|\\   ____\\              |\\  \\|\\   __  \\|\\   ____\\    ",
  77.     "\\ \\  \\|\\  \\ \\  \\___|  ____________\\ \\  \\ \\  \\|\\  \\ \\  \\___|    ",
  78.     " \\ \\  \\\\\\  \\ \\  \\    |\\____________\\ \\  \\ \\   _  _\\ \\  \\       ",
  79.     "  \\ \\  \\\\\\  \\ \\  \\___\\|____________|\\ \\  \\ \\  \\\\  \\\\ \\  \\____  ",
  80.     "   \\ \\_______\\ \\_______\\             \\ \\__\\ \\__\\\\ _\\\\ \\_______\\",
  81.     "    \\|_______|\\|_______|              \\|__|\\|__|\\|__|\\|_______|"
  82.   }
  83.   local width, height = gpu.getResolution()
  84.   local startY = height - #logo - 1
  85.   for i, line in ipairs(logo) do
  86.     gpu.set(1, startY + i, line)
  87.   end
  88. end
  89.  
  90. local function drawServerList()
  91.   local oldForeground = gpu.getForeground()
  92.   local oldBackground = gpu.getBackground()
  93.   gpu.setBackground(0x000000)
  94.   term.clear()
  95.   for i, server in ipairs(servers) do
  96.     if i == selectedServerIndex then
  97.       if getGPUTier() == 1 then
  98.         gpu.set(1, i, "-> " .. string.format("%d. %s (%s:%d)", i, server.name, server.address, server.port))
  99.       else
  100.         gpu.setForeground(0x00FF00)
  101.         gpu.set(1, i, string.format("%d. %s (%s:%d)", i, server.name, server.address, server.port))
  102.         gpu.setForeground(0xFFFFFF)
  103.       end
  104.     else
  105.       gpu.set(1, i, string.format("%d. %s (%s:%d)", i, server.name, server.address, server.port))
  106.     end
  107.   end
  108.   gpu.setForeground(0xFFFFFF)
  109.   gpu.set(1, #servers + 2, "Press N to change username. Current username: " .. username)
  110.   gpu.set(1, #servers + 3, "Press A to add a custom server, R to remove a server")
  111.   gpu.set(1, #servers + 4, "Use arrow keys to navigate, Enter to connect, X to exit")
  112.   drawLogo()
  113.   gpu.setForeground(oldForeground)
  114.   gpu.setBackground(oldBackground)
  115. end
  116.  
  117. local function changeNickname()
  118.   term.write("\nEnter new nickname: ")
  119.   local newNick = term.read():gsub("\n", "")
  120.   if socket then socket.write("NICK " .. newNick .. "\r\n") end
  121.   username = newNick
  122.   saveUsername()
  123.   return newNick
  124. end
  125.  
  126. local function addCustomServer()
  127.   term.clear()
  128.   print("Adding a custom server:")
  129.   term.write("Enter server name: ")
  130.   local name = term.read():gsub("\n", "")
  131.   term.write("Enter server address: ")
  132.   local address = term.read():gsub("\n", "")
  133.   term.write("Enter server port: ")
  134.   local portInput = term.read():gsub("\n", "")
  135.   local port = tonumber(portInput)
  136.   if not port or port < 1 or port > 65535 then
  137.     print("Invalid port number. Please enter a number between 1 and 65535.")
  138.     os.sleep(2)
  139.     return
  140.   end
  141.  
  142.   if name and address and port then
  143.     table.insert(servers, {name = name, address = address, port = port})
  144.     saveServers()
  145.     print("Custom server added successfully!")
  146.   else
  147.     print("Invalid input. Server not added.")
  148.   end
  149.   os.sleep(2)
  150. end
  151.  
  152. local function removeServer()
  153.   term.clear()
  154.   print("Removing a server:")
  155.   for i, server in ipairs(servers) do
  156.     print(string.format("%d. %s (%s:%d)", i, server.name, server.address, server.port))
  157.   end
  158.   term.write("Enter the number of the server to remove: ")
  159.   local input = term.read():gsub("\n", "")
  160.   local index = tonumber(input)
  161.   if index and servers[index] then
  162.     table.remove(servers, index)
  163.     saveServers()
  164.     print("Server removed successfully!")
  165.   else
  166.     print("Invalid input. No server removed.")
  167.   end
  168.   os.sleep(2)
  169. end
  170.  
  171. local function readWithTimeout(socket, timeout)
  172.   local buffer = ""
  173.   local endTime = computer.uptime() + timeout
  174.   while computer.uptime() < endTime do
  175.     local chunk = socket.read(1024)
  176.     if chunk then
  177.       buffer = buffer .. chunk
  178.       if buffer:find("\n") then return buffer end
  179.     end
  180.     os.sleep(0.05)
  181.   end
  182.   return buffer ~= "" and buffer or nil
  183. end
  184.  
  185. local function addMessage(message)
  186.   table.insert(messageBuffer, message)
  187.   if #messageBuffer > maxBufferSize then table.remove(messageBuffer, 1) end
  188.   currentBufferIndex = #messageBuffer
  189. end
  190.  
  191. local function updateUserList(channel, users)
  192.   if not userList[channel] then userList[channel] = {} end
  193.   userList[channel] = {}
  194.   for user in users:gmatch("%S+") do table.insert(userList[channel], user) end
  195. end
  196.  
  197. local function drawModernUI()
  198.   local width, height = gpu.getResolution()
  199.   local userListWidth = 20
  200.   local messageWidth = width - userListWidth - 1
  201.   local inputHeight = 3
  202.   local messageHeight = height - inputHeight - 1
  203.  
  204.   gpu.setBackground(0x000000)
  205.   gpu.fill(1, 1, width, height, " ")
  206.  
  207.   gpu.setBackground(0x1E1E1E)
  208.   gpu.fill(messageWidth + 2, 1, userListWidth, height, " ")
  209.   gpu.setForeground(0xFFFFFF)
  210.   gpu.set(messageWidth + 2, 1, "Users")
  211.   if currentChannel and userList[currentChannel] then
  212.     for i, user in ipairs(userList[currentChannel]) do
  213.       if i <= height - 2 then
  214.         gpu.set(messageWidth + 2, i + 1, user:sub(1, userListWidth - 1))
  215.       else break end
  216.     end
  217.   end
  218.  
  219.   gpu.setBackground(0x000000)
  220.   gpu.fill(1, 1, messageWidth, messageHeight, " ")
  221.  
  222.   gpu.setBackground(0x2D2D2D)
  223.   gpu.fill(1, height - inputHeight + 1, width, inputHeight, " ")
  224.   gpu.setForeground(0xFFFFFF)
  225.   gpu.set(1, height - inputHeight + 1, "Input:")
  226.  
  227.   gpu.setBackground(0x4A4A4A)
  228.   gpu.fill(1, messageHeight + 1, width, 1, "─")
  229.   gpu.fill(messageWidth + 1, 1, 1, height, "│")
  230.   gpu.set(messageWidth + 1, messageHeight + 1, "┴")
  231.  
  232.   gpu.setBackground(0x000000)
  233.   gpu.setForeground(0xFFFFFF)
  234. end
  235.  
  236. local function wrapText(text, width)
  237.   local lines = {}
  238.   local line = ""
  239.   for word in text:gmatch("%S+") do
  240.     if #line + #word + 1 > width then
  241.       table.insert(lines, line)
  242.       line = word
  243.     else
  244.       if #line > 0 then line = line .. " " .. word
  245.       else line = word end
  246.     end
  247.   end
  248.   if #line > 0 then table.insert(lines, line) end
  249.   return lines
  250. end
  251.  
  252. local function displayBuffer()
  253.   local width, height = gpu.getResolution()
  254.   if getGPUTier() == 3 then
  255.     local userListWidth = 20
  256.     local messageWidth = width - userListWidth - 1
  257.     local inputHeight = 3
  258.     local messageHeight = height - inputHeight - 1
  259.  
  260.     gpu.setBackground(0x000000)
  261.     gpu.fill(1, 1, messageWidth, messageHeight, " ")
  262.  
  263.     local startIndex = math.max(1, #messageBuffer - messageHeight + 1)
  264.     for i = 1, messageHeight do
  265.       local message = messageBuffer[startIndex + i - 1]
  266.       if message then
  267.         gpu.set(1, i, message:sub(1, messageWidth))
  268.       end
  269.     end
  270.   else
  271.     term.clear()
  272.     local startIndex = math.max(1, currentBufferIndex - maxBufferSize + 1)
  273.     for i = startIndex, currentBufferIndex do print(messageBuffer[i] or "") end
  274.   end
  275. end
  276.  
  277. local function connectToServer(server)
  278.   term.clear()
  279.   addMessage("Connecting to " .. server.address .. " as " .. username)
  280.   displayBuffer()
  281.   socket = internet.connect(server.address, server.port)
  282.   if not socket then
  283.     addMessage("Failed to connect to " .. server.address)
  284.     displayBuffer()
  285.     return nil
  286.   end
  287.   local timeout = 10
  288.   local start = computer.uptime()
  289.   while not socket.finishConnect() do
  290.     if computer.uptime() - start > timeout then
  291.       addMessage("Connection timed out")
  292.       displayBuffer()
  293.       return nil
  294.     end
  295.     os.sleep(0.1)
  296.   end
  297.   socket.write("NICK " .. username .. "\r\n")
  298.   socket.write("USER " .. username .. " 0 * :" .. username .. "\r\n")
  299.   local connected = false
  300.   local timeout = 30
  301.   local start = computer.uptime()
  302.   while computer.uptime() - start < timeout do
  303.     local response = readWithTimeout(socket, 1)
  304.     if response then
  305.       for line in response:gmatch("[^\r\n]+") do
  306.         addMessage(line)
  307.         if line:find("433") then
  308.           addMessage("Nickname is already in use. Please choose a different nickname.")
  309.           displayBuffer()
  310.           username = changeNickname()
  311.           socket.write("NICK " .. username .. "\r\n")
  312.         elseif line:find("001") then
  313.           addMessage("Successfully connected to " .. server.address .. " as " .. username)
  314.           connected = true
  315.           break
  316.         end
  317.       end
  318.       displayBuffer()
  319.     end
  320.     if connected then break end
  321.   end
  322.   if not connected then
  323.     addMessage("Connection timed out or failed")
  324.     displayBuffer()
  325.     socket.close()
  326.     return nil
  327.   end
  328.   return socket
  329. end
  330.  
  331. local function disconnectFromServer()
  332.   if socket then
  333.     socket.write("QUIT :Disconnecting\r\n")
  334.     socket.close()
  335.     socket = nil
  336.     currentChannel = nil
  337.     addMessage("Disconnected from server.")
  338.     displayBuffer()
  339.     return true
  340.   end
  341.   return false
  342. end
  343.  
  344. local function exitChannel()
  345.   if currentChannel then
  346.     socket.write("PART " .. currentChannel .. " :Leaving\r\n")
  347.     addMessage("Left channel: " .. currentChannel)
  348.     currentChannel = nil
  349.     displayBuffer()
  350.     return true
  351.   end
  352.   return false
  353. end
  354.  
  355. local function ping()
  356.   if not socket then
  357.     addMessage("Not connected to a server.")
  358.     return
  359.   end
  360.   local start = computer.uptime()
  361.   socket.write("PING :ocping\r\n")
  362.   local timeout = computer.uptime() + 5
  363.   while computer.uptime() < timeout do
  364.     local response = readWithTimeout(socket, 0.1)
  365.     if response and response:find("PONG :ocping") then
  366.       local pingTime = (computer.uptime() - start) * 1000
  367.       addMessage(string.format("Server ping: %.2f ms", pingTime))
  368.       return
  369.     end
  370.   end
  371.   addMessage("Ping timed out.")
  372. end
  373.  
  374. local function showHelp()
  375.   addMessage("Available commands:")
  376.   addMessage(":helpoc - Show this help message")
  377.   addMessage("connect #channel - Join a channel")
  378.   addMessage("exit - Leave the current channel")
  379.   addMessage("disconnect - Disconnect from the server")
  380.   addMessage("ping - Ping the server and current channel")
  381.   displayBuffer()
  382. end
  383.  
  384. local function getTextInput(prompt, y)
  385.   local width, _ = gpu.getResolution()
  386.   local input = ""
  387.   local cursorPos = 1
  388.  
  389.   local function redrawInput()
  390.     gpu.fill(1, y, width, 1, " ")
  391.     gpu.set(1, y, prompt .. input)
  392.     gpu.set(#prompt + cursorPos, y, "_")
  393.   end
  394.  
  395.   redrawInput()
  396.  
  397.   while true do
  398.     local _, _, char, code = event.pull("key_down")
  399.     if code == 28 then return input
  400.     elseif code == 14 then
  401.       if cursorPos > 1 then
  402.         input = input:sub(1, cursorPos - 2) .. input:sub(cursorPos)
  403.         cursorPos = cursorPos - 1
  404.       end
  405.     elseif code == 203 then cursorPos = math.max(1, cursorPos - 1)
  406.     elseif code == 205 then cursorPos = math.min(#input + 1, cursorPos + 1)
  407.     elseif char >= 32 and char <= 126 then
  408.       input = input:sub(1, cursorPos - 1) .. string.char(char) .. input:sub(cursorPos)
  409.       cursorPos = cursorPos + 1
  410.     end
  411.     redrawInput()
  412.   end
  413. end
  414.  
  415. local function handleUserInput(isCommand)
  416.   local prefix = isCommand and "Command" or "Chat"
  417.   local input
  418.   if getGPUTier() == 3 then
  419.     local width, height = gpu.getResolution()
  420.     input = getTextInput(prefix .. "> ", height - 1)
  421.   else
  422.     term.write("\n" .. prefix .. "> ")
  423.     input = term.read():gsub("\n", "")
  424.   end
  425.  
  426.   if input then
  427.     if isCommand then
  428.       if input:match("^connect #") then
  429.         currentChannel = input:match("^connect (.+)$")
  430.         socket.write("JOIN " .. currentChannel .. "\r\n")
  431.       elseif input == "disconnect" then
  432.         return disconnectFromServer()
  433.       elseif input == "exit" then
  434.         return exitChannel()
  435.       elseif input == ":helpoc" then
  436.         showHelp()
  437.       elseif input == "ping" then
  438.         ping()
  439.       else
  440.         socket.write(input .. "\r\n")
  441.       end
  442.     else
  443.       if currentChannel then
  444.         socket.write("PRIVMSG " .. currentChannel .. " :" .. input .. "\r\n")
  445.         addMessage("<" .. username .. "> " .. input)
  446.       else
  447.         addMessage("Not connected to a channel. Use 'connect #channel' to join one.")
  448.       end
  449.     end
  450.     displayBuffer()
  451.   end
  452.   return false
  453. end
  454.  
  455. local function handleServerCommunication()
  456.   if getGPUTier() == 3 then drawModernUI() end
  457.   addMessage("Connected to server. Press Ctrl for chat, Alt for commands.")
  458.   addMessage("Use 'connect #channel' to join a channel, 'exit' to leave a channel, and 'disconnect' to disconnect from the server.")
  459.   addMessage("Use 'ping' to ping the server and current channel.")
  460.   addMessage("Use Up/Down arrows or PageUp/PageDown to scroll through messages.")
  461.   displayBuffer()
  462.  
  463.   local buffer = ""
  464.   while true do
  465.     local chunk = socket.read(1024)
  466.     if chunk then
  467.       buffer = buffer .. chunk
  468.       local lines = {}
  469.       buffer = buffer:gsub("(.-)\r\n", function(line)
  470.         table.insert(lines, line)
  471.         return ""
  472.       end)
  473.      
  474.       for _, line in ipairs(lines) do
  475.         if line:find("^PING") then
  476.           local pingId = line:match("^PING :(.+)")
  477.           socket.write("PONG :" .. pingId .. "\r\n")
  478.         elseif line:match("PRIVMSG .* :\001PING ") then
  479.           local user = line:match("^:([^!]+)")
  480.           local pingTime = line:match("PRIVMSG .* :\001PING (.+)\001")
  481.           socket.write("NOTICE " .. user .. " :\001PING " .. pingTime .. "\001\r\n")
  482.         elseif line:match("^:.+ 353 .+ = #%S+ :.*$") then
  483.           local channel, users = line:match("^:.+ 353 .+ = (#%S+) :(.*)$")
  484.           updateUserList(channel, users)
  485.           if getGPUTier() == 3 then drawModernUI() end
  486.         else
  487.           addMessage(line)
  488.         end
  489.       end
  490.       displayBuffer()
  491.     end
  492.    
  493.     local eventName, _, char, code = event.pull(0.05)
  494.     if eventName == "key_down" then
  495.       if code == 29 then
  496.         if handleUserInput(false) then return end
  497.       elseif code == 56 then
  498.         if handleUserInput(true) then return end
  499.       elseif code == 200 then
  500.         currentBufferIndex = math.max(1, currentBufferIndex - 1)
  501.         displayBuffer()
  502.       elseif code == 208 then
  503.         currentBufferIndex = math.min(#messageBuffer, currentBufferIndex + 1)
  504.         displayBuffer()
  505.       elseif code == 201 then
  506.         currentBufferIndex = math.max(1, currentBufferIndex - maxBufferSize)
  507.         displayBuffer()
  508.       elseif code == 209 then
  509.         currentBufferIndex = math.min(#messageBuffer, currentBufferIndex + maxBufferSize)
  510.         displayBuffer()
  511.       end
  512.     end
  513.   end
  514. end
  515.  
  516. local function updateMaxBufferSize()
  517.   local _, height = gpu.getResolution()
  518.   if getGPUTier() == 3 then
  519.     maxBufferSize = height - 4
  520.   else
  521.     maxBufferSize = 49
  522.   end
  523. end
  524.  
  525. local function main()
  526.   while true do
  527.     drawServerList()
  528.     local eventName, _, char, code = event.pull("key_down")
  529.     if char == 120 or char == 88 then
  530.       print("Exiting OC-IRC...")
  531.       os.exit()
  532.     elseif code == 200 then
  533.       selectedServerIndex = math.max(1, selectedServerIndex - 1)
  534.     elseif code == 208 then
  535.       selectedServerIndex = math.min(#servers, selectedServerIndex + 1)
  536.     elseif code == 203 then
  537.       selectedServerIndex = math.max(1, selectedServerIndex - 1)
  538.     elseif code == 205 then
  539.       selectedServerIndex = math.min(#servers, selectedServerIndex + 1)
  540.     elseif char == 110 or char == 78 then
  541.       username = changeNickname()
  542.     elseif char == 97 or char == 65 then
  543.       addCustomServer()
  544.     elseif char == 114 or char == 82 then
  545.       removeServer()
  546.     elseif code == 28 then
  547.       term.clear()
  548.       socket = connectToServer(servers[selectedServerIndex])
  549.       if not socket then
  550.         addMessage("Connection failed. Press any key to return to server list.")
  551.         displayBuffer()
  552.         event.pull("key_down")
  553.       else
  554.         if isColorSupported then
  555.           gpu.setResolution(gpu.maxResolution())
  556.           gpu.setBackground(0x000000)
  557.           gpu.setForeground(0xFFFFFF)
  558.         end
  559.         updateMaxBufferSize()
  560.         handleServerCommunication()
  561.       end
  562.     end
  563.   end
  564. end
  565.  
  566. main()
  567.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement