Advertisement
Vladimir2

Untitled

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