dannysmc95

OC-IRC-Client

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