osmarks

oirc

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