SHOW:
|
|
- or go back to the newest paste.
| 1 | --[[ | |
| 2 | IRCCC v 0.9.2 ComputerCraft IRC Client | |
| 3 | Matti Vapa, 2014 | |
| 4 | https://github.com/mattijv/IRCCC/ | |
| 5 | http://pastebin.com/gaSL8HZC | |
| 6 | ||
| 7 | This is a ComputerCraft IRC client, or more specifically a client for the qwebirc backend server. | |
| 8 | Unfortunately the HTTP API in ComputerCraft doesn't allow for a true IRC client (even though the | |
| 9 | HTTP protocol can in theory be used to communicate with an IRC server[1]). This client provides | |
| 10 | a work-around by interfacing with the qwebirc backend. qwebirc is a popular webchat interface and | |
| 11 | is relatively straightforward to work with. In addition, several IRC servers already host a webchat | |
| 12 | server, so there's no need to set up your own, although that remains a possibility if you can't | |
| 13 | find a webchat for your favourite server to connect to. | |
| 14 | ||
| 15 | How to use: | |
| 16 | ||
| 17 | * Download with: pastebin get gaSL8HZC irc | |
| 18 | * Check that the baseUrl and dynamicUrl parameters are correct. Examples are provided for espernet | |
| 19 | and quakenet. By default the client connects to espernet. | |
| 20 | * Start the client with: | |
| 21 | irc | |
| 22 | ||
| 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. | |
| 24 | Currently supported commands are: | |
| 25 | ||
| 26 | /window N - switch to window N | |
| 27 | /join CHANNEL - join CHANNEL | |
| 28 | /part - part the current channel or private chat | |
| 29 | /quit - disconnect and quit | |
| 30 | /nick NICK - change nickname to NICK | |
| 31 | /query USER - start a private chat with user USER | |
| 32 | /help - list available commands | |
| 33 | ||
| 34 | ||
| 35 | 1. https://www.youtube.com/watch?v=2ctRfWnisSk#t=343 | |
| 36 | ||
| 37 | TODO: | |
| 38 | - color support | |
| 39 | - better handling of the IRC protocol | |
| 40 | - use window API where available | |
| 41 | ||
| 42 | This program is released under the MIT license. | |
| 43 | ||
| 44 | ]]-- | |
| 45 | ||
| 46 | ||
| 47 | -- look at the source of the qwebirc webchat login page and take the values | |
| 48 | -- for baseUrl and dynamicBaseUrl for use here | |
| 49 | -- examples for espernet and quakenet | |
| 50 | ||
| 51 | local baseUrl = "http://webchat.esper.net/" | |
| 52 | local dynamicUrl = "" | |
| 53 | --local baseUrl = "http://webchat.quakenet.org/" | |
| 54 | --local dynamicUrl = "dynamic/leibniz/" | |
| 55 | ||
| 56 | ||
| 57 | -------------------------------------------------------------------------------- | |
| 58 | -------------------------------------------------------------------------------- | |
| 59 | -- JSON4Lua: JSON encoding / decoding support for the Lua language. | |
| 60 | -- json Module. | |
| 61 | -- Author: Craig Mason-Jones | |
| 62 | -- Homepage: http://json.luaforge.net/ | |
| 63 | -- Version: 0.9.40 | |
| 64 | -- This module is released under the MIT License (MIT). | |
| 65 | ||
| 66 | -- edited for brevity | |
| 67 | ||
| 68 | local base = _G | |
| 69 | local decode_scanArray | |
| 70 | local decode_scanComment | |
| 71 | local decode_scanConstant | |
| 72 | local decode_scanNumber | |
| 73 | local decode_scanObject | |
| 74 | local decode_scanString | |
| 75 | local decode_scanWhitespace | |
| 76 | local encodeString | |
| 77 | local isArray | |
| 78 | local isEncodable | |
| 79 | ||
| 80 | local function encode (v) | |
| 81 | -- Handle nil values | |
| 82 | if v==nil then | |
| 83 | return "null" | |
| 84 | end | |
| 85 | ||
| 86 | local vtype = base.type(v) | |
| 87 | ||
| 88 | -- Handle strings | |
| 89 | if vtype=='string' then | |
| 90 | return '"' .. encodeString(v) .. '"' -- Need to handle encoding in string | |
| 91 | end | |
| 92 | ||
| 93 | -- Handle booleans | |
| 94 | if vtype=='number' or vtype=='boolean' then | |
| 95 | return base.tostring(v) | |
| 96 | end | |
| 97 | ||
| 98 | -- Handle tables | |
| 99 | if vtype=='table' then | |
| 100 | local rval = {}
| |
| 101 | -- Consider arrays separately | |
| 102 | local bArray, maxCount = isArray(v) | |
| 103 | if bArray then | |
| 104 | for i = 1,maxCount do | |
| 105 | table.insert(rval, encode(v[i])) | |
| 106 | end | |
| 107 | else -- An object, not an array | |
| 108 | for i,j in base.pairs(v) do | |
| 109 | if isEncodable(i) and isEncodable(j) then | |
| 110 | table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j)) | |
| 111 | end | |
| 112 | end | |
| 113 | end | |
| 114 | if bArray then | |
| 115 | return '[' .. table.concat(rval,',') ..']' | |
| 116 | else | |
| 117 | return '{' .. table.concat(rval,',') .. '}'
| |
| 118 | end | |
| 119 | end | |
| 120 | ||
| 121 | -- Handle null values | |
| 122 | if vtype=='function' and v==null then | |
| 123 | return 'null' | |
| 124 | end | |
| 125 | ||
| 126 | base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v)) | |
| 127 | end | |
| 128 | ||
| 129 | local function decode(s, startPos) | |
| 130 | startPos = startPos and startPos or 1 | |
| 131 | startPos = decode_scanWhitespace(s,startPos) | |
| 132 | base.assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') | |
| 133 | local curChar = string.sub(s,startPos,startPos) | |
| 134 | -- Object | |
| 135 | if curChar=='{' then
| |
| 136 | return decode_scanObject(s,startPos) | |
| 137 | end | |
| 138 | -- Array | |
| 139 | if curChar=='[' then | |
| 140 | return decode_scanArray(s,startPos) | |
| 141 | end | |
| 142 | -- Number | |
| 143 | if string.find("+-0123456789.e", curChar, 1, true) then
| |
| 144 | return decode_scanNumber(s,startPos) | |
| 145 | end | |
| 146 | -- String | |
| 147 | if curChar==[["]] or curChar==[[']] then | |
| 148 | return decode_scanString(s,startPos) | |
| 149 | end | |
| 150 | if string.sub(s,startPos,startPos+1)=='/*' then | |
| 151 | return decode(s, decode_scanComment(s,startPos)) | |
| 152 | end | |
| 153 | -- Otherwise, it must be a constant | |
| 154 | return decode_scanConstant(s,startPos) | |
| 155 | end | |
| 156 | ||
| 157 | local function null() | |
| 158 | return null -- so json.null() will also return null ;-) | |
| 159 | end | |
| 160 | ||
| 161 | ||
| 162 | function decode_scanArray(s,startPos) | |
| 163 | local array = {} -- The return value
| |
| 164 | local stringLen = string.len(s) | |
| 165 | base.assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) | |
| 166 | startPos = startPos + 1 | |
| 167 | -- Infinite loop for array elements | |
| 168 | repeat | |
| 169 | startPos = decode_scanWhitespace(s,startPos) | |
| 170 | base.assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') | |
| 171 | local curChar = string.sub(s,startPos,startPos) | |
| 172 | if (curChar==']') then | |
| 173 | return array, startPos+1 | |
| 174 | end | |
| 175 | if (curChar==',') then | |
| 176 | startPos = decode_scanWhitespace(s,startPos+1) | |
| 177 | end | |
| 178 | base.assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') | |
| 179 | object, startPos = decode(s,startPos) | |
| 180 | table.insert(array,object) | |
| 181 | until false | |
| 182 | end | |
| 183 | ||
| 184 | function decode_scanComment(s, startPos) | |
| 185 | base.assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) | |
| 186 | local endPos = string.find(s,'*/',startPos+2) | |
| 187 | base.assert(endPos~=nil, "Unterminated comment in string at " .. startPos) | |
| 188 | return endPos+2 | |
| 189 | end | |
| 190 | ||
| 191 | function decode_scanConstant(s, startPos) | |
| 192 | local consts = { ["true"] = true, ["false"] = false, ["null"] = nil }
| |
| 193 | local constNames = {"true","false","null"}
| |
| 194 | ||
| 195 | for i,k in base.pairs(constNames) do | |
| 196 | --print ("[" .. string.sub(s,startPos, startPos + string.len(k) -1) .."]", k)
| |
| 197 | if string.sub(s,startPos, startPos + string.len(k) -1 )==k then | |
| 198 | return consts[k], startPos + string.len(k) | |
| 199 | end | |
| 200 | end | |
| 201 | base.assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) | |
| 202 | end | |
| 203 | ||
| 204 | function decode_scanNumber(s,startPos) | |
| 205 | local endPos = startPos+1 | |
| 206 | local stringLen = string.len(s) | |
| 207 | local acceptableChars = "+-0123456789.e" | |
| 208 | while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) | |
| 209 | and endPos<=stringLen | |
| 210 | ) do | |
| 211 | endPos = endPos + 1 | |
| 212 | end | |
| 213 | local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) | |
| 214 | local stringEval = base.loadstring(stringValue) | |
| 215 | base.assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) | |
| 216 | return stringEval(), endPos | |
| 217 | end | |
| 218 | ||
| 219 | function decode_scanObject(s,startPos) | |
| 220 | local object = {}
| |
| 221 | local stringLen = string.len(s) | |
| 222 | local key, value | |
| 223 | base.assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s)
| |
| 224 | startPos = startPos + 1 | |
| 225 | repeat | |
| 226 | startPos = decode_scanWhitespace(s,startPos) | |
| 227 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') | |
| 228 | local curChar = string.sub(s,startPos,startPos) | |
| 229 | if (curChar=='}') then | |
| 230 | return object,startPos+1 | |
| 231 | end | |
| 232 | if (curChar==',') then | |
| 233 | startPos = decode_scanWhitespace(s,startPos+1) | |
| 234 | end | |
| 235 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') | |
| 236 | -- Scan the key | |
| 237 | key, startPos = decode(s,startPos) | |
| 238 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
| 239 | startPos = decode_scanWhitespace(s,startPos) | |
| 240 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
| 241 | base.assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) | |
| 242 | startPos = decode_scanWhitespace(s,startPos+1) | |
| 243 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
| 244 | value, startPos = decode(s,startPos) | |
| 245 | object[key]=value | |
| 246 | until false -- infinite loop while key-value pairs are found | |
| 247 | end | |
| 248 | ||
| 249 | function decode_scanString(s,startPos) | |
| 250 | base.assert(startPos, 'decode_scanString(..) called without start position') | |
| 251 | local startChar = string.sub(s,startPos,startPos) | |
| 252 | base.assert(startChar==[[']] or startChar==[["]],'decode_scanString called for a non-string') | |
| 253 | local escaped = false | |
| 254 | local endPos = startPos + 1 | |
| 255 | local bEnded = false | |
| 256 | local stringLen = string.len(s) | |
| 257 | repeat | |
| 258 | local curChar = string.sub(s,endPos,endPos) | |
| 259 | -- Character escaping is only used to escape the string delimiters | |
| 260 | if not escaped then | |
| 261 | if curChar==[[\]] then | |
| 262 | escaped = true | |
| 263 | else | |
| 264 | bEnded = curChar==startChar | |
| 265 | end | |
| 266 | else | |
| 267 | -- If we're escaped, we accept the current character come what may | |
| 268 | escaped = false | |
| 269 | end | |
| 270 | endPos = endPos + 1 | |
| 271 | base.assert(endPos <= stringLen+1, "String decoding failed: unterminated string at position " .. endPos) | |
| 272 | until bEnded | |
| 273 | local stringValue = 'return ' .. string.sub(s, startPos, endPos-1) | |
| 274 | local stringEval = base.loadstring(stringValue) | |
| 275 | base.assert(stringEval, 'Failed to load string [ ' .. stringValue .. '] in JSON4Lua.decode_scanString at position ' .. startPos .. ' : ' .. endPos) | |
| 276 | return stringEval(), endPos | |
| 277 | end | |
| 278 | ||
| 279 | function decode_scanWhitespace(s,startPos) | |
| 280 | local whitespace=" \n\r\t" | |
| 281 | local stringLen = string.len(s) | |
| 282 | while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do | |
| 283 | startPos = startPos + 1 | |
| 284 | end | |
| 285 | return startPos | |
| 286 | end | |
| 287 | ||
| 288 | function encodeString(s) | |
| 289 | s = string.gsub(s,'\\','\\\\') | |
| 290 | s = string.gsub(s,'"','\\"') | |
| 291 | s = string.gsub(s,'\n','\\n') | |
| 292 | s = string.gsub(s,'\t','\\t') | |
| 293 | return s | |
| 294 | end | |
| 295 | ||
| 296 | function isArray(t) | |
| 297 | -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable | |
| 298 | -- (with the possible exception of 'n') | |
| 299 | local maxIndex = 0 | |
| 300 | for k,v in base.pairs(t) do | |
| 301 | if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair | |
| 302 | if (not isEncodable(v)) then return false end -- All array elements must be encodable | |
| 303 | maxIndex = math.max(maxIndex,k) | |
| 304 | else | |
| 305 | if (k=='n') then | |
| 306 | if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements | |
| 307 | else -- Else of (k=='n') | |
| 308 | if isEncodable(v) then return false end | |
| 309 | end -- End of (k~='n') | |
| 310 | end -- End of k,v not an indexed pair | |
| 311 | end -- End of loop across all pairs | |
| 312 | return true, maxIndex | |
| 313 | end | |
| 314 | ||
| 315 | function isEncodable(o) | |
| 316 | local t = base.type(o) | |
| 317 | return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) | |
| 318 | end | |
| 319 | ||
| 320 | -------------------------------------------------------------------------------- | |
| 321 | -------------------------------------------------------------------------------- | |
| 322 | ||
| 323 | -- ComputerCraft IRC client code begins here | |
| 324 | ||
| 325 | -------------------------------------------------------------------------------- | |
| 326 | -------------------------------------------------------------------------------- | |
| 327 | ||
| 328 | local defaultNick = "cc-irc-client"..tostring(math.random(1,1000)) | |
| 329 | local nick = defaultNick | |
| 330 | local password = nil | |
| 331 | local newNick = "" | |
| 332 | ||
| 333 | local counter = 0 -- used by the qwebirc protocol for something | |
| 334 | local sessionID = "" -- as is this, although a bit more self evident | |
| 335 | ||
| 336 | local currentChannel = "status" | |
| 337 | local quitReason = "" | |
| 338 | ||
| 339 | local BUFFERSIZE = 100 -- keep this many lines in each windows history | |
| 340 | local offset = 0 -- for scrolling, not implemented | |
| 341 | ||
| 342 | local w,h = term.getSize() | |
| 343 | local legacy = true --(window == nil) CC v < 1.6 doesn't have the window API which would be useful, but maybe later | |
| 344 | local windows = {}
| |
| 345 | local winnumbers = {}
| |
| 346 | local seen = {}
| |
| 347 | if not legacy then | |
| 348 | local origTerm = term.current() | |
| 349 | windows["input"] = window.create(origTerm,1,h-1,w,2) | |
| 350 | windows["status"] = window.create(origTerm,1,1,w,h-2) | |
| 351 | windows["current"] = windows["status"] | |
| 352 | else | |
| 353 | windows["status"] = {}
| |
| 354 | windows["current"] = windows["status"] | |
| 355 | end | |
| 356 | table.insert(winnumbers,"status") | |
| 357 | seen[windows["status"]] = true | |
| 358 | local lock = "off" | |
| 359 | ||
| 360 | local win2num = function(win) | |
| 361 | for k,v in pairs(winnumbers) do | |
| 362 | if v == win then return k end | |
| 363 | end | |
| 364 | return nil | |
| 365 | end | |
| 366 | ||
| 367 | --os.loadAPI("json")
| |
| 368 | ||
| 369 | local post = function(url,data) | |
| 370 | local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed... | |
| 371 | local resp = http.post(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data) | |
| 372 | counter = counter + 1 | |
| 373 | return resp | |
| 374 | end | |
| 375 | ||
| 376 | -- better responsiveness with asynchronous methods as we usually let the recv couroutine handle the responses | |
| 377 | local request = function(url,data) | |
| 378 | local cacheAvoidance = "abc"..tostring(math.random(0,10000)) -- not sure if this is needed... | |
| 379 | http.request(baseUrl..dynamicUrl.."e/"..url,"r="..cacheAvoidance.."&t="..tostring(counter)..data) | |
| 380 | counter = counter + 1 | |
| 381 | end | |
| 382 | ||
| 383 | -- these special URLs are used by the webchat server for different methods | |
| 384 | local send = function(data) | |
| 385 | return request("p","&s="..sessionID.."&c="..textutils.urlEncode(data))
| |
| 386 | end | |
| 387 | ||
| 388 | local recv = function() | |
| 389 | return post("s","&s="..sessionID)
| |
| 390 | end | |
| 391 | ||
| 392 | local connect = function(password) | |
| 393 | if password ~= nil then | |
| 394 | return post("n","&nick="..nick.."&password="..password)
| |
| 395 | else | |
| 396 | return post("n","&nick="..nick)
| |
| 397 | end | |
| 398 | end | |
| 399 | ||
| 400 | -- some helper functions | |
| 401 | local pong = function(data) | |
| 402 | return send("PONG :"..data)
| |
| 403 | end | |
| 404 | ||
| 405 | local quit = function(_reason) | |
| 406 | local reason = _reason or "" | |
| 407 | return send("QUIT :"..reason)
| |
| 408 | end | |
| 409 | ||
| 410 | -- lua default string methods suck :/ | |
| 411 | local split = function(str,sep) | |
| 412 | local sep, fields = sep or ":", {}
| |
| 413 | local pattern = string.format("([^%s]+)", sep)
| |
| 414 | str:gsub(pattern, function(c) fields[#fields+1] = c end) | |
| 415 | return fields | |
| 416 | end | |
| 417 | ||
| 418 | local exit = function(reason) | |
| 419 | local r = reason or "" | |
| 420 | term.clear() | |
| 421 | term.setCursorPos(1,1) | |
| 422 | print(r) | |
| 423 | end | |
| 424 | ||
| 425 | -- for debug | |
| 426 | local writeLine | |
| 427 | writeLine = function(data,line) | |
| 428 | if not (type(data) == "table") then | |
| 429 | return tostring(data) | |
| 430 | end | |
| 431 | for i=1,#data do | |
| 432 | if type(data[i]) == "table" then | |
| 433 | line = writeLine(data[i],line) | |
| 434 | elseif data[i] ~= nick then | |
| 435 | line = line..data[i].." " | |
| 436 | end | |
| 437 | end | |
| 438 | return line | |
| 439 | end | |
| 440 | ||
| 441 | -- some IRC protocol codes we handle "properly" | |
| 442 | local codes = {}
| |
| 443 | codes["371"] = "RPL_INFO" | |
| 444 | codes["374"] = "RPL_ENDINFO" | |
| 445 | codes["375"] = "RPL_MOTDSTART" | |
| 446 | codes["372"] = "RPL_MOTD" | |
| 447 | codes["376"] = "RPL_ENDOFMOTD" | |
| 448 | codes["352"] = "RPL_WHOREPLY" | |
| 449 | ||
| 450 | -- add lines to window | |
| 451 | local writeToWin = function(win, s) | |
| 452 | - | if win ~= windows["current"] then |
| 452 | + | print(s) |
| 453 | - | seen[win] = false |
| 453 | + | |
| 454 | ||
| 455 | -- helper | |
| 456 | - | while #s > w do |
| 456 | + | |
| 457 | - | table.insert(win,s:sub(1,w)) |
| 457 | + | |
| 458 | - | s = s:sub(w+1) |
| 458 | + | |
| 459 | end | |
| 460 | - | table.insert(win,s) |
| 460 | + | |
| 461 | - | while #win > BUFFERSIZE do |
| 461 | + | |
| 462 | - | table.remove(win,1) |
| 462 | + | |
| 463 | local drawWin = function(win) | |
| 464 | if legacy then | |
| 465 | local x,y = term.getCursorPos() | |
| 466 | for i = 2,h-2 do | |
| 467 | term.setCursorPos(1,i) | |
| 468 | term.clearLine() | |
| 469 | end | |
| 470 | if #win > 0 then | |
| 471 | local i = math.max(1,#win-h+4) | |
| 472 | iend = #win | |
| 473 | local row = 2 | |
| 474 | while i <= iend do | |
| 475 | term.setCursorPos(1,row) | |
| 476 | local line = win[i] | |
| 477 | --[[ | |
| 478 | local n = 0 | |
| 479 | while #line > w do | |
| 480 | term.write(line:sub(1,w)) | |
| 481 | row = row + 1 | |
| 482 | n = n + 1 | |
| 483 | term.setCursorPos(1,row) | |
| 484 | line = line:sub(w+1) | |
| 485 | end | |
| 486 | ]]-- | |
| 487 | term.write(line) | |
| 488 | row = row + 1 | |
| 489 | i = i + 1 | |
| 490 | end | |
| 491 | end | |
| 492 | term.setCursorPos(x,y) | |
| 493 | end | |
| 494 | end | |
| 495 | ||
| 496 | -- draw the separator with list of windows and indicators of activity | |
| 497 | ||
| 498 | local drawSeparator = function() | |
| 499 | if legacy then | |
| 500 | local x,y = term.getCursorPos() | |
| 501 | local nwin = #winnumbers | |
| 502 | --local left = math.floor(0.5*(w-2*nwin+1)) | |
| 503 | --local right = w-left-2*nwin-1 | |
| 504 | term.setCursorPos(1,h-1) | |
| 505 | term.write(string.rep("-",w))
| |
| 506 | local newcount = 0 | |
| 507 | for i = 1,nwin do | |
| 508 | if not seen[windows[winnumbers[i]]] then | |
| 509 | newcount = newcount + 1 | |
| 510 | end | |
| 511 | end | |
| 512 | local start = math.ceil(0.5*(w-(2*nwin+1+newcount))) | |
| 513 | term.setCursorPos(start,h-1) | |
| 514 | for i=1,nwin do | |
| 515 | term.write(" ")
| |
| 516 | if winnumbers[i] == currentChannel then | |
| 517 | term.setBackgroundColor(colors.white) | |
| 518 | term.setTextColor(colors.black) | |
| 519 | end | |
| 520 | term.write(string.format("%d",i))
| |
| 521 | if not seen[windows[winnumbers[i]]] then | |
| 522 | term.write("+")
| |
| 523 | end | |
| 524 | term.setBackgroundColor(colors.black) | |
| 525 | term.setTextColor(colors.white) | |
| 526 | end | |
| 527 | term.write(" ")
| |
| 528 | term.setCursorPos(x,y) | |
| 529 | end | |
| 530 | end | |
| 531 | ||
| 532 | -- top banner with channel name | |
| 533 | local drawBanner = function() | |
| 534 | local x,y = term.getCursorPos() | |
| 535 | term.setCursorPos(1,1) | |
| 536 | term.clearLine() | |
| 537 | --local banner = "["..currentChannel.."]" | |
| 538 | local banner = currentChannel | |
| 539 | local start = math.ceil(0.5*(w-#banner)) | |
| 540 | term.setCursorPos(start,1) | |
| 541 | term.write(banner) | |
| 542 | term.setCursorPos(x,y) | |
| 543 | end | |
| 544 | ||
| 545 | local drawInput = function() | |
| 546 | if legacy then | |
| 547 | term.setCursorPos(1,h) | |
| 548 | term.clearLine() | |
| 549 | term.write("> ")
| |
| 550 | end | |
| 551 | end | |
| 552 | ||
| 553 | local handleResponse = function(data) | |
| 554 | for i = 1, #data do | |
| 555 | local id = data[i][2] | |
| 556 | if id == "PING" then | |
| 557 | pong(data[i][4][1]) | |
| 558 | elseif id == "PRIVMSG" then | |
| 559 | local senderDetails = data[i][3] | |
| 560 | local sender = senderDetails:sub(1,senderDetails:find("!")-1)
| |
| 561 | local channel = data[i][4][1]:lower() | |
| 562 | --print(textutils.serialize(data[i])) | |
| 563 | --print(channel) | |
| 564 | if channel == nick then | |
| 565 | channel = sender | |
| 566 | if not windows[sender] then | |
| 567 | if legacy then | |
| 568 | windows[channel] = {}
| |
| 569 | end | |
| 570 | table.insert(winnumbers,channel) | |
| 571 | seen[windows[channel]] = false | |
| 572 | end | |
| 573 | end | |
| 574 | local msg = data[i][4][2] | |
| 575 | if legacy then | |
| 576 | writeToWin(windows[channel],"<"..sender.."> "..msg) | |
| 577 | end | |
| 578 | --print("<"..sender.."> "..msg)
| |
| 579 | elseif id == "NICK" then | |
| 580 | if newNick ~= "" then | |
| 581 | local name = data[i][3]:sub(1,data[i][3]:find("!")-1)
| |
| 582 | if name == nick then | |
| 583 | nick = newNick | |
| 584 | newNick = "" | |
| 585 | end | |
| 586 | end | |
| 587 | elseif id == "433" then | |
| 588 | writeToWin(windows["status"],"Nickname already in use!") | |
| 589 | newNick = "" | |
| 590 | elseif codes[id] then | |
| 591 | if legacy then | |
| 592 | writeToWin(windows["status"],writeLine(data[i][4],"")) | |
| 593 | --print(data[i][4][2]) | |
| 594 | end | |
| 595 | --print(data[i][4][2]) | |
| 596 | else | |
| 597 | errormsg(writeLine(data[i],"")) | |
| 598 | end | |
| 599 | end | |
| 600 | end | |
| 601 | ||
| 602 | commands = {}
| |
| 603 | commands["join"] = function(input) | |
| 604 | if not input[2] then | |
| 605 | errormsg("No channel specified!")
| |
| 606 | return true | |
| 607 | end | |
| 608 | local channel = input[2]:lower() | |
| 609 | if channel:sub(1,1) ~= "#" then | |
| 610 | errormsg("Invalid channel name!")
| |
| 611 | return true | |
| 612 | end | |
| 613 | currentChannel = channel | |
| 614 | if legacy then | |
| 615 | windows[channel] = {}
| |
| 616 | end | |
| 617 | windows["current"] = windows[currentChannel] | |
| 618 | table.insert(winnumbers,channel) | |
| 619 | seen[windows[channel]] = true | |
| 620 | send("JOIN "..channel)
| |
| 621 | return true | |
| 622 | end | |
| 623 | ||
| 624 | commands["who"] = function(input) | |
| 625 | local channel = input[2] | |
| 626 | send("WHO "..channel)
| |
| 627 | return true | |
| 628 | end | |
| 629 | ||
| 630 | commands["whois"] = function(input) | |
| 631 | local user = input[2] | |
| 632 | send("WHOIS "..user)
| |
| 633 | return true | |
| 634 | end | |
| 635 | ||
| 636 | commands["part"] = function(input) | |
| 637 | local channel = currentChannel | |
| 638 | if channel == "status" then | |
| 639 | errormsg("Can't part status window!")
| |
| 640 | else | |
| 641 | local nwin | |
| 642 | for i,v in pairs(winnumbers) do | |
| 643 | if v == channel then | |
| 644 | nwin = i | |
| 645 | break | |
| 646 | end | |
| 647 | end | |
| 648 | table.remove(winnumbers,nwin) | |
| 649 | seen[windows[channel]] = nil | |
| 650 | windows[channel] = nil | |
| 651 | currentChannel = winnumbers[#winnumbers] | |
| 652 | print(currentChannel) | |
| 653 | windows["current"] = windows[currentChannel] | |
| 654 | seen[windows["current"]] = true | |
| 655 | if channel:sub(1,1) == "#" then | |
| 656 | send("PART "..channel)
| |
| 657 | end | |
| 658 | end | |
| 659 | return true | |
| 660 | end | |
| 661 | ||
| 662 | commands["quit"] = function(input) | |
| 663 | quit(input[2]) | |
| 664 | return false | |
| 665 | end | |
| 666 | ||
| 667 | commands["window"] = function(input) | |
| 668 | local nwin = tonumber(input[2]) | |
| 669 | if not nwin then | |
| 670 | errormsg("Invalid window number!")
| |
| 671 | elseif nwin > #winnumbers or nwin < 1 then | |
| 672 | errormsg("Invalid window number!")
| |
| 673 | else | |
| 674 | windows["current"] = windows[winnumbers[nwin]] | |
| 675 | seen[windows["current"]] = true | |
| 676 | currentChannel = winnumbers[nwin] | |
| 677 | end | |
| 678 | return true | |
| 679 | end | |
| 680 | ||
| 681 | commands["nick"] = function(input) | |
| 682 | local nickname = input[2] | |
| 683 | if nickname then | |
| 684 | send("NICK "..nickname)
| |
| 685 | newNick = nickname | |
| 686 | writeToWin(windows["status"],"Changed nick to: "..newNick) | |
| 687 | else | |
| 688 | errormsg("No nickname given.")
| |
| 689 | end | |
| 690 | return true | |
| 691 | end | |
| 692 | ||
| 693 | commands["query"] = function(input) | |
| 694 | local user = input[2] | |
| 695 | if user then | |
| 696 | currentChannel = user | |
| 697 | if legacy then | |
| 698 | windows[user] = {}
| |
| 699 | end | |
| 700 | windows["current"] = windows[currentChannel] | |
| 701 | table.insert(winnumbers,user) | |
| 702 | seen[windows[user]] = true | |
| 703 | --send("JOIN "..channel)
| |
| 704 | else | |
| 705 | errormsg("No username given.")
| |
| 706 | end | |
| 707 | return true | |
| 708 | end | |
| 709 | ||
| 710 | commands["help"] = function(input) | |
| 711 | writeToWin(windows["status"],"Available commands:") | |
| 712 | for k,v in pairs(commands) do | |
| 713 | writeToWin(windows["status"],"- "..k) | |
| 714 | end | |
| 715 | return true | |
| 716 | end | |
| 717 | ||
| 718 | local alias = {}
| |
| 719 | alias["w"] = "window" | |
| 720 | alias["j"] = "join" | |
| 721 | alias["p"] = "part" | |
| 722 | alias["q"] = "quit" | |
| 723 | alias["exit"] = "quit" | |
| 724 | alias["e"] = "quit" | |
| 725 | alias["n"] = "nick" | |
| 726 | alias["qr"] = "query" | |
| 727 | alias["h"] = "help" | |
| 728 | ||
| 729 | ||
| 730 | local changeWindow = function(nwin) | |
| 731 | windows["current"] = windows[winnumbers[nwin]] | |
| 732 | seen[windows["current"]] = true | |
| 733 | currentChannel = winnumbers[nwin] | |
| 734 | return true | |
| 735 | end | |
| 736 | ||
| 737 | local handeInput = function(input) | |
| 738 | if not input then return true end | |
| 739 | if input:sub(1,1) == "/" then | |
| 740 | input = split(input," ") | |
| 741 | local cmd = input[1]:sub(2) | |
| 742 | if commands[cmd] then | |
| 743 | return commands[cmd](input) | |
| 744 | elseif alias[cmd] then | |
| 745 | return commands[alias[cmd]](input) | |
| 746 | else | |
| 747 | errormsg("Invalid command!")
| |
| 748 | end | |
| 749 | elseif currentChannel ~= "status" then | |
| 750 | send("PRIVMSG "..currentChannel.." :"..input)
| |
| 751 | writeToWin(windows["current"],"<"..nick.."> "..input) | |
| 752 | else | |
| 753 | writeToWin(windows["status"],input) | |
| 754 | end | |
| 755 | return true | |
| 756 | end | |
| 757 | ||
| 758 | local keys = {}
| |
| 759 | ||
| 760 | -- arrow keys cause problems with read() :/ | |
| 761 | ||
| 762 | --[[ | |
| 763 | keys[203] = function() -- LEFT | |
| 764 | local n = win2num(currentChannel) | |
| 765 | if n > 1 then | |
| 766 | changeWindow(n-1) | |
| 767 | end | |
| 768 | return true | |
| 769 | end | |
| 770 | ||
| 771 | keys[205] = function() -- RIGHT | |
| 772 | local n = win2num(currentChannel) | |
| 773 | if n < #winnumbers then | |
| 774 | changeWindow(n+1) | |
| 775 | end | |
| 776 | return true | |
| 777 | end | |
| 778 | ]]-- | |
| 779 | ||
| 780 | -- TAB to cycle through windows | |
| 781 | keys[15] = function() | |
| 782 | local n = win2num(currentChannel) | |
| 783 | if n < #winnumbers then | |
| 784 | changeWindow(n+1) | |
| 785 | else | |
| 786 | changeWindow(1) | |
| 787 | end | |
| 788 | return true | |
| 789 | end | |
| 790 | ||
| 791 | local receive = function() | |
| 792 | resp = recv() | |
| 793 | while resp do | |
| 794 | _data = resp.readAll() | |
| 795 | if #_data > 0 then | |
| 796 | data = decode(_data) | |
| 797 | handleResponse(data) | |
| 798 | while lock == "on" do | |
| 799 | sleep(0.1) | |
| 800 | end | |
| 801 | lock = "on" | |
| 802 | drawBanner() | |
| 803 | drawWin(windows["current"]) | |
| 804 | drawSeparator() | |
| 805 | --drawInput() | |
| 806 | lock = "off" | |
| 807 | end | |
| 808 | resp = recv() | |
| 809 | end | |
| 810 | quitReason = "Disconnected!" | |
| 811 | end | |
| 812 | ||
| 813 | local interface = function() | |
| 814 | input = read() | |
| 815 | while handeInput(input) do | |
| 816 | while lock == "on" do | |
| 817 | sleep(0.1) | |
| 818 | end | |
| 819 | lock = "on" | |
| 820 | drawBanner() | |
| 821 | drawWin(windows["current"]) | |
| 822 | drawSeparator() | |
| 823 | drawInput() | |
| 824 | lock = "off" | |
| 825 | input = read() | |
| 826 | end | |
| 827 | quitReason = "Bye!" | |
| 828 | end | |
| 829 | ||
| 830 | local specialKeys = function() | |
| 831 | while true do | |
| 832 | local ev, key = os.pullEvent("key")
| |
| 833 | if keys[key] then | |
| 834 | if not keys[key]() then break end | |
| 835 | while lock == "on" do | |
| 836 | sleep(0.1) | |
| 837 | end | |
| 838 | lock = "on" | |
| 839 | drawBanner() | |
| 840 | drawWin(windows["current"]) | |
| 841 | drawSeparator() | |
| 842 | --drawInput() | |
| 843 | lock = "off" | |
| 844 | end | |
| 845 | end | |
| 846 | quitReason = "Bye!" | |
| 847 | end | |
| 848 | ||
| 849 | local args = {...}
| |
| 850 | if #args > 0 then | |
| 851 | if args[1] == "help" then | |
| 852 | print("Usage: irc [nick] [password]")
| |
| 853 | print("Edit the file to change servers.")
| |
| 854 | return | |
| 855 | else | |
| 856 | nick = args[1] | |
| 857 | password = args[2] | |
| 858 | end | |
| 859 | end | |
| 860 | drawBanner() | |
| 861 | drawWin(windows["current"]) | |
| 862 | drawSeparator() | |
| 863 | drawInput() | |
| 864 | errormsg("Connecting...")
| |
| 865 | resp = connect(password) | |
| 866 | if not resp then exit("Unable to connect!") return end
| |
| 867 | _data = resp.readAll() | |
| 868 | data = decode(_data) | |
| 869 | sessionID = data[2] | |
| 870 | errormsg("Got sessionID: "..sessionID)
| |
| 871 | --sleep(1) | |
| 872 | parallel.waitForAny(receive,interface,specialKeys) | |
| 873 | exit(quitReason) |