Advertisement
Alakazard12

OC IRC

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