Advertisement
Guest User

Untitled

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