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