View difference between Paste ID: nJppZks6 and gaSL8HZC
SHOW: | | - or go back to the newest paste.
1-
--[[
1+
local baseUrl = "http://86.11.11.101:9090/"
2-
IRCCC v 0.9.2 ComputerCraft IRC Client
2+
3-
Matti Vapa, 2014
3+
4-
https://github.com/mattijv/IRCCC/
4+
5-
http://pastebin.com/gaSL8HZC
5+
6
-- JSON4Lua: JSON encoding / decoding support for the Lua language.
7-
This is a ComputerCraft IRC client, or more specifically a client for the qwebirc backend server.
7+
8-
Unfortunately the HTTP API in ComputerCraft doesn't allow for a true IRC client (even though the
8+
9-
HTTP protocol can in theory be used to communicate with an IRC server[1]). This client provides
9+
10-
a work-around by interfacing with the qwebirc backend. qwebirc is a popular webchat interface and
10+
11-
is relatively straightforward to work with. In addition, several IRC servers already host a webchat
11+
12-
server, so there's no need to set up your own, although that remains a possibility if you can't
12+
13-
find a webchat for your favourite server to connect to.
13+
14
 
15-
How to use:
15+
local errormsg = printError
16
local base = _G
17-
	* Download with: pastebin get gaSL8HZC irc
17+
18-
	* Check that the baseUrl and dynamicUrl parameters are correct. Examples are provided for espernet
18+
19-
	  and quakenet. By default the client connects to espernet.
19+
20-
	* Start the client with:
20+
21-
		irc
21+
22
local decode_scanString
23-
You can also supply an optional nick argument to set your nickname, or you can change your nick within the client with the standard /nick command.
23+
24-
Currently supported commands are:
24+
25
local isArray
26-
/window N			- switch to window N
26+
27-
/join CHANNEL		- join CHANNEL
27+
28-
/part				- part the current channel or private chat
28+
29-
/quit 				- disconnect and quit
29+
30-
/nick NICK			- change nickname to NICK
30+
31-
/query USER 		- start a private chat with user USER
31+
32-
/help 				- list available commands
32+
33
 
34
  local vtype = base.type(v)  
35-
1. https://www.youtube.com/watch?v=2ctRfWnisSk#t=343
35+
36
  -- Handle strings
37-
TODO:
37+
38-
		- color support
38+
39-
		- better handling of the IRC protocol
39+
40-
		- use window API where available
40+
41
  -- Handle booleans
42-
This program is released under the MIT license.
42+
43
    return base.tostring(v)
44-
]]--
44+
45
 
46
  -- Handle tables
47-
-- look at the source of the qwebirc webchat login page and take the values
47+
48-
-- for baseUrl and dynamicBaseUrl for use here
48+
49-
-- examples for espernet and quakenet
49+
50
    local bArray, maxCount = isArray(v)
51-
local baseUrl = "http://webchat.esper.net/"
51+
52
      for i = 1,maxCount do
53-
--local baseUrl = "http://webchat.quakenet.org/"
53+
54-
--local dynamicUrl = "dynamic/leibniz/"
54+
55
    else        -- An object, not an array
56
      for i,j in base.pairs(v) do
57
        if isEncodable(i) and isEncodable(j) then
58
          table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j))
59
        end
60
      end
61
    end
62
    if bArray then
63
      return '[' .. table.concat(rval,',') ..']'
64
    else
65
      return '{' .. table.concat(rval,',') .. '}'
66
    end
67
  end
68
 
69
  -- Handle null values
70
  if vtype=='function' and v==null then
71
    return 'null'
72
  end
73
 
74
  base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v))
75
end
76
 
77
local function decode(s, startPos)
78
  startPos = startPos and startPos or 1
79
  startPos = decode_scanWhitespace(s,startPos)
80
  base.assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']')
81
  local curChar = string.sub(s,startPos,startPos)
82
  -- Object
83
  if curChar=='{' then
84
    return decode_scanObject(s,startPos)
85
  end
86
  -- Array
87
  if curChar=='[' then
88
    return decode_scanArray(s,startPos)
89
  end
90
  -- Number
91
  if string.find("+-0123456789.e", curChar, 1, true) then
92
    return decode_scanNumber(s,startPos)
93
  end
94
  -- String
95
  if curChar==[["]] or curChar==[[']] then
96
    return decode_scanString(s,startPos)
97
  end
98
  if string.sub(s,startPos,startPos+1)=='/*' then
99
    return decode(s, decode_scanComment(s,startPos))
100
  end
101
  -- Otherwise, it must be a constant
102
  return decode_scanConstant(s,startPos)
103
end
104
 
105
local function null()
106
  return null -- so json.null() will also return null ;-)
107
end
108
 
109
 
110
function decode_scanArray(s,startPos)
111
  local array = {}      -- The return value
112
  local stringLen = string.len(s)
113
  base.assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s )
114
  startPos = startPos + 1
115
  -- Infinite loop for array elements
116
  repeat
117
    startPos = decode_scanWhitespace(s,startPos)
118
    base.assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.')
119
    local curChar = string.sub(s,startPos,startPos)
120
    if (curChar==']') then
121
      return array, startPos+1
122
    end
123
    if (curChar==',') then
124
      startPos = decode_scanWhitespace(s,startPos+1)
125
    end
126
    base.assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.')
127
    object, startPos = decode(s,startPos)
128
    table.insert(array,object)
129
  until false
130
end
131
 
132
function decode_scanComment(s, startPos)
133
  base.assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos)
134
  local endPos = string.find(s,'*/',startPos+2)
135
  base.assert(endPos~=nil, "Unterminated comment in string at " .. startPos)
136
  return endPos+2  
137
end
138
 
139
function decode_scanConstant(s, startPos)
140
  local consts = { ["true"] = true, ["false"] = false, ["null"] = nil }
141
  local constNames = {"true","false","null"}
142
 
143
  for i,k in base.pairs(constNames) do
144
    --print ("[" .. string.sub(s,startPos, startPos + string.len(k) -1) .."]", k)
145
    if string.sub(s,startPos, startPos + string.len(k) -1 )==k then
146
      return consts[k], startPos + string.len(k)
147
    end
148
  end
149
  base.assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos)
150
end
151
 
152
function decode_scanNumber(s,startPos)
153
  local endPos = startPos+1
154
  local stringLen = string.len(s)
155
  local acceptableChars = "+-0123456789.e"
156
  while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true)
157
        and endPos<=stringLen
158
        ) do
159
    endPos = endPos + 1
160
  end
161
  local stringValue = 'return ' .. string.sub(s,startPos, endPos-1)
162
  local stringEval = base.loadstring(stringValue)
163
  base.assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos)
164
  return stringEval(), endPos
165
end
166
 
167
function decode_scanObject(s,startPos)
168
  local object = {}
169
  local stringLen = string.len(s)
170
  local key, value
171
  base.assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s)
172
  startPos = startPos + 1
173
  repeat
174
    startPos = decode_scanWhitespace(s,startPos)
175
    base.assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.')
176
    local curChar = string.sub(s,startPos,startPos)
177
    if (curChar=='}') then
178
      return object,startPos+1
179
    end
180
    if (curChar==',') then
181
      startPos = decode_scanWhitespace(s,startPos+1)
182
    end
183
    base.assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.')
184
    -- Scan the key
185
    key, startPos = decode(s,startPos)
186
    base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
187
    startPos = decode_scanWhitespace(s,startPos)
188
    base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
189
    base.assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos)
190
    startPos = decode_scanWhitespace(s,startPos+1)
191
    base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
192
    value, startPos = decode(s,startPos)
193
    object[key]=value
194
  until false   -- infinite loop while key-value pairs are found
195
end
196
 
197
function decode_scanString(s,startPos)
198
  base.assert(startPos, 'decode_scanString(..) called without start position')
199
  local startChar = string.sub(s,startPos,startPos)
200
  base.assert(startChar==[[']] or startChar==[["]],'decode_scanString called for a non-string')
201
  local escaped = false
202
  local endPos = startPos + 1
203
  local bEnded = false
204
  local stringLen = string.len(s)
205
  repeat
206
    local curChar = string.sub(s,endPos,endPos)
207
    -- Character escaping is only used to escape the string delimiters
208
    if not escaped then
209
      if curChar==[[\]] then
210
        escaped = true
211
      else
212
        bEnded = curChar==startChar
213
      end
214
    else
215
      -- If we're escaped, we accept the current character come what may
216
      escaped = false
217
    end
218
    endPos = endPos + 1
219
    base.assert(endPos <= stringLen+1, "String decoding failed: unterminated string at position " .. endPos)
220
  until bEnded
221
  local stringValue = 'return ' .. string.sub(s, startPos, endPos-1)
222
  local stringEval = base.loadstring(stringValue)
223
  base.assert(stringEval, 'Failed to load string [ ' .. stringValue .. '] in JSON4Lua.decode_scanString at position ' .. startPos .. ' : ' .. endPos)
224
  return stringEval(), endPos  
225
end
226
 
227
function decode_scanWhitespace(s,startPos)
228
  local whitespace=" \n\r\t"
229
  local stringLen = string.len(s)
230
  while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true)  and startPos <= stringLen) do
231
    startPos = startPos + 1
232
  end
233
  return startPos
234
end
235
 
236
function encodeString(s)
237
  s = string.gsub(s,'\\','\\\\')
238
  s = string.gsub(s,'"','\\"')
239
  s = string.gsub(s,'\n','\\n')
240
  s = string.gsub(s,'\t','\\t')
241
  return s
242
end
243
 
244
function isArray(t)
245
  -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable
246
  -- (with the possible exception of 'n')
247
  local maxIndex = 0
248
  for k,v in base.pairs(t) do
249
    if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then      -- k,v is an indexed pair
250
      if (not isEncodable(v)) then return false end     -- All array elements must be encodable
251
      maxIndex = math.max(maxIndex,k)
252
    else
253
      if (k=='n') then
254
        if v ~= table.getn(t) then return false end  -- False if n does not hold the number of elements
255
      else -- Else of (k=='n')
256
        if isEncodable(v) then return false end
257
      end  -- End of (k~='n')
258
    end -- End of k,v not an indexed pair
259
  end  -- End of loop across all pairs
260
  return true, maxIndex
261
end
262
 
263
function isEncodable(o)
264
  local t = base.type(o)
265
  return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null)
266
end
267
268
--------------------------------------------------------------------------------
269
--------------------------------------------------------------------------------
270
271
-- ComputerCraft IRC client code begins here
272
273
--------------------------------------------------------------------------------
274
--------------------------------------------------------------------------------
275
276
local defaultNick = "cc-irc-client"..tostring(math.random(1,1000))
277
local nick = defaultNick
278
local password = nil
279
local newNick = ""
280
281
local counter = 0 -- used by the qwebirc protocol for something
282
local sessionID = "" -- as is this, although a bit more self evident
283
284
local currentChannel = "status"
285
local quitReason = ""
286
287
local BUFFERSIZE = 100 -- keep this many lines in each windows history
288
local offset = 0 -- for scrolling, not implemented
289
290
local w,h = term.getSize()
291
local legacy = true --(window == nil) CC v < 1.6 doesn't have the window API which would be useful, but maybe later
292
local windows = {}
293
local winnumbers = {}
294
local seen = {}
295
if not legacy then
296
	local origTerm = term.current()
297
	windows["input"] = window.create(origTerm,1,h-1,w,2)
298
	windows["status"] = window.create(origTerm,1,1,w,h-2)
299
	windows["current"] = windows["status"]
300
else
301
	windows["status"] = {}
302
	windows["current"] = windows["status"]
303
end
304
table.insert(winnumbers,"status")
305
seen[windows["status"]] = true
306
local lock = "off"
307
308
local win2num = function(win)
309
	for k,v in pairs(winnumbers) do
310
		if v == win then return k end
311
	end
312
	return nil
313
end
314
315
--os.loadAPI("json")
316
317
local post = function(url,data)
318
	local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed...
319
	local resp = http.post(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data)
320
	counter = counter + 1
321
	return resp
322
end
323
324
-- better responsiveness with asynchronous methods as we usually let the recv couroutine handle the responses
325
local request = function(url,data)
326
	local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed...
327
	http.request(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data)
328
	counter = counter + 1
329
end
330
331
-- these special URLs are used by the webchat server for different methods
332
local send = function(data)
333
	return request("p","&s="..sessionID.."&c="..textutils.urlEncode(data))
334
end
335
336
local recv = function()
337
	return post("s","&s="..sessionID)
338
end
339
340
local connect = function(password)
341
  if password ~= nil then
342
  	return post("n","&nick="..nick.."&password="..password)
343
  else
344
    return post("n","&nick="..nick)
345
  end
346
end
347
348
-- some helper functions
349
local pong = function(data)
350
	return send("PONG :"..data)
351
end
352
353
local quit = function(_reason)
354
	local reason = _reason or ""
355
	return send("QUIT :"..reason)
356
end
357
358
-- lua default string methods suck :/
359
local split = function(str,sep)
360
        local sep, fields = sep or ":", {}
361
        local pattern = string.format("([^%s]+)", sep)
362
        str:gsub(pattern, function(c) fields[#fields+1] = c end)
363
        return fields
364
end
365
366
local exit = function(reason)
367
	local r = reason or ""
368
	term.clear()
369
	term.setCursorPos(1,1)
370
	print(r)
371
end
372
373
-- for debug
374
local writeLine
375
writeLine = function(data,line)
376
	if not (type(data) == "table") then
377
		return tostring(data)
378
	end
379
	for i=1,#data do
380
		if type(data[i]) == "table" then
381
			line = writeLine(data[i],line)
382
		elseif data[i] ~= nick then
383
			line = line..data[i].." "
384
		end
385
	end
386
	return line
387
end
388
389
-- some IRC protocol codes we handle "properly"
390
local codes = {}
391
codes["371"] = "RPL_INFO"
392
codes["374"] = "RPL_ENDINFO"
393
codes["375"] = "RPL_MOTDSTART"
394
codes["372"] = "RPL_MOTD"
395
codes["376"] = "RPL_ENDOFMOTD"
396
codes["352"] = "RPL_WHOREPLY"
397
398
commands = {}
399
commands["join"] = function(input)
400
  if not input[2] then
401
    errormsg("No channel specified!")
402
    return true
403
  end
404
    local channel = input[2]:lower()
405
    if channel:sub(1,1) ~= "#" then
406
        errormsg("Invalid channel name!")
407
        return true
408
    end
409
    currentChannel = channel
410
    if legacy then
411
        windows[channel] = {}
412
    end
413
    windows["current"] = windows[currentChannel]
414
    table.insert(winnumbers,channel)
415
    seen[windows[channel]] = true
416
    send("JOIN "..channel)
417
    return true
418
end
419
 
420
commands["who"] = function(input)
421
    local channel = input[2]
422
    send("WHO "..channel)
423
    return true
424
end
425
 
426
commands["whois"] = function(input)
427
    local user = input[2]
428
    send("WHOIS "..user)
429
    return true
430
end
431
 
432
commands["part"] = function(input)
433
    local channel = currentChannel
434
    if channel == "status" then
435
        errormsg("Can't part status window!")
436
    else
437
        local nwin
438
        for i,v in pairs(winnumbers) do
439
            if v == channel then
440
                nwin = i
441
                break
442
            end
443
        end
444
        table.remove(winnumbers,nwin)
445
        seen[windows[channel]] = nil
446
        windows[channel] = nil
447
        currentChannel = winnumbers[#winnumbers]
448
        print(currentChannel)
449
        windows["current"] = windows[currentChannel]
450-
-- add lines to window
450+
        seen[windows["current"]] = true
451-
local writeToWin = function(win, s)
451+
        if channel:sub(1,1) == "#" then
452-
	if win ~= windows["current"] then
452+
            send("PART "..channel)
453-
		seen[win] = false
453+
454
    end
455-
	if legacy then
455+
456-
		while #s > w do
456+
457-
			table.insert(win,s:sub(1,w))
457+
458-
			s = s:sub(w+1)
458+
459
    quit(input[2])
460-
		table.insert(win,s)
460+
    return false
461-
		while #win > BUFFERSIZE do
461+
462-
			table.remove(win,1)
462+
463
commands["window"] = function(input)
464
    local nwin = tonumber(input[2])
465
    if not nwin then
466
        errormsg("Invalid window number!")
467
    elseif nwin > #winnumbers or nwin < 1 then
468
        errormsg("Invalid window number!")
469-
	if legacy then
469+
470-
		writeToWin(windows["status"],msg)
470+
        windows["current"] = windows[winnumbers[nwin]]
471
        seen[windows["current"]] = true
472
        currentChannel = winnumbers[nwin]
473
    end
474-
-- draw the current window to the screen
474+
475-
local drawWin = function(win)
475+
476-
	if legacy then
476+
477-
		local x,y = term.getCursorPos()
477+
478-
		for i = 2,h-2 do
478+
    local nickname = input[2]
479-
			term.setCursorPos(1,i)
479+
    if nickname then
480-
			term.clearLine()
480+
        send("NICK "..nickname)
481
        newNick = nickname
482-
		if #win > 0 then
482+
        writeToWin(windows["status"],"Changed nick to: "..newNick)
483-
			local i = math.max(1,#win-h+4)
483+
484-
			iend = #win
484+
        errormsg("No nickname given.")
485-
			local row = 2
485+
486-
			while i <= iend do
486+
487-
				term.setCursorPos(1,row)
487+
488-
				local line = win[i]
488+
489-
				--[[
489+
490-
				local n = 0
490+
    local user = input[2]
491-
				while #line > w do
491+
    if user then
492-
					term.write(line:sub(1,w))
492+
        currentChannel = user
493-
					row = row + 1
493+
        if legacy then
494-
					n = n + 1
494+
            windows[user] = {}
495-
					term.setCursorPos(1,row)
495+
496-
					line = line:sub(w+1)
496+
        windows["current"] = windows[currentChannel]
497-
				end
497+
        table.insert(winnumbers,user)
498-
				]]--
498+
        seen[windows[user]] = true
499-
				term.write(line)
499+
        --send("JOIN "..channel)
500-
				row = row + 1
500+
501-
				i = i + 1
501+
        errormsg("No username given.")
502
    end
503
    return true
504-
		term.setCursorPos(x,y)
504+
505
 
506
commands["help"] = function(input)
507
    writeToWin(windows["status"],"Available commands:")
508-
-- draw the separator with list of windows and indicators of activity
508+
    for k,v in pairs(commands) do
509
        writeToWin(windows["status"],"- "..k)
510-
local drawSeparator = function()
510+
511-
	if legacy then
511+
512-
		local x,y = term.getCursorPos()
512+
513-
		local nwin = #winnumbers
513+
514-
		--local left = math.floor(0.5*(w-2*nwin+1))
514+
515-
		--local right = w-left-2*nwin-1
515+
516-
		term.setCursorPos(1,h-1)
516+
517-
		term.write(string.rep("-",w))
517+
518-
		local newcount = 0
518+
519-
		for i = 1,nwin do
519+
520-
			if not seen[windows[winnumbers[i]]] then
520+
521-
				newcount = newcount + 1
521+
522
alias["qr"] = "query"
523
alias["h"] = "help"
524-
		local start = math.ceil(0.5*(w-(2*nwin+1+newcount)))
524+
525-
		term.setCursorPos(start,h-1)
525+
526-
		for i=1,nwin do
526+
527-
			term.write(" ")
527+
	print(msg)
528-
			if winnumbers[i] == currentChannel then
528+
529-
				term.setBackgroundColor(colors.white)
529+
530-
				term.setTextColor(colors.black)
530+
531
	dat = data
532-
			term.write(string.format("%d",i))
532+
533-
			if not seen[windows[winnumbers[i]]] then
533+
534-
				term.write("+")
534+
535
			pong(data[i][4][1])
536-
			term.setBackgroundColor(colors.black)
536+
537-
			term.setTextColor(colors.white)
537+
538
			local sender = senderDetails:sub(1,senderDetails:find("!")-1)
539-
		term.write(" ")
539+
540-
		term.setCursorPos(x,y)
540+
541
				channel = sender
542
			end
543
			local msg = data[i][4][2]
544-
-- top banner with channel name
544+
			print("<"..sender.."> "..msg)
545-
local drawBanner = function()
545+
		elseif id == "376" then
546-
	local x,y = term.getCursorPos()
546+
			print("Joining channel")
547
			send("JOIN #" .. channel)
548-
	term.clearLine()
548+
549-
	--local banner = "["..currentChannel.."]"
549+
            if newNick ~= "" then
550-
	local banner = currentChannel
550+
            	local name = data[i][3]:sub(1,data[i][3]:find("!")-1)
551-
	local start = math.ceil(0.5*(w-#banner))
551+
              	if name == nick then
552-
	term.setCursorPos(start,1)
552+
                   	nick = newNick
553-
	term.write(banner)
553+
                   	newNick = ""
554-
	term.setCursorPos(x,y)
554+
               	end
555
			end
556
   		elseif id == "433" then
557-
local drawInput = function()
557+
           	print("Nickname already in use!")
558-
	if legacy then
558+
            newNick = ""
559-
		term.setCursorPos(1,h)
559+
        elseif codes[id] then
560-
		term.clearLine()
560+
          	print(data[i][4][2])
561-
		term.write("> ")
561+
562
	end
563
end
564
565
566
local receive = function()
567
	resp = recv()
568
	while resp do
569
		_data = resp.readAll()
570
		if #_data > 0 then
571
			data = decode(_data)
572
			handleResponse(data)
573
		end
574-
      --print(textutils.serialize(data[i]))
574+
575-
      --print(channel)
575+
576
	quitReason = "Disconnected!"
577
end
578-
				if not windows[sender] then
578+
579-
					if legacy then
579+
local nick = "twitchplayzcomputercraft"
580-
						windows[channel] = {}
580+
local password = "oauth:pq386z6ttls22h7bmsqbiuil1i2ns3"
581-
					end
581+
local channel = "rockettek"
582-
					table.insert(winnumbers,channel)
582+
583-
					seen[windows[channel]] = false
583+
local resp = connect(password)
584-
				end
584+
if not resp then printError("Unable to connect!") return end
585
local _data = resp.readAll()
586
local data = decode(_data)
587-
			if legacy then
587+
local sessionID = data[2]
588-
				writeToWin(windows[channel],"<"..sender.."> "..msg)
588+
589
590-
			--print("<"..sender.."> "..msg)
590+
receive()