Wistaf

IRC

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