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