View difference between Paste ID: 1WLuqC2q and GPR7eGTj
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