Advertisement
Guest User

irc.lua

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