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 | + | local nick = "rockettek" |
5 | - | http://pastebin.com/gaSL8HZC |
5 | + | local password = "oauth:c306b08cc06j7foa7uzxez63fyiff4" |
6 | ||
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 | -- This module is released under the MIT License (MIT). | |
15 | - | How to use: |
15 | + | |
16 | -- edited for brevity | |
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_scanNumber | |
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 decode_scanWhitespace | |
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 | return "null" | |
34 | end | |
35 | - | 1. https://www.youtube.com/watch?v=2ctRfWnisSk#t=343 |
35 | + | |
36 | local vtype = base.type(v) | |
37 | - | TODO: |
37 | + | |
38 | - | - color support |
38 | + | |
39 | - | - better handling of the IRC protocol |
39 | + | |
40 | - | - use window API where available |
40 | + | |
41 | end | |
42 | - | This program is released under the MIT license. |
42 | + | |
43 | -- Handle booleans | |
44 | - | ]]-- |
44 | + | |
45 | return base.tostring(v) | |
46 | end | |
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 rval = {} | |
51 | - | local baseUrl = "http://webchat.esper.net/" |
51 | + | |
52 | local bArray, maxCount = isArray(v) | |
53 | - | --local baseUrl = "http://webchat.quakenet.org/" |
53 | + | |
54 | - | --local dynamicUrl = "dynamic/leibniz/" |
54 | + | |
55 | table.insert(rval, encode(v[i])) | |
56 | end | |
57 | else -- An object, not an array | |
58 | for i,j in base.pairs(v) do | |
59 | if isEncodable(i) and isEncodable(j) then | |
60 | table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j)) | |
61 | end | |
62 | end | |
63 | end | |
64 | if bArray then | |
65 | return '[' .. table.concat(rval,',') ..']' | |
66 | else | |
67 | return '{' .. table.concat(rval,',') .. '}' | |
68 | end | |
69 | end | |
70 | ||
71 | -- Handle null values | |
72 | if vtype=='function' and v==null then | |
73 | return 'null' | |
74 | end | |
75 | ||
76 | base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v)) | |
77 | end | |
78 | ||
79 | local function decode(s, startPos) | |
80 | startPos = startPos and startPos or 1 | |
81 | startPos = decode_scanWhitespace(s,startPos) | |
82 | base.assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') | |
83 | local curChar = string.sub(s,startPos,startPos) | |
84 | -- Object | |
85 | if curChar=='{' then | |
86 | return decode_scanObject(s,startPos) | |
87 | end | |
88 | -- Array | |
89 | if curChar=='[' then | |
90 | return decode_scanArray(s,startPos) | |
91 | end | |
92 | -- Number | |
93 | if string.find("+-0123456789.e", curChar, 1, true) then | |
94 | return decode_scanNumber(s,startPos) | |
95 | end | |
96 | -- String | |
97 | if curChar==[["]] or curChar==[[']] then | |
98 | return decode_scanString(s,startPos) | |
99 | end | |
100 | if string.sub(s,startPos,startPos+1)=='/*' then | |
101 | return decode(s, decode_scanComment(s,startPos)) | |
102 | end | |
103 | -- Otherwise, it must be a constant | |
104 | return decode_scanConstant(s,startPos) | |
105 | end | |
106 | ||
107 | local function null() | |
108 | return null -- so json.null() will also return null ;-) | |
109 | end | |
110 | ||
111 | ||
112 | function decode_scanArray(s,startPos) | |
113 | local array = {} -- The return value | |
114 | local stringLen = string.len(s) | |
115 | base.assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) | |
116 | startPos = startPos + 1 | |
117 | -- Infinite loop for array elements | |
118 | repeat | |
119 | startPos = decode_scanWhitespace(s,startPos) | |
120 | base.assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') | |
121 | local curChar = string.sub(s,startPos,startPos) | |
122 | if (curChar==']') then | |
123 | return array, startPos+1 | |
124 | end | |
125 | if (curChar==',') then | |
126 | startPos = decode_scanWhitespace(s,startPos+1) | |
127 | end | |
128 | base.assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') | |
129 | object, startPos = decode(s,startPos) | |
130 | table.insert(array,object) | |
131 | until false | |
132 | end | |
133 | ||
134 | function decode_scanComment(s, startPos) | |
135 | base.assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) | |
136 | local endPos = string.find(s,'*/',startPos+2) | |
137 | base.assert(endPos~=nil, "Unterminated comment in string at " .. startPos) | |
138 | return endPos+2 | |
139 | end | |
140 | ||
141 | function decode_scanConstant(s, startPos) | |
142 | local consts = { ["true"] = true, ["false"] = false, ["null"] = nil } | |
143 | local constNames = {"true","false","null"} | |
144 | ||
145 | for i,k in base.pairs(constNames) do | |
146 | --print ("[" .. string.sub(s,startPos, startPos + string.len(k) -1) .."]", k) | |
147 | if string.sub(s,startPos, startPos + string.len(k) -1 )==k then | |
148 | return consts[k], startPos + string.len(k) | |
149 | end | |
150 | end | |
151 | base.assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) | |
152 | end | |
153 | ||
154 | function decode_scanNumber(s,startPos) | |
155 | local endPos = startPos+1 | |
156 | local stringLen = string.len(s) | |
157 | local acceptableChars = "+-0123456789.e" | |
158 | while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) | |
159 | and endPos<=stringLen | |
160 | ) do | |
161 | endPos = endPos + 1 | |
162 | end | |
163 | local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) | |
164 | local stringEval = base.loadstring(stringValue) | |
165 | base.assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) | |
166 | return stringEval(), endPos | |
167 | end | |
168 | ||
169 | function decode_scanObject(s,startPos) | |
170 | local object = {} | |
171 | local stringLen = string.len(s) | |
172 | local key, value | |
173 | base.assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s) | |
174 | startPos = startPos + 1 | |
175 | repeat | |
176 | startPos = decode_scanWhitespace(s,startPos) | |
177 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') | |
178 | local curChar = string.sub(s,startPos,startPos) | |
179 | if (curChar=='}') then | |
180 | return object,startPos+1 | |
181 | end | |
182 | if (curChar==',') then | |
183 | startPos = decode_scanWhitespace(s,startPos+1) | |
184 | end | |
185 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') | |
186 | -- Scan the key | |
187 | key, startPos = decode(s,startPos) | |
188 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
189 | startPos = decode_scanWhitespace(s,startPos) | |
190 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
191 | base.assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) | |
192 | startPos = decode_scanWhitespace(s,startPos+1) | |
193 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
194 | value, startPos = decode(s,startPos) | |
195 | object[key]=value | |
196 | until false -- infinite loop while key-value pairs are found | |
197 | end | |
198 | ||
199 | function decode_scanString(s,startPos) | |
200 | base.assert(startPos, 'decode_scanString(..) called without start position') | |
201 | local startChar = string.sub(s,startPos,startPos) | |
202 | base.assert(startChar==[[']] or startChar==[["]],'decode_scanString called for a non-string') | |
203 | local escaped = false | |
204 | local endPos = startPos + 1 | |
205 | local bEnded = false | |
206 | local stringLen = string.len(s) | |
207 | repeat | |
208 | local curChar = string.sub(s,endPos,endPos) | |
209 | -- Character escaping is only used to escape the string delimiters | |
210 | if not escaped then | |
211 | if curChar==[[\]] then | |
212 | escaped = true | |
213 | else | |
214 | bEnded = curChar==startChar | |
215 | end | |
216 | else | |
217 | -- If we're escaped, we accept the current character come what may | |
218 | escaped = false | |
219 | end | |
220 | endPos = endPos + 1 | |
221 | base.assert(endPos <= stringLen+1, "String decoding failed: unterminated string at position " .. endPos) | |
222 | until bEnded | |
223 | local stringValue = 'return ' .. string.sub(s, startPos, endPos-1) | |
224 | local stringEval = base.loadstring(stringValue) | |
225 | base.assert(stringEval, 'Failed to load string [ ' .. stringValue .. '] in JSON4Lua.decode_scanString at position ' .. startPos .. ' : ' .. endPos) | |
226 | return stringEval(), endPos | |
227 | end | |
228 | ||
229 | function decode_scanWhitespace(s,startPos) | |
230 | local whitespace=" \n\r\t" | |
231 | local stringLen = string.len(s) | |
232 | while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do | |
233 | startPos = startPos + 1 | |
234 | end | |
235 | return startPos | |
236 | end | |
237 | ||
238 | function encodeString(s) | |
239 | s = string.gsub(s,'\\','\\\\') | |
240 | s = string.gsub(s,'"','\\"') | |
241 | s = string.gsub(s,'\n','\\n') | |
242 | s = string.gsub(s,'\t','\\t') | |
243 | return s | |
244 | end | |
245 | ||
246 | function isArray(t) | |
247 | -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable | |
248 | -- (with the possible exception of 'n') | |
249 | local maxIndex = 0 | |
250 | for k,v in base.pairs(t) do | |
251 | if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair | |
252 | if (not isEncodable(v)) then return false end -- All array elements must be encodable | |
253 | maxIndex = math.max(maxIndex,k) | |
254 | else | |
255 | if (k=='n') then | |
256 | if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements | |
257 | else -- Else of (k=='n') | |
258 | if isEncodable(v) then return false end | |
259 | end -- End of (k~='n') | |
260 | end -- End of k,v not an indexed pair | |
261 | end -- End of loop across all pairs | |
262 | return true, maxIndex | |
263 | end | |
264 | ||
265 | function isEncodable(o) | |
266 | local t = base.type(o) | |
267 | return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) | |
268 | end | |
269 | ||
270 | -------------------------------------------------------------------------------- | |
271 | -------------------------------------------------------------------------------- | |
272 | ||
273 | -- ComputerCraft IRC client code begins here | |
274 | ||
275 | -------------------------------------------------------------------------------- | |
276 | -------------------------------------------------------------------------------- | |
277 | ||
278 | local defaultNick = "cc-irc-client"..tostring(math.random(1,1000)) | |
279 | local nick = defaultNick | |
280 | local password = nil | |
281 | local newNick = "" | |
282 | ||
283 | local counter = 0 -- used by the qwebirc protocol for something | |
284 | local sessionID = "" -- as is this, although a bit more self evident | |
285 | ||
286 | local currentChannel = "status" | |
287 | local quitReason = "" | |
288 | ||
289 | local BUFFERSIZE = 100 -- keep this many lines in each windows history | |
290 | local offset = 0 -- for scrolling, not implemented | |
291 | ||
292 | local win2num = function(win) | |
293 | for k,v in pairs(winnumbers) do | |
294 | if v == win then return k end | |
295 | end | |
296 | return nil | |
297 | end | |
298 | ||
299 | --os.loadAPI("json") | |
300 | ||
301 | local post = function(url,data) | |
302 | local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed... | |
303 | local resp = http.post(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data) | |
304 | counter = counter + 1 | |
305 | return resp | |
306 | end | |
307 | ||
308 | -- better responsiveness with asynchronous methods as we usually let the recv couroutine handle the responses | |
309 | local request = function(url,data) | |
310 | local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed... | |
311 | http.request(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data) | |
312 | counter = counter + 1 | |
313 | end | |
314 | ||
315 | -- these special URLs are used by the webchat server for different methods | |
316 | local send = function(data) | |
317 | return request("p","&s="..sessionID.."&c="..textutils.urlEncode(data)) | |
318 | end | |
319 | ||
320 | local recv = function() | |
321 | return post("s","&s="..sessionID) | |
322 | end | |
323 | ||
324 | local connect = function(password) | |
325 | if password ~= nil then | |
326 | return post("n","&nick="..nick.."&password="..password) | |
327 | else | |
328 | return post("n","&nick="..nick) | |
329 | end | |
330 | end | |
331 | ||
332 | -- some helper functions | |
333 | local pong = function(data) | |
334 | return send("PONG :"..data) | |
335 | end | |
336 | ||
337 | local quit = function(_reason) | |
338 | local reason = _reason or "" | |
339 | return send("QUIT :"..reason) | |
340 | end | |
341 | ||
342 | - | local w,h = term.getSize() |
342 | + | |
343 | - | local legacy = true --(window == nil) CC v < 1.6 doesn't have the window API which would be useful, but maybe later |
343 | + | |
344 | - | local windows = {} |
344 | + | |
345 | - | local winnumbers = {} |
345 | + | |
346 | - | local seen = {} |
346 | + | |
347 | - | if not legacy then |
347 | + | |
348 | - | local origTerm = term.current() |
348 | + | |
349 | - | windows["input"] = window.create(origTerm,1,h-1,w,2) |
349 | + | |
350 | - | windows["status"] = window.create(origTerm,1,1,w,h-2) |
350 | + | |
351 | - | windows["current"] = windows["status"] |
351 | + | |
352 | - | else |
352 | + | |
353 | - | windows["status"] = {} |
353 | + | |
354 | - | windows["current"] = windows["status"] |
354 | + | |
355 | end | |
356 | - | table.insert(winnumbers,"status") |
356 | + | |
357 | - | seen[windows["status"]] = true |
357 | + | |
358 | - | local lock = "off" |
358 | + | |
359 | writeLine = function(data,line) | |
360 | if not (type(data) == "table") then | |
361 | return tostring(data) | |
362 | end | |
363 | for i=1,#data do | |
364 | if type(data[i]) == "table" then | |
365 | line = writeLine(data[i],line) | |
366 | elseif data[i] ~= nick then | |
367 | line = line..data[i].." " | |
368 | end | |
369 | end | |
370 | return line | |
371 | end | |
372 | ||
373 | -- some IRC protocol codes we handle "properly" | |
374 | local codes = {} | |
375 | codes["371"] = "RPL_INFO" | |
376 | codes["374"] = "RPL_ENDINFO" | |
377 | codes["375"] = "RPL_MOTDSTART" | |
378 | codes["372"] = "RPL_MOTD" | |
379 | codes["376"] = "RPL_ENDOFMOTD" | |
380 | codes["352"] = "RPL_WHOREPLY" | |
381 | ||
382 | -- add lines to window | |
383 | local writeToWin = function(win, s) | |
384 | if win ~= windows["current"] then | |
385 | seen[win] = false | |
386 | end | |
387 | if legacy then | |
388 | while #s > w do | |
389 | table.insert(win,s:sub(1,w)) | |
390 | s = s:sub(w+1) | |
391 | end | |
392 | table.insert(win,s) | |
393 | while #win > BUFFERSIZE do | |
394 | table.remove(win,1) | |
395 | end | |
396 | end | |
397 | end | |
398 | ||
399 | -- helper | |
400 | local errormsg = function(msg) | |
401 | if legacy then | |
402 | print(msg) | |
403 | end | |
404 | end | |
405 | ||
406 | local handleResponse = function(data) | |
407 | for i = 1, #data do | |
408 | local id = data[i][2] | |
409 | if id == "PING" then | |
410 | pong(data[i][4][1]) | |
411 | elseif id == "PRIVMSG" then | |
412 | local senderDetails = data[i][3] | |
413 | local sender = senderDetails:sub(1,senderDetails:find("!")-1) | |
414 | local channel = data[i][4][1]:lower() | |
415 | --print(textutils.serialize(data[i])) | |
416 | --print(channel) | |
417 | if channel == nick then | |
418 | channel = sender | |
419 | end | |
420 | local msg = data[i][4][2] | |
421 | print("<"..sender.."> "..msg) | |
422 | elseif id == "NICK" then | |
423 | if newNick ~= "" then | |
424 | local name = data[i][3]:sub(1,data[i][3]:find("!")-1) | |
425 | if name == nick then | |
426 | nick = newNick | |
427 | newNick = "" | |
428 | end | |
429 | end | |
430 | elseif id == "433" then | |
431 | print("Nickname already in use!") | |
432 | newNick = "" | |
433 | elseif codes[id] then | |
434 | --[[if legacy then | |
435 | print(writeLine(data[i][4],"")) | |
436 | --print(data[i][4][2]) | |
437 | end]] | |
438 | print(data[i][4][2]) | |
439 | else | |
440 | print(writeLine(data[i],"")) | |
441 | end | |
442 | end | |
443 | end | |
444 | ||
445 | commands = {} | |
446 | commands["join"] = function(input) | |
447 | if not input[2] then | |
448 | print("No channel specified!") | |
449 | return true | |
450 | end | |
451 | local channel = input[2]:lower() | |
452 | if channel:sub(1,1) ~= "#" then | |
453 | errormsg("Invalid channel name!") | |
454 | return true | |
455 | end | |
456 | currentChannel = channel | |
457 | send("JOIN "..channel) | |
458 | return true | |
459 | end | |
460 | ||
461 | local alias = {} | |
462 | alias["j"] = "join" | |
463 | ||
464 | local receive = function() | |
465 | resp = recv() | |
466 | while resp do | |
467 | _data = resp.readAll() | |
468 | if #_data > 0 then | |
469 | data = decode(_data) | |
470 | - | writeToWin(windows["status"],msg) |
470 | + | |
471 | while lock == "on" do | |
472 | sleep(0.1) | |
473 | end | |
474 | - | -- draw the current window to the screen |
474 | + | |
475 | - | local drawWin = function(win) |
475 | + | |
476 | drawWin(windows["current"]) | |
477 | - | local x,y = term.getCursorPos() |
477 | + | |
478 | - | for i = 2,h-2 do |
478 | + | |
479 | - | term.setCursorPos(1,i) |
479 | + | |
480 | - | term.clearLine() |
480 | + | |
481 | resp = recv() | |
482 | - | if #win > 0 then |
482 | + | |
483 | - | local i = math.max(1,#win-h+4) |
483 | + | |
484 | - | iend = #win |
484 | + | |
485 | - | local row = 2 |
485 | + | print("Connecting...") |
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 | + | |
491 | - | while #line > w do |
491 | + | |
492 | - | term.write(line:sub(1,w)) |
492 | + | return commands[alias["j"]]("#rockettek") |
493 | - | row = row + 1 |
493 | + | interface() |