Advertisement
accelleon

Untitled

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