Advertisement
Doob

[OpenComputers] IRC client + terminal glasses

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