Advertisement
Guest User

irc.lua

a guest
Jul 24th, 2022
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.96 KB | None | 0 0
  1.  
  2.  
  3. local component = require("component")
  4. local computer = require("computer")
  5.  
  6. if not component.isAvailable("internet") then
  7.   io.stderr:write("OpenIRC requires an Internet Card to run!\n")
  8.   return
  9. end
  10.  
  11. local event = require("event")
  12. local internet = require("internet")
  13. local shell = require("shell")
  14. local term = require("term")
  15. local text = require("text")
  16.  
  17. local args, options = shell.parse(...)
  18. if #args < 1 then
  19.   print("Usage: irc <nickname> [server[:port]]")
  20.   return
  21. end
  22.  
  23. local nick = args[1]
  24. local host = args[2] or "irc.Esper.net:6667"
  25.  
  26. if not host:find(":") then
  27.   host = host .. ":6667"
  28. end
  29.  
  30. -- try to connect to server.
  31. local sock, reason = internet.open(host)
  32. if not sock then
  33.   io.stderr:write(reason .. "\n")
  34.   return
  35. end
  36.  
  37. -- custom print that uses all except the last line for printing.
  38. local function print(message, overwrite)
  39.   local w, h = component.gpu.getResolution()
  40.   local line
  41.   repeat
  42.     line, message = text.wrap(text.trim(message), w, w)
  43.     if not overwrite then
  44.       component.gpu.copy(1, 1, w, h - 1, 0, -1)
  45.     end
  46.     overwrite = false
  47.     component.gpu.fill(1, h - 1, w, 1, " ")
  48.     component.gpu.set(1, h - 1, line)
  49.   until not message or message == ""
  50. end
  51.  
  52. -- utility method for reply tracking tables.
  53. function autocreate(table, key)
  54.   table[key] = {}
  55.   return table[key]
  56. end
  57.  
  58. -- extract nickname from identity.
  59. local function name(identity)
  60.   return identity and identity:match("^[^!]+") or identity or "Anonymous"
  61. end
  62.  
  63. -- user defined callback for messages (via `lua function(msg) ... end`)
  64. local callback = nil
  65.  
  66. -- list of whois info per user (used to accumulate whois replies).
  67. local whois = setmetatable({}, {__index=autocreate})
  68.  
  69. -- list of users per channel (used to accumulate names replies).
  70. local names = setmetatable({}, {__index=autocreate})
  71.  
  72. -- timer used to drive socket reading.
  73. local timer
  74.  
  75. -- ignored commands, reserved according to RFC.
  76. -- http://tools.ietf.org/html/rfc2812#section-5.3
  77. local ignore = {
  78.   [213]=true, [214]=true, [215]=true, [216]=true, [217]=true,
  79.   [218]=true, [231]=true, [232]=true, [233]=true, [240]=true,
  80.   [241]=true, [244]=true, [244]=true, [246]=true, [247]=true,
  81.   [250]=true, [300]=true, [316]=true, [361]=true, [362]=true,
  82.   [363]=true, [373]=true, [384]=true, [492]=true,
  83.   -- custom ignored responses.
  84.   [265]=true, [266]=true, [330]=true
  85. }
  86.  
  87. -- command numbers to names.
  88. local commands = {
  89. --Replys
  90.   RPL_WELCOME = "001",
  91.   RPL_YOURHOST = "002",
  92.   RPL_CREATED = "003",
  93.   RPL_MYINFO = "004",
  94.   RPL_BOUNCE = "005",
  95.   RPL_LUSERCLIENT = "251",
  96.   RPL_LUSEROP = "252",
  97.   RPL_LUSERUNKNOWN = "253",
  98.   RPL_LUSERCHANNELS = "254",
  99.   RPL_LUSERME = "255",
  100.   RPL_AWAY = "301",
  101.   RPL_UNAWAY = "305",
  102.   RPL_NOWAWAY = "306",
  103.   RPL_WHOISUSER = "311",
  104.   RPL_WHOISSERVER = "312",
  105.   RPL_WHOISOPERATOR = "313",
  106.   RPL_WHOISIDLE = "317",
  107.   RPL_ENDOFWHOIS = "318",
  108.   RPL_WHOISCHANNELS = "319",
  109.   RPL_CHANNELMODEIS = "324",
  110.   RPL_NOTOPIC = "331",
  111.   RPL_TOPIC = "332",
  112.   RPL_NAMREPLY = "353",
  113.   RPL_ENDOFNAMES = "366",
  114.   RPL_MOTDSTART = "375",
  115.   RPL_MOTD = "372",
  116.   RPL_ENDOFMOTD = "376",
  117.   RPL_WHOISSECURE = "671",
  118.   RPL_HELPSTART = "704",
  119.   RPL_HELPTXT = "705",
  120.   RPL_ENDOFHELP = "706",
  121.   RPL_UMODEGMSG = "718",
  122.  
  123. --Errors
  124.   ERR_BANLISTFULL = "478",
  125.   ERR_CHANNELISFULL = "471",
  126.   ERR_UNKNOWNMODE = "472",
  127.   ERR_INVITEONLYCHAN = "473",
  128.   ERR_BANNEDFROMCHAN = "474",
  129.   ERR_CHANOPRIVSNEEDED = "482",
  130.   ERR_UNIQOPRIVSNEEDED = "485",
  131.   ERR_USERNOTINCHANNEL = "441",
  132.   ERR_NOTONCHANNEL = "442",
  133.   ERR_NICKCOLLISION = "436",
  134.   ERR_NICKNAMEINUSE = "433",
  135.   ERR_ERRONEUSNICKNAME = "432",
  136.   ERR_WASNOSUCHNICK = "406",
  137.   ERR_TOOMANYCHANNELS = "405",
  138.   ERR_CANNOTSENDTOCHAN = "404",
  139.   ERR_NOSUCHCHANNEL = "403",
  140.   ERR_NOSUCHNICK = "401",
  141.   ERR_MODELOCK = "742"
  142. }
  143.  
  144. -- main command handling callback.
  145. local function handleCommand(prefix, command, args, message)
  146.   ---------------------------------------------------
  147.   -- Keepalive
  148.  
  149.   if command == "PING" then
  150.     sock:write(string.format("PONG :%s\r\n", message))
  151.     sock:flush()
  152.  
  153.   ---------------------------------------------------
  154.   -- General commands
  155.   elseif command == "NICK" then
  156.     local oldNick, newNick = name(prefix), tostring(args[1] or message)
  157.     if oldNick == nick then
  158.       nick = newNick
  159.     end
  160.     print(oldNick .. " is now known as " .. newNick .. ".")
  161.   elseif command == "MODE" then
  162.     if #args == 2 then
  163.       print("[" .. args[1] .. "] " .. name(prefix) .. " set mode".. ( #args[2] > 2 and "s" or "" ) .. " " .. tostring(args[2] or message) .. ".")
  164.     else
  165.       local setmode = {}
  166.       local cumode = "+"
  167.       args[2]:gsub(".", function(char)
  168.         if char == "-" or char == "+" then
  169.           cumode = char
  170.         else
  171.           table.insert(setmode, {cumode, char})
  172.         end
  173.       end)
  174.       local d = {}
  175.       local users = {}
  176.       for i = 3, #args do
  177.         users[i-2] = args[i]
  178.       end
  179.       users[#users+1] = message
  180.       local last
  181.       local ctxt = ""
  182.       for c = 1, #users do
  183.         if not setmode[c] then
  184.           break
  185.         end
  186.         local mode = setmode[c][2]
  187.         local pfx = setmode[c][1]=="+"
  188.         local key = mode == "o" and (pfx and "opped" or "deoped") or
  189.           mode == "v" and (pfx and "voiced" or "devoiced") or
  190.           mode == "q" and (pfx and "quieted" or "unquieted") or
  191.           mode == "b" and (pfx and "banned" or "unbanned") or
  192.           "set " .. setmode[c][1] .. mode .. " on"
  193.         if last ~= key then
  194.           if last then
  195.             print(ctxt)
  196.           end
  197.           ctxt = "[" .. args[1] .. "] " .. name(prefix) .. " " .. key
  198.           last = key
  199.         end
  200.         ctxt = ctxt .. " " .. users[c]
  201.       end
  202.       if #ctxt > 0 then
  203.         print(ctxt)
  204.       end
  205.     end
  206.   elseif command == "QUIT" then
  207.     print(name(prefix) .. " quit (" .. (message or "Quit") .. ").")
  208.   elseif command == "JOIN" then
  209.     print("[" .. args[1] .. "] " .. name(prefix) .. " entered the room.")
  210.   elseif command == "PART" then
  211.     print("[" .. args[1] .. "] " .. name(prefix) .. " has left the room (quit: " .. (message or "Quit") .. ").")
  212.   elseif command == "TOPIC" then
  213.     print("[" .. args[1] .. "] " .. name(prefix) .. " has changed the topic to: " .. message)
  214.   elseif command == "KICK" then
  215.     print("[" .. args[1] .. "] " .. name(prefix) .. " kicked " .. args[2])
  216.   elseif command == "PRIVMSG" then
  217.     local ctcp = message:match("^\1(.-)\1$")
  218.     if ctcp then
  219.       local orig_ctcp, param = ctcp:match("^(%S+) ?(.-)$")
  220.       ctcp = orig_ctcp:upper()
  221.       if ctcp == "TIME" then
  222.         sock:write("NOTICE " .. name(prefix) .. " :\001TIME " .. os.date() .. "\001\r\n")
  223.         sock:flush()
  224.       elseif ctcp == "VERSION" then
  225.         sock:write("NOTICE " .. name(prefix) .. " :\001VERSION Minecraft/OpenComputers Lua 5.2\001\r\n")
  226.         sock:flush()
  227.       elseif ctcp == "PING" then
  228.         sock:write("NOTICE " .. name(prefix) .. " :\001PING " .. param .. "\001\r\n")
  229.         sock:flush()
  230.       elseif ctcp == "ACTION" then
  231.         print("[" .. args[1] .. "] * " .. name(prefix) .. string.gsub(string.gsub(message, "\001ACTION", ""), "\001", ""))
  232.       else
  233.         -- Here we print the CTCP message if it was unhandled...
  234.         print("[" .. name(prefix) .. "] CTCP " .. orig_ctcp)
  235.       end
  236.     else
  237.       if string.find(message, nick) then
  238.         computer.beep()
  239.       end
  240.       print("[" .. args[1] .. "] " .. name(prefix) .. ": " .. message)
  241.     end
  242.   elseif command == "NOTICE" then
  243.     print("[NOTICE] " .. message)
  244.   elseif command == "ERROR" then
  245.     print("[ERROR] " .. message)
  246.  
  247.   ---------------------------------------------------
  248.   -- Ignored reserved numbers
  249.   -- -- http://tools.ietf.org/html/rfc2812#section-5.3
  250.  
  251.   elseif tonumber(command) and ignore[tonumber(command)] then
  252.     -- ignore
  253.  
  254.   ---------------------------------------------------
  255.   -- Command replies
  256.   -- http://tools.ietf.org/html/rfc2812#section-5.1
  257.  
  258.   elseif command == commands.RPL_WELCOME then
  259.     print(message)
  260.   elseif command == commands.RPL_YOURHOST then -- ignore
  261.   elseif command == commands.RPL_CREATED then -- ignore
  262.   elseif command == commands.RPL_MYINFO then -- ignore
  263.   elseif command == commands.RPL_BOUNCE then -- ignore
  264.   elseif command == commands.RPL_LUSERCLIENT then
  265.     print(message)
  266.   elseif command == commands.RPL_LUSEROP then -- ignore
  267.   elseif command == commands.RPL_LUSERUNKNOWN then -- ignore
  268.   elseif command == commands.RPL_LUSERCHANNELS then -- ignore
  269.   elseif command == commands.RPL_LUSERME then
  270.     print(message)
  271.   elseif command == commands.RPL_AWAY then
  272.     print(string.format("%s is away: %s", name(args[1]), message))
  273.   elseif command == commands.RPL_UNAWAY or command == commands.RPL_NOWAWAY then
  274.     print(message)
  275.   elseif command == commands.RPL_WHOISUSER then
  276.     local nick = args[2]:lower()
  277.     whois[nick].nick = args[2]
  278.     whois[nick].user = args[3]
  279.     whois[nick].host = args[4]
  280.     whois[nick].realName = message
  281.   elseif command == commands.RPL_WHOISSERVER then
  282.     local nick = args[2]:lower()
  283.     whois[nick].server = args[3]
  284.     whois[nick].serverInfo = message
  285.   elseif command == commands.RPL_WHOISOPERATOR then
  286.     local nick = args[2]:lower()
  287.     whois[nick].isOperator = true
  288.   elseif command == commands.RPL_WHOISIDLE then
  289.     local nick = args[2]:lower()
  290.     whois[nick].idle = tonumber(args[3])
  291.   elseif command == commands.RPL_WHOISSECURE then
  292.     local nick = args[2]:lower()
  293.     whois[nick].secureconn = "Is using a secure connection"
  294.   elseif command == commands.RPL_ENDOFWHOIS then
  295.     local nick = args[2]:lower()
  296.     local info = whois[nick]
  297.     if info.nick then print("Nick: " .. info.nick) end
  298.     if info.user then print("User name: " .. info.user) end
  299.     if info.realName then print("Real name: " .. info.realName) end
  300.     if info.host then print("Host: " .. info.host) end
  301.     if info.server then print("Server: " .. info.server .. (info.serverInfo and (" (" .. info.serverInfo .. ")") or "")) end
  302.     if info.secureconn then print(info.secureconn) end
  303.     if info.channels then print("Channels: " .. info.channels) end
  304.     if info.idle then print("Idle for: " .. info.idle) end
  305.     whois[nick] = nil
  306.   elseif command == commands.RPL_WHOISCHANNELS then
  307.     local nick = args[2]:lower()
  308.     whois[nick].channels = message
  309.   elseif command == commands.RPL_CHANNELMODEIS then
  310.     print("Channel mode for " .. args[1] .. ": " .. args[2] .. " (" .. args[3] .. ")")
  311.   elseif command == commands.RPL_NOTOPIC then
  312.     print("No topic is set for " .. args[1] .. ".")
  313.   elseif command == commands.RPL_TOPIC then
  314.     print("Topic for " .. args[1] .. ": " .. message)
  315.   elseif command == commands.RPL_NAMREPLY then
  316.     local channel = args[3]
  317.     table.insert(names[channel], message)
  318.   elseif command == commands.RPL_ENDOFNAMES then
  319.     local channel = args[2]
  320.     print("Users on " .. channel .. ": " .. (#names[channel] > 0 and table.concat(names[channel], " ") or "none"))
  321.     names[channel] = nil
  322.   elseif command == commands.RPL_MOTDSTART then
  323.     if options.motd then
  324.       print(message .. args[1])
  325.     end
  326.   elseif command == commands.RPL_MOTD then
  327.     if options.motd then
  328.       print(message)
  329.     end
  330.   elseif command == commands.RPL_ENDOFMOTD then -- ignore
  331.   elseif command == commands.RPL_HELPSTART or
  332.   command == commands.RPL_HELPTXT or
  333.   command == commands.RPL_ENDOFHELP then
  334.     print(message)
  335.   elseif command == commands.ERR_BANLISTFULL or
  336.   command == commands.ERR_BANNEDFROMCHAN or
  337.   command == commands.ERR_CANNOTSENDTOCHAN or
  338.   command == commands.ERR_CHANNELISFULL or
  339.   command == commands.ERR_CHANOPRIVSNEEDED or
  340.   command == commands.ERR_ERRONEUSNICKNAME or
  341.   command == commands.ERR_INVITEONLYCHAN or
  342.   command == commands.ERR_NICKCOLLISION or
  343.   command == commands.ERR_NOSUCHNICK or
  344.   command == commands.ERR_NOTONCHANNEL or
  345.   command == commands.ERR_UNIQOPRIVSNEEDED or
  346.   command == commands.ERR_UNKNOWNMODE or
  347.   command == commands.ERR_USERNOTINCHANNEL or
  348.   command == commands.ERR_WASNOSUCHNICK or
  349.   command == commands.ERR_MODELOCK then
  350.     print("[ERROR]: " .. message)
  351.   elseif tonumber(command) and (tonumber(command) >= 200 and tonumber(command) < 400) then
  352.     print("[Response " .. command .. "] " .. table.concat(args, ", ") .. ": " .. message)
  353.  
  354.   ---------------------------------------------------
  355.   -- Error messages. No real point in handling those manually.
  356.   -- http://tools.ietf.org/html/rfc2812#section-5.2
  357.  
  358.   elseif tonumber(command) and (tonumber(command) >= 400 and tonumber(command) < 600) then
  359.     print("[Error] " .. table.concat(args, ", ") .. ": " .. message)
  360.  
  361.   ---------------------------------------------------
  362.   -- Unhandled message.
  363.  
  364.   else
  365.     print("Unhandled command: " .. command .. ": " .. message)
  366.   end
  367. end
  368.  
  369. -- catch errors to allow manual closing of socket and removal of timer.
  370. local result, reason = pcall(function()
  371.   -- say hello.
  372.   term.clear()
  373.   print("Welcome to UnionICE")
  374.  
  375.   -- avoid sock:read locking up the computer.
  376.   sock:setTimeout(0.05)
  377.  
  378.   -- http://tools.ietf.org/html/rfc2812#section-3.1
  379.   sock:write(string.format("NICK %s\r\n", nick))
  380.   sock:write(string.format("USER %s 0 * :%s [OpenComputers]\r\n", nick:lower(), nick))
  381.   sock:flush()
  382.  
  383.   -- socket reading logic (receive messages) driven by a timer.
  384.   timer = event.timer(0.5, function()
  385.     if not sock then
  386.       return false
  387.     end
  388.     repeat
  389.       local ok, line = pcall(sock.read, sock)
  390.       if ok then
  391.         if not line then
  392.           print("Connection lost.")
  393.           sock:close()
  394.           sock = nil
  395.           return false
  396.         end
  397.         line = text.trim(line) -- get rid of trailing \r
  398.         local match, prefix = line:match("^(:(%S+) )")
  399.         if match then line = line:sub(#match + 1) end
  400.         local match, command = line:match("^(([^:]%S*))")
  401.         if match then line = line:sub(#match + 1) end
  402.         local args = {}
  403.         repeat
  404.           local match, arg = line:match("^( ([^:]%S*))")
  405.           if match then
  406.             line = line:sub(#match + 1)
  407.             table.insert(args, arg)
  408.           end
  409.         until not match
  410.         local message = line:match("^ :(.*)$")
  411.  
  412.         if callback then
  413.           local result, reason = pcall(callback, prefix, command, args, message)
  414.           if not result then
  415.             print("Error in callback: " .. tostring(reason))
  416.           end
  417.         end
  418.         handleCommand(prefix, command, args, message)
  419.       end
  420.     until not ok
  421.   end, math.huge)
  422.  
  423.   -- default target for messages, so we don't have to type /msg all the time.
  424.   local target = nil
  425.  
  426.   -- command history.
  427.   local history = {}
  428.  
  429.   repeat
  430.     local w, h = component.gpu.getResolution()
  431.     term.setCursor(1, h)
  432.     term.write((target or "?") .. "> ")
  433.     local line = term.read(history)
  434.     if sock and line and line ~= "" then
  435.       line = text.trim(line)
  436.       if line:lower():sub(1,4) == "/me " then
  437.         print("[" .. (target or "?") .. "] " .. nick .. " " .. line:sub(5), true)
  438.       elseif line~="" then
  439.         print("[" .. (target or "?") .. "] " .. nick .. ": " .. line, true)
  440.       end
  441.       if line:lower():sub(1, 5) == "/msg " then
  442.         local user, message = line:sub(6):match("^(%S+) (.+)$")
  443.         if message then
  444.           message = text.trim(message)
  445.         end
  446.         if not user or not message or message == "" then
  447.           print("Invalid use of /msg. Usage: /msg nick|channel message.")
  448.           line = ""
  449.         else
  450.           target = user
  451.           line = "PRIVMSG " .. target .. " :" .. message
  452.         end
  453.       elseif line:lower():sub(1, 6) == "/join " then
  454.         local channel = "#cc.ru"
  455.         if not channel or channel == "" then
  456.           print("Invalid use of /join. Usage: /join channel.")
  457.           line = ""
  458.         else
  459.           target = channel
  460.           line = "JOIN " .. "#cc.ru"
  461.         end
  462.       elseif line:lower():sub(1, 5) == "/lua " then
  463.         local script = text.trim(line:sub(6))
  464.         local result, reason = load(script, "=stdin", nil, setmetatable({print=print, socket=sock, nick=nick}, {__index=_G}))
  465.         if not result then
  466.           result, reason = load("return " .. script, "=stdin", nil, setmetatable({print=print, socket=sock, nick=nick}, {__index=_G}))
  467.         end
  468.         line = ""
  469.         if not result then
  470.           print("Error: " .. tostring(reason))
  471.         else
  472.           result, reason = pcall(result)
  473.           if not result then
  474.             print("Error: " .. tostring(reason))
  475.           elseif type(reason) == "function" then
  476.             callback = reason
  477.           elseif reason then
  478.             line = tostring(reason)
  479.           end
  480.         end
  481.       elseif line:lower():sub(1,4) == "/me " then
  482.         if not target then
  483.           print("No default target set. Use /msg or /join to set one.")
  484.           line = ""
  485.         else
  486.           line = "PRIVMSG " .. target .. " :\001ACTION " .. line:sub(5) .. "\001"
  487.         end
  488.       elseif line:sub(1, 1) == "/" then
  489.         line = line:sub(2)
  490.       elseif line ~= "" then
  491.         if not target then
  492.           print("No default target set. Use /msg or /join to set one.")
  493.           line = ""
  494.         else
  495.           line = "PRIVMSG " .. target .. " :" .. line
  496.         end
  497.       end
  498.       if line and line ~= "" then
  499.         sock:write(line .. "\r\n")
  500.         sock:flush()
  501.       end
  502.     end
  503.   until not sock or not line
  504. end)
  505.  
  506. if sock then
  507.   sock:write("QUIT\r\n")
  508.   sock:close()
  509. end
  510. if timer then
  511.   event.cancel(timer)
  512. end
  513.  
  514. if not result then
  515.   error(reason, 0)
  516. end
  517. return reason
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement