2 -- Gaim Citadel plugin.
5 -- This code is licensed under the GPL v2. See the file COPYING in this
6 -- directory for the full license text.
8 -- $Id:citadel.lua 4326 2006-02-18 12:26:22Z hjalfi $
10 -----------------------------------------------------------------------------
12 -----------------------------------------------------------------------------
15 local username, servername, port
22 -----------------------------------------------------------------------------
24 -----------------------------------------------------------------------------
26 -- Our version number. Remember to update!
28 local VERSION_NUMBER = "0.3"
30 -- Special values returned as Citadel's response codes.
32 local LISTING_FOLLOWS = 100
35 local SEND_LISTING = 400
37 local BINARY_FOLLOWS = 600
38 local SEND_BINARY = 700
39 local START_CHAT_MODE = 800
41 local INTERNAL_ERROR = 10
43 local ILLEGAL_VALUE = 12
44 local NOT_LOGGED_IN = 20
45 local CMD_NOT_SUPPORTED = 30
46 local PASSWORD_REQUIRED = 40
47 local ALREADY_LOGGED_IN = 41
48 local USERNAME_REQUIRED = 42
49 local HIGHER_ACCESS_REQUIRED = 50
50 local MAX_SESSIONS_EXCEEDED = 51
51 local RESOURCE_BUSY = 52
52 local RESOURCE_NOT_OPEN = 53
54 local INVALID_FLOOR_OPERATION = 61
55 local NO_SUCH_USER = 70
56 local FILE_NOT_FOUND = 71
57 local ROOM_NOT_FOUND = 72
58 local NO_SUCH_SYSTEM = 73
59 local ALREADY_EXISTS = 74
60 local MESSAGE_NOT_FOUND = 75
65 -- Other Citadel settings.
67 local CITADEL_DEFAULT_PORT = 504
68 local CITADEL_CONFIG_ROOM = "My Citadel Config"
69 local WAITING_ROOM = "Sent/Received Pages"
70 local CITADEL_BUDDY_MSG = "__ Buddy List __"
71 local CITADEL_POLL_INTERVAL = 60
73 -----------------------------------------------------------------------------
75 -----------------------------------------------------------------------------
77 --local stderr = io.stderr
79 local function log(...)
81 for _, i in ipairs(arg) do
82 table.insert(s, tostring(i))
85 gaim_debug_info("citadel", (string.gsub(s, "%%", "%%")))
88 local function unexpectederror()
89 error("The Citadel server said something unexpected. Giving up.")
92 local function warning(...)
94 for _, i in ipairs(arg) do
95 table.insert(s, tostring(i))
97 gaim_connection_notice(gc, s)
100 local olderror = error
103 log("traceback: ", debug.traceback())
107 -----------------------------------------------------------------------------
109 -----------------------------------------------------------------------------
113 local inscheduler = false
115 local yield = coroutine.yield
117 local function schedule_now()
118 if not inscheduler then
121 while taskqueue[1] do
122 -- Pull the first task off the queue, creating it if necessary.
124 local task = taskqueue[1]
125 if (type(task) == "function") then
126 task = coroutine.create(task)
132 local s, e = coroutine.resume(task)
135 log("traceback: ", debug.traceback())
136 gaim_connection_error(gc, e)
139 -- If it's not dead, then it must have yielded --- return back to C.
141 if (coroutine.status(task) ~= "dead") then
145 -- Otherwise, remove it from the queue and go again.
147 table.remove(taskqueue, 1)
154 local function queue(func)
155 table.insert(taskqueue, func)
157 table.insert(taskqueue, function()
158 local i, e = pcall(func)
160 log("coroutine died with error! ", e)
161 gaim_connection_error(gc, e)
168 local function lazyqueue(func)
169 if not queued[func] then
179 -----------------------------------------------------------------------------
181 -----------------------------------------------------------------------------
183 local inputbuffer = ""
185 -- Read a single line of text from the server, maing Lua's coroutines do the
186 -- vast bulk of the work of managing Gaim's state machine for us. Woo!
188 local function readline()
189 -- Always yield at least once. Otherwise, Lua hogs all the CPU time.
195 -- Read some data from the remote server, if any's
198 local i = interface_readdata(fd, gsc)
200 error("Unexpected disconnection from Citadel server")
203 inputbuffer = inputbuffer..i
205 -- Have we read a complete line of text?
207 local s, e, l = string.find(inputbuffer, "^([^\n]*)\n")
211 inputbuffer = string.sub(inputbuffer, e+1)
216 -- Otherwise, wait some more.
222 local function unpack_citadel_data_line(s, a)
224 for i in string.gfind(s, "([^|]*)|?") do
230 -- Read in an parse a packet from the Citadel server.
232 local function get_response()
235 -- The first line of a message is of the format:
236 -- 123 String|String|String
238 -- The 123 is a response code.
241 message.response = tonumber(string.sub(s, 1, 3))
244 unpack_citadel_data_line(s, message)
246 -- If the response code is LISTING_FOLLOWS, then there's more data
249 if (message.response == LISTING_FOLLOWS) then
257 --log("Got xarg: ", s)
258 table.insert(message.xargs, s)
262 -- If the response code is BINARY_FOLLOWS, there's a big binary chunk
263 -- coming --- which we don't support.
265 if (message.response == BINARY_FOLLOWS) then
266 error("Server sent a binary chunk, which we don't support yet")
272 -----------------------------------------------------------------------------
273 -- OUTPUT MANGLING --
274 -----------------------------------------------------------------------------
276 local function writeline(...)
277 local s = table.concat(arg)
280 interface_writedata(fd, gsc, s)
281 interface_writedata(fd, gsc, "\n")
284 -----------------------------------------------------------------------------
285 -- PRESENCE MANAGEMENT --
286 -----------------------------------------------------------------------------
288 local function cant_save_buddy_list()
289 warning("Unable to send buddy list to server.")
292 local function save_buddy_list()
293 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
294 local m = get_response()
295 if (m.response ~= CIT_OK) then
296 cant_save_buddy_list()
300 -- Search and destroy any old buddy list.
302 writeline("MSGS ALL|0|1")
304 if (m.response ~= START_CHAT_MODE) then
305 cant_save_buddy_list()
309 writeline("subj|"..CITADEL_BUDDY_MSG)
317 if (not m) and (s ~= "000") then
323 writeline("DELE "..m)
325 if (m.response ~= CIT_OK) then
326 cant_save_buddy_list()
331 -- Save our buddy list.
333 writeline("ENT0 1||0|1|"..CITADEL_BUDDY_MSG.."|")
335 if (m.response ~= SEND_LISTING) then
336 cant_save_buddy_list()
340 for name, _ in pairs(buddies) do
341 local b = gaim_find_buddy(ga, name)
343 local alias = gaim_buddy_get_alias(b) or ""
344 local group = gaim_find_buddys_group(b)
345 local groupname = gaim_group_get_name(group)
346 writeline(name.."|"..alias.."|"..groupname)
351 -- Go back to the lobby.
353 writeline("GOTO "..WAITING_ROOM)
357 local function update_buddy_status()
359 local m = get_response()
360 if (m.response ~= LISTING_FOLLOWS) then
363 log("attempting to scan and update buddies")
365 local onlinebuddies = {}
366 for _, s in ipairs(m.xargs) do
367 local name = unpack_citadel_data_line(s)[2]
368 if (name ~= "(not logged in)") then
369 onlinebuddies[name] = true
373 -- Anyone who's not online is offline.
375 for s, _ in pairs(buddies) do
376 if not onlinebuddies[s] then
377 serv_got_update(gc, s, false, 0, 0, 0, 0)
381 -- Anyone who's online is, er, online.
383 for s, _ in pairs(onlinebuddies) do
384 -- If we're in no-buddy-list mode and this buddy isn't on our
385 -- list, add them automatically.
388 if not gaim_find_buddy(ga, s) then
389 local buddy = gaim_buddy_new(ga, s, s)
390 local group = gaim_group_new("Citadel")
392 -- buddy is not garbage collected! This must succeed!
393 gaim_blist_add_buddy(buddy, nil, group, nil)
398 serv_got_update(gc, s, true, 0, 0, 0, 0)
402 -----------------------------------------------------------------------------
404 -----------------------------------------------------------------------------
406 function citadel_schedule_now()
410 function citadel_input()
411 -- If there's no task, create one to handle this input.
413 if not taskqueue[1] then
418 function citadel_setfd(_fd)
420 log("fd = ", tonumber(fd))
423 function citadel_setgsc(_gsc)
424 gsc = tolua.cast(_gsc, "GaimSslConnection")
425 log("gsc registered")
428 function citadel_connect(_ga)
429 ga = tolua.cast(_ga, "GaimAccount")
430 gc = gaim_account_get_connection(ga)
435 username = gaim_account_get_username(ga)
436 _, _, username, servername = string.find(username, "^(.*)@(.*)$")
437 port = gaim_account_get_int(ga, "port", CITADEL_DEFAULT_PORT)
438 noblist = gaim_account_get_bool(ga, "no_blist", false)
440 log("connect to ", username, " on server ", servername, " port ", port)
444 gaim_connection_update_progress(gc, "Connecting", 1, STEPS)
445 local i = interface_connect(ga, gc, servername, port)
447 error("Unable to create socket")
450 local m = get_response()
451 if (m.response ~= CIT_OK) then
452 error("Unexpected response from server")
455 -- Switch to TLS mode, if desired.
457 if gaim_account_get_bool(ga, "use_tls", true) then
458 gaim_connection_update_progress(gc, "Requesting TLS", 2, STEPS)
461 if (m.response ~= 200) then
462 error("This Citadel server does not support TLS.")
465 -- This will always work. If the handshake fails, Lua will be
466 -- shot and we don't need to worry about cleaning up.
468 gaim_connection_update_progress(gc, "TLS handshake", 3, STEPS)
469 interface_tlson(gc, ga, fd)
471 -- Wait for the gsc to be hooked up.
480 gaim_connection_update_progress(gc, "Sending username", 4, STEPS)
481 writeline("USER "..username)
483 if (m.response == (ERROR+NO_SUCH_USER)) then
484 error("There is no user with name '", username, "' on this server.")
486 if (m.response ~= MORE_DATA) then
492 gaim_connection_update_progress(gc, "Sending password", 5, STEPS)
493 writeline("PASS "..gaim_account_get_password(ga))
495 if (m.response ~= CIT_OK) then
496 error("Incorrect password.")
499 -- Tell Citadel who we are.
501 gaim_connection_update_progress(gc, "Setting up", 6, STEPS)
502 writeline("IDEN 226|0|"..VERSION_NUMBER.."|Gaim Citadel plugin|")
505 -- Set asynchronous mode.
507 gaim_connection_update_progress(gc, "Setting up", 7, STEPS)
510 if (m.response ~= CIT_OK) then
511 error("This Citadel server does not support instant messaging.")
515 -- Switch to private configuration room.
517 gaim_connection_update_progress(gc, "Setting up", 8, STEPS)
518 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
520 if (m.response ~= CIT_OK) then
521 warning("Unable to fetch buddy list from server.")
525 -- Look for our preferences.
527 gaim_connection_update_progress(gc, "Setting up", 9, STEPS)
528 writeline("MSGS ALL|0|1")
530 if (m.response ~= START_CHAT_MODE) then
531 warning("Unable to fetch buddy list from server.")
535 writeline("subj|"..CITADEL_BUDDY_MSG)
543 if (not m) and (s ~= "000") then
548 log("preference message in #", m)
553 gaim_connection_update_progress(gc, "Setting up", 10, STEPS)
554 writeline("MSG0 "..m)
560 if (s == "text") then
570 local name, alias, groupname = unpack(unpack_citadel_data_line(s))
571 if not gaim_find_buddy(ga, name) then
572 local buddy = gaim_buddy_new(ga, name, alias)
573 local group = gaim_group_new(groupname)
574 log("adding new buddy ", name)
576 -- buddy is not garbage collected! This must succeed!
577 gaim_blist_add_buddy(buddy, nil, group, nil)
583 -- Update buddy list with who's online.
585 gaim_connection_update_progress(gc, "Setting up", 11, STEPS)
586 update_buddy_status()
588 -- Go back to the Lobby.
590 gaim_connection_update_progress(gc, "Setting up", 12, STEPS)
591 writeline("GOTO "..WAITING_ROOM)
594 -- Switch on the timer.
596 timerhandle = interface_timeron(gc,
597 gaim_account_get_int(ga, "interval", CITADEL_POLL_INTERVAL)*1000)
601 gaim_connection_update_progress(gc, "Connected", 13, STEPS)
602 gaim_connection_set_state(gc, GAIM_CONNECTED)
606 function citadel_close()
607 interface_disconnect(fd or -1, gsc)
609 interface_timeroff(gc, timerhandle)
611 schedule_now = function() end
614 function citadel_send_im(who, what, flags)
616 writeline("SEXP ", who, "|-")
617 local m = get_response()
618 if (m.response ~= SEND_LISTING) then
619 serv_got_im(gc, "Citadel", "Unable to send message", GAIM_MESSAGE_ERROR, 0);
627 function citadel_fetch_pending_messages()
631 local m = get_response()
632 if (m.response ~= LISTING_FOLLOWS) then
636 local s = table.concat(m.xargs)
637 --log("got message from ", m[4], " at ", m[2], ": ", s)
638 serv_got_im(gc, m[4], s, GAIM_MESSAGE_RECV, m[2])
643 function citadel_get_info(name)
645 writeline("RBIO "..name)
646 local m = get_response()
647 if (m.response ~= LISTING_FOLLOWS) then
648 m = "That user has been boojumed."
650 m = table.concat(m.xargs, "<br>")
653 gaim_notify_userinfo(gc, name, name.."'s biography",
654 name, "Biography", m, nil, nil)
658 -----------------------------------------------------------------------------
660 -----------------------------------------------------------------------------
662 function citadel_add_buddy(name)
663 if not buddies[name] then
665 lazyqueue(update_buddy_status)
666 lazyqueue(save_buddy_list)
670 function citadel_remove_buddy(name)
671 if buddies[name] then
673 lazyqueue(save_buddy_list)
677 function citadel_alias_buddy(name)
678 if buddies[name] then
679 lazyqueue(save_buddy_list)
683 function citadel_group_buddy(name, oldgroup, newgroup)
684 if buddies[name] then
685 lazyqueue(save_buddy_list)
689 function citadel_timer()
691 lazyqueue(update_buddy_status)
694 -----------------------------------------------------------------------------
696 -----------------------------------------------------------------------------
700 local m = get_response()
701 if (m.response == (ASYNC_MSG+ASYNC_GEXP)) then
702 citadel_fetch_pending_messages()