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 -- Special values returned as Citadel's response codes.
28 local LISTING_FOLLOWS = 100
31 local SEND_LISTING = 400
33 local BINARY_FOLLOWS = 600
34 local SEND_BINARY = 700
35 local START_CHAT_MODE = 800
37 local INTERNAL_ERROR = 10
39 local ILLEGAL_VALUE = 12
40 local NOT_LOGGED_IN = 20
41 local CMD_NOT_SUPPORTED = 30
42 local PASSWORD_REQUIRED = 40
43 local ALREADY_LOGGED_IN = 41
44 local USERNAME_REQUIRED = 42
45 local HIGHER_ACCESS_REQUIRED = 50
46 local MAX_SESSIONS_EXCEEDED = 51
47 local RESOURCE_BUSY = 52
48 local RESOURCE_NOT_OPEN = 53
50 local INVALID_FLOOR_OPERATION = 61
51 local NO_SUCH_USER = 70
52 local FILE_NOT_FOUND = 71
53 local ROOM_NOT_FOUND = 72
54 local NO_SUCH_SYSTEM = 73
55 local ALREADY_EXISTS = 74
56 local MESSAGE_NOT_FOUND = 75
61 -- Other Citadel settings.
63 local CITADEL_DEFAULT_PORT = 504
64 local CITADEL_CONFIG_ROOM = "My Citadel Config"
65 local WAITING_ROOM = "Sent/Received Pages"
66 local CITADEL_BUDDY_MSG = "__ Buddy List __"
67 local CITADEL_POLL_INTERVAL = 60
69 -----------------------------------------------------------------------------
71 -----------------------------------------------------------------------------
73 --local stderr = io.stderr
75 local function log(...)
77 for _, i in ipairs(arg) do
78 table.insert(s, tostring(i))
81 gaim_debug_info("citadel", (string.gsub(s, "%%", "%%")))
84 local function unexpectederror()
85 error("The Citadel server said something unexpected. Giving up.")
88 local function warning(...)
90 for _, i in ipairs(arg) do
91 table.insert(s, tostring(i))
93 gaim_connection_notice(gc, s)
96 local olderror = error
99 log("traceback: ", debug.traceback())
103 -----------------------------------------------------------------------------
105 -----------------------------------------------------------------------------
109 local inscheduler = false
111 local yield = coroutine.yield
113 local function schedule_now()
114 if not inscheduler then
117 while taskqueue[1] do
118 -- Pull the first task off the queue, creating it if necessary.
120 local task = taskqueue[1]
121 if (type(task) == "function") then
122 task = coroutine.create(task)
128 local s, e = coroutine.resume(task)
131 log("traceback: ", debug.traceback())
132 gaim_connection_error(gc, e)
135 -- If it's not dead, then it must have yielded --- return back to C.
137 if (coroutine.status(task) ~= "dead") then
141 -- Otherwise, remove it from the queue and go again.
143 table.remove(taskqueue, 1)
150 local function queue(func)
151 table.insert(taskqueue, func)
153 table.insert(taskqueue, function()
154 local i, e = pcall(func)
156 log("coroutine died with error! ", e)
157 gaim_connection_error(gc, e)
164 local function lazyqueue(func)
165 if not queued[func] then
175 -----------------------------------------------------------------------------
177 -----------------------------------------------------------------------------
179 local inputbuffer = ""
181 -- Read a single line of text from the server, maing Lua's coroutines do the
182 -- vast bulk of the work of managing Gaim's state machine for us. Woo!
184 local function readline()
185 -- Always yield at least once. Otherwise, Lua hogs all the CPU time.
191 -- Read some data from the remote server, if any's
194 local i = interface_readdata(fd, gsc)
196 error("Unexpected disconnection from Citadel server")
199 inputbuffer = inputbuffer..i
201 -- Have we read a complete line of text?
203 local s, e, l = string.find(inputbuffer, "^([^\n]*)\n")
207 inputbuffer = string.sub(inputbuffer, e+1)
212 -- Otherwise, wait some more.
218 local function unpack_citadel_data_line(s, a)
220 for i in string.gfind(s, "([^|]*)|?") do
226 -- Read in an parse a packet from the Citadel server.
228 local function get_response()
231 -- The first line of a message is of the format:
232 -- 123 String|String|String
234 -- The 123 is a response code.
237 message.response = tonumber(string.sub(s, 1, 3))
240 unpack_citadel_data_line(s, message)
242 -- If the response code is LISTING_FOLLOWS, then there's more data
245 if (message.response == LISTING_FOLLOWS) then
253 --log("Got xarg: ", s)
254 table.insert(message.xargs, s)
258 -- If the response code is BINARY_FOLLOWS, there's a big binary chunk
259 -- coming --- which we don't support.
261 if (message.response == BINARY_FOLLOWS) then
262 error("Server sent a binary chunk, which we don't support yet")
268 -----------------------------------------------------------------------------
269 -- OUTPUT MANGLING --
270 -----------------------------------------------------------------------------
272 local function writeline(...)
273 local s = table.concat(arg)
276 interface_writedata(fd, gsc, s)
277 interface_writedata(fd, gsc, "\n")
280 -----------------------------------------------------------------------------
281 -- PRESENCE MANAGEMENT --
282 -----------------------------------------------------------------------------
284 local function cant_save_buddy_list()
285 warning("Unable to send buddy list to server.")
288 local function save_buddy_list()
289 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
290 local m = get_response()
291 if (m.response ~= CIT_OK) then
292 cant_save_buddy_list()
296 -- Search and destroy any old buddy list.
298 writeline("MSGS ALL|0|1")
300 if (m.response ~= START_CHAT_MODE) then
301 cant_save_buddy_list()
305 writeline("subj|"..CITADEL_BUDDY_MSG)
313 if (not m) and (s ~= "000") then
319 writeline("DELE "..m)
321 if (m.response ~= CIT_OK) then
322 cant_save_buddy_list()
327 -- Save our buddy list.
329 writeline("ENT0 1||0|1|"..CITADEL_BUDDY_MSG.."|")
331 if (m.response ~= SEND_LISTING) then
332 cant_save_buddy_list()
336 for name, _ in pairs(buddies) do
337 local b = gaim_find_buddy(ga, name)
339 local alias = gaim_buddy_get_alias(b) or ""
340 local group = gaim_find_buddys_group(b)
341 local groupname = gaim_group_get_name(group)
342 writeline(name.."|"..alias.."|"..groupname)
347 -- Go back to the lobby.
349 writeline("GOTO "..WAITING_ROOM)
353 local function update_buddy_status()
355 local m = get_response()
356 if (m.response ~= LISTING_FOLLOWS) then
359 log("attempting to scan and update buddies")
361 local onlinebuddies = {}
362 for _, s in ipairs(m.xargs) do
363 local name = unpack_citadel_data_line(s)[2]
364 if (name ~= "(not logged in)") then
365 onlinebuddies[name] = true
369 -- Anyone who's not online is offline.
371 for s, _ in pairs(buddies) do
372 if not onlinebuddies[s] then
373 serv_got_update(gc, s, false, 0, 0, 0, 0)
377 -- Anyone who's online is, er, online.
379 for s, _ in pairs(onlinebuddies) do
380 -- If we're in no-buddy-list mode and this buddy isn't on our
381 -- list, add them automatically.
384 if not gaim_find_buddy(ga, s) then
385 local buddy = gaim_buddy_new(ga, s, s)
386 local group = gaim_group_new("Citadel")
388 -- buddy is not garbage collected! This must succeed!
389 gaim_blist_add_buddy(buddy, nil, group, nil)
394 serv_got_update(gc, s, true, 0, 0, 0, 0)
398 -----------------------------------------------------------------------------
400 -----------------------------------------------------------------------------
402 function citadel_schedule_now()
406 function citadel_input()
407 -- If there's no task, create one to handle this input.
409 if not taskqueue[1] then
414 function citadel_setfd(_fd)
416 log("fd = ", tonumber(fd))
419 function citadel_setgsc(_gsc)
420 gsc = tolua.cast(_gsc, "GaimSslConnection")
421 log("gsc registered")
424 function citadel_connect(_ga)
425 ga = tolua.cast(_ga, "GaimAccount")
426 gc = gaim_account_get_connection(ga)
431 username = gaim_account_get_username(ga)
432 _, _, username, servername = string.find(username, "^(.*)@(.*)$")
433 port = gaim_account_get_int(ga, "port", CITADEL_DEFAULT_PORT)
434 noblist = gaim_account_get_bool(ga, "no_blist", false)
436 log("connect to ", username, " on server ", servername, " port ", port)
440 gaim_connection_update_progress(gc, "Connecting", 1, STEPS)
441 local i = interface_connect(ga, gc, servername, port)
443 error("Unable to create socket")
446 local m = get_response()
447 if (m.response ~= CIT_OK) then
448 error("Unexpected response from server")
451 -- Switch to TLS mode, if desired.
453 if gaim_account_get_bool(ga, "use_tls", true) then
454 gaim_connection_update_progress(gc, "Requesting TLS", 2, STEPS)
457 if (m.response ~= 200) then
458 error("This Citadel server does not support TLS.")
461 -- This will always work. If the handshake fails, Lua will be
462 -- shot and we don't need to worry about cleaning up.
464 gaim_connection_update_progress(gc, "TLS handshake", 3, STEPS)
465 interface_tlson(gc, ga, fd)
467 -- Wait for the gsc to be hooked up.
476 gaim_connection_update_progress(gc, "Sending username", 4, STEPS)
477 writeline("USER "..username)
479 if (m.response == (ERROR+NO_SUCH_USER)) then
480 error("There is no user with name '", username, "' on this server.")
482 if (m.response ~= MORE_DATA) then
488 gaim_connection_update_progress(gc, "Sending password", 5, STEPS)
489 writeline("PASS "..gaim_account_get_password(ga))
491 if (m.response ~= CIT_OK) then
492 error("Incorrect password.")
495 -- Tell Citadel who we are.
497 gaim_connection_update_progress(gc, "Setting up", 6, STEPS)
498 writeline("IDEN 226|0|0.2|Gaim Citadel plugin|")
501 -- Set asynchronous mode.
503 gaim_connection_update_progress(gc, "Setting up", 7, STEPS)
506 if (m.response ~= CIT_OK) then
507 error("This Citadel server does not support instant messaging.")
511 -- Switch to private configuration room.
513 gaim_connection_update_progress(gc, "Setting up", 8, STEPS)
514 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
516 if (m.response ~= CIT_OK) then
517 warning("Unable to fetch buddy list from server.")
521 -- Look for our preferences.
523 gaim_connection_update_progress(gc, "Setting up", 9, STEPS)
524 writeline("MSGS ALL|0|1")
526 if (m.response ~= START_CHAT_MODE) then
527 warning("Unable to fetch buddy list from server.")
531 writeline("subj|"..CITADEL_BUDDY_MSG)
539 if (not m) and (s ~= "000") then
544 log("preference message in #", m)
549 gaim_connection_update_progress(gc, "Setting up", 10, STEPS)
550 writeline("MSG0 "..m)
556 if (s == "text") then
566 local name, alias, groupname = unpack(unpack_citadel_data_line(s))
567 if not gaim_find_buddy(ga, name) then
568 local buddy = gaim_buddy_new(ga, name, alias)
569 local group = gaim_group_new(groupname)
570 log("adding new buddy ", name)
572 -- buddy is not garbage collected! This must succeed!
573 gaim_blist_add_buddy(buddy, nil, group, nil)
579 -- Update buddy list with who's online.
581 gaim_connection_update_progress(gc, "Setting up", 11, STEPS)
582 update_buddy_status()
584 -- Go back to the Lobby.
586 gaim_connection_update_progress(gc, "Setting up", 12, STEPS)
587 writeline("GOTO "..WAITING_ROOM)
590 -- Switch on the timer.
592 timerhandle = interface_timeron(gc,
593 gaim_account_get_int(ga, "interval", CITADEL_POLL_INTERVAL)*1000)
597 gaim_connection_update_progress(gc, "Connected", 13, STEPS)
598 gaim_connection_set_state(gc, GAIM_CONNECTED)
602 function citadel_close()
603 interface_disconnect(fd or -1, gsc)
605 interface_timeroff(gc, timerhandle)
607 schedule_now = function() end
610 function citadel_send_im(who, what, flags)
612 writeline("SEXP ", who, "|-")
613 local m = get_response()
614 if (m.response ~= SEND_LISTING) then
615 serv_got_im(gc, "Citadel", "Unable to send message", GAIM_MESSAGE_ERROR, 0);
623 function citadel_fetch_pending_messages()
627 local m = get_response()
628 if (m.response ~= LISTING_FOLLOWS) then
632 local s = table.concat(m.xargs)
633 --log("got message from ", m[4], " at ", m[2], ": ", s)
634 serv_got_im(gc, m[4], s, GAIM_MESSAGE_RECV, m[2])
639 function citadel_get_info(name)
641 writeline("RBIO "..name)
642 local m = get_response()
643 if (m.response ~= LISTING_FOLLOWS) then
644 m = "That user has been boojumed."
646 m = table.concat(m.xargs, "<br>")
649 gaim_notify_userinfo(gc, name, name.."'s biography",
650 name, "Biography", m, nil, nil)
654 -----------------------------------------------------------------------------
656 -----------------------------------------------------------------------------
658 function citadel_add_buddy(name)
659 if not buddies[name] then
661 lazyqueue(update_buddy_status)
662 lazyqueue(save_buddy_list)
666 function citadel_remove_buddy(name)
667 if buddies[name] then
669 lazyqueue(save_buddy_list)
673 function citadel_alias_buddy(name)
674 if buddies[name] then
675 lazyqueue(save_buddy_list)
679 function citadel_group_buddy(name, oldgroup, newgroup)
680 if buddies[name] then
681 lazyqueue(save_buddy_list)
685 function citadel_timer()
687 lazyqueue(update_buddy_status)
690 -----------------------------------------------------------------------------
692 -----------------------------------------------------------------------------
696 local m = get_response()
697 if (m.response == (ASYNC_MSG+ASYNC_GEXP)) then
698 citadel_fetch_pending_messages()