2 -- Gaim Citadel plugin.
5 -- This code is licensed under the GPL v3. 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
16 local serverfriendlyname
23 -----------------------------------------------------------------------------
25 -----------------------------------------------------------------------------
27 -- Our version number. Remember to update!
29 local VERSION_NUMBER = "0.3.1"
31 -- Special values returned as Citadel's response codes.
33 local LISTING_FOLLOWS = 100
36 local SEND_LISTING = 400
38 local BINARY_FOLLOWS = 600
39 local SEND_BINARY = 700
40 local START_CHAT_MODE = 800
42 local INTERNAL_ERROR = 10
44 local ILLEGAL_VALUE = 12
45 local NOT_LOGGED_IN = 20
46 local CMD_NOT_SUPPORTED = 30
47 local PASSWORD_REQUIRED = 40
48 local ALREADY_LOGGED_IN = 41
49 local USERNAME_REQUIRED = 42
50 local HIGHER_ACCESS_REQUIRED = 50
51 local MAX_SESSIONS_EXCEEDED = 51
52 local RESOURCE_BUSY = 52
53 local RESOURCE_NOT_OPEN = 53
55 local INVALID_FLOOR_OPERATION = 61
56 local NO_SUCH_USER = 70
57 local FILE_NOT_FOUND = 71
58 local ROOM_NOT_FOUND = 72
59 local NO_SUCH_SYSTEM = 73
60 local ALREADY_EXISTS = 74
61 local MESSAGE_NOT_FOUND = 75
66 -- Other Citadel settings.
68 local CITADEL_DEFAULT_PORT = 504
69 local CITADEL_CONFIG_ROOM = "My Citadel Config"
70 local WAITING_ROOM = "Sent/Received Pages"
71 local CITADEL_BUDDY_MSG = "__ Buddy List __"
72 local CITADEL_POLL_INTERVAL = 60
74 -----------------------------------------------------------------------------
76 -----------------------------------------------------------------------------
78 --local stderr = io.stderr
80 local function log(...)
82 for _, i in ipairs(arg) do
83 table.insert(s, tostring(i))
86 gaim_debug_info("citadel", string.gsub(s, "%%", "%%").."\n")
89 local function unexpectederror()
90 error("The Citadel server said something unexpected. Giving up.")
93 local function warning(...)
95 for _, i in ipairs(arg) do
96 table.insert(s, tostring(i))
98 gaim_connection_notice(gc, s)
101 local olderror = error
104 log("traceback: ", debug.traceback())
108 -----------------------------------------------------------------------------
110 -----------------------------------------------------------------------------
114 local inscheduler = false
116 local yield = coroutine.yield
118 local function schedule_now()
119 if not inscheduler then
122 while taskqueue[1] do
123 -- Pull the first task off the queue, creating it if necessary.
125 local task = taskqueue[1]
126 if (type(task) == "function") then
127 task = coroutine.create(task)
133 local s, e = coroutine.resume(task)
136 log("traceback: ", debug.traceback())
137 gaim_connection_error(gc, e)
140 -- If it's not dead, then it must have yielded --- return back to C.
142 if (coroutine.status(task) ~= "dead") then
146 -- Otherwise, remove it from the queue and go again.
148 table.remove(taskqueue, 1)
155 local function queue(func)
156 table.insert(taskqueue, func)
158 table.insert(taskqueue, function()
159 local i, e = pcall(func)
161 log("coroutine died with error! ", e)
162 gaim_connection_error(gc, e)
169 local function lazyqueue(func)
170 if not queued[func] then
180 -----------------------------------------------------------------------------
182 -----------------------------------------------------------------------------
184 local inputbuffer = ""
186 -- Read a single line of text from the server, maing Lua's coroutines do the
187 -- vast bulk of the work of managing Gaim's state machine for us. Woo!
189 local function readline()
190 -- Always yield at least once. Otherwise, Lua hogs all the CPU time.
196 -- Read some data from the remote server, if any's
199 local i = interface_readdata(fd, gsc)
201 error("Unexpected disconnection from Citadel server")
204 inputbuffer = inputbuffer..i
206 -- Have we read a complete line of text?
208 local s, e, l = string.find(inputbuffer, "^([^\n]*)\n")
212 inputbuffer = string.sub(inputbuffer, e+1)
217 -- Otherwise, wait some more.
223 local function unpack_citadel_data_line(s, a)
225 for i in string.gfind(s, "([^|]*)|?") do
231 -- Read in an parse a packet from the Citadel server.
233 local function get_response()
236 -- The first line of a message is of the format:
237 -- 123 String|String|String
239 -- The 123 is a response code.
242 message.response = tonumber(string.sub(s, 1, 3))
245 unpack_citadel_data_line(s, message)
247 -- If the response code is LISTING_FOLLOWS, then there's more data
250 if (message.response == LISTING_FOLLOWS) then
258 --log("Got xarg: ", s)
259 table.insert(message.xargs, s)
263 -- If the response code is BINARY_FOLLOWS, there's a big binary chunk
264 -- coming --- which we don't support.
266 if (message.response == BINARY_FOLLOWS) then
267 error("Server sent a binary chunk, which we don't support yet")
273 -----------------------------------------------------------------------------
274 -- OUTPUT MANGLING --
275 -----------------------------------------------------------------------------
277 local function writeline(...)
278 local s = table.concat(arg)
281 interface_writedata(fd, gsc, s)
282 interface_writedata(fd, gsc, "\n")
285 -----------------------------------------------------------------------------
286 -- PRESENCE MANAGEMENT --
287 -----------------------------------------------------------------------------
289 local function cant_save_buddy_list()
290 warning("Unable to send buddy list to server.")
293 local function save_buddy_list()
294 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
295 local m = get_response()
296 if (m.response ~= CIT_OK) then
297 cant_save_buddy_list()
301 -- Search and destroy any old buddy list.
303 writeline("MSGS ALL|0|1")
305 if (m.response ~= START_CHAT_MODE) then
306 cant_save_buddy_list()
310 writeline("subj|"..CITADEL_BUDDY_MSG)
318 if (not m) and (s ~= "000") then
324 writeline("DELE "..m)
326 if (m.response ~= CIT_OK) then
327 cant_save_buddy_list()
332 -- Save our buddy list.
334 writeline("ENT0 1||0|1|"..CITADEL_BUDDY_MSG.."|")
336 if (m.response ~= SEND_LISTING) then
337 cant_save_buddy_list()
341 for name, _ in pairs(buddies) do
342 local b = gaim_find_buddy(ga, name)
344 local alias = gaim_buddy_get_alias(b) or ""
345 local group = gaim_find_buddys_group(b)
346 local groupname = gaim_group_get_name(group)
347 writeline(name.."|"..alias.."|"..groupname)
352 -- Go back to the lobby.
354 writeline("GOTO "..WAITING_ROOM)
358 local function update_buddy_status()
360 local m = get_response()
361 if (m.response ~= LISTING_FOLLOWS) then
364 log("attempting to scan and update buddies")
366 local onlinebuddies = {}
367 for _, s in ipairs(m.xargs) do
368 local name = unpack_citadel_data_line(s)[2]
369 if (name ~= "(not logged in)") then
370 onlinebuddies[name] = true
374 -- Anyone who's not online is offline.
376 for s, _ in pairs(buddies) do
377 if not onlinebuddies[s] then
378 serv_got_update(gc, s, false, 0, 0, 0, 0)
382 -- Anyone who's online is, er, online.
384 for s, _ in pairs(onlinebuddies) do
385 -- If we're in no-buddy-list mode and this buddy isn't on our
386 -- list, add them automatically.
389 if not gaim_find_buddy(ga, s) then
390 log("trying to add new buddy ", s)
391 local buddy = gaim_buddy_new(ga, s, s)
393 -- buddy is not garbage collected! This must succeed!
394 local group = gaim_find_group(serverfriendlyname)
396 group = gaim_group_new(serverfriendlyname)
397 gaim_blist_add_group(group, nil)
400 gaim_blist_add_buddy(buddy, nil, group, nil)
402 warning("Unable to add "..s.." to your buddy list. This error should never happen.")
408 serv_got_update(gc, s, true, 0, 0, 0, 0)
412 -----------------------------------------------------------------------------
414 -----------------------------------------------------------------------------
416 function citadel_schedule_now()
420 function citadel_input()
421 -- If there's no task, create one to handle this input.
423 if not taskqueue[1] then
428 function citadel_setfd(_fd)
430 log("fd = ", tonumber(fd))
433 function citadel_setgsc(_gsc)
434 gsc = tolua.cast(_gsc, "GaimSslConnection")
435 log("gsc registered")
438 function citadel_connect(_ga)
439 ga = tolua.cast(_ga, "GaimAccount")
440 gc = gaim_account_get_connection(ga)
445 username = gaim_account_get_username(ga)
446 _, _, username, servername = string.find(username, "^(.*)@(.*)$")
447 serverfriendlyname = servername
448 port = gaim_account_get_int(ga, "port", CITADEL_DEFAULT_PORT)
449 noblist = gaim_account_get_bool(ga, "no_blist", false)
451 log("connect to ", username, " on server ", servername, " port ", port)
455 gaim_connection_update_progress(gc, "Connecting", 1, STEPS)
456 local i = interface_connect(ga, gc, servername, port)
458 error("Unable to create socket")
461 local m = get_response()
462 if (m.response ~= CIT_OK) then
463 error("Unexpected response from server")
466 -- Switch to TLS mode, if desired.
468 if gaim_account_get_bool(ga, "use_tls", true) then
469 gaim_connection_update_progress(gc, "Requesting TLS", 2, STEPS)
472 if (m.response ~= 200) then
473 error("This Citadel server does not support TLS.")
476 -- This will always work. If the handshake fails, Lua will be
477 -- shot and we don't need to worry about cleaning up.
479 gaim_connection_update_progress(gc, "TLS handshake", 3, STEPS)
480 interface_tlson(gc, ga, fd)
482 -- Wait for the gsc to be hooked up.
491 gaim_connection_update_progress(gc, "Sending username", 4, STEPS)
492 writeline("USER "..username)
494 if (m.response == (ERROR+NO_SUCH_USER)) then
495 error("There is no user with name '", username, "' on this server.")
497 if (m.response ~= MORE_DATA) then
503 gaim_connection_update_progress(gc, "Sending password", 5, STEPS)
504 writeline("PASS "..gaim_account_get_password(ga))
506 if (m.response ~= CIT_OK) then
507 error("Incorrect password.")
510 -- Tell Citadel who we are.
512 gaim_connection_update_progress(gc, "Setting up", 6, STEPS)
513 writeline("IDEN 226|0|"..VERSION_NUMBER.."|Gaim Citadel plugin|")
516 -- Get information about the Citadel server.
518 gaim_connection_update_progress(gc, "Setting up", 7, STEPS)
521 serverfriendlyname = m.xargs[3]
523 -- Set asynchronous mode.
525 gaim_connection_update_progress(gc, "Setting up", 8, STEPS)
528 if (m.response ~= CIT_OK) then
529 error("This Citadel server does not support instant messaging.")
533 -- Switch to private configuration room.
535 gaim_connection_update_progress(gc, "Setting up", 9, STEPS)
536 writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
538 if (m.response ~= CIT_OK) then
539 warning("Unable to fetch buddy list from server.")
543 -- Look for our preferences.
545 gaim_connection_update_progress(gc, "Setting up", 10, STEPS)
546 writeline("MSGS ALL|0|1")
548 if (m.response ~= START_CHAT_MODE) then
549 warning("Unable to fetch buddy list from server.")
553 writeline("subj|"..CITADEL_BUDDY_MSG)
561 if (not m) and (s ~= "000") then
566 log("preference message in #", m)
571 gaim_connection_update_progress(gc, "Setting up", 11, STEPS)
572 writeline("MSG0 "..m)
578 if (s == "text") then
588 local name, alias, groupname = unpack(unpack_citadel_data_line(s))
589 if not gaim_find_buddy(ga, name) then
590 local buddy = gaim_buddy_new(ga, name, alias)
591 local group = gaim_group_new(groupname)
592 log("adding new buddy ", name)
594 -- buddy is not garbage collected! This must succeed!
595 gaim_blist_add_buddy(buddy, nil, group, nil)
601 -- Update buddy list with who's online.
603 gaim_connection_update_progress(gc, "Setting up", 12, STEPS)
604 update_buddy_status()
606 -- Go back to the Lobby.
608 gaim_connection_update_progress(gc, "Setting up", 13, STEPS)
609 writeline("GOTO "..WAITING_ROOM)
612 -- Switch on the timer.
614 timerhandle = interface_timeron(gc,
615 gaim_account_get_int(ga, "interval", CITADEL_POLL_INTERVAL)*1000)
619 gaim_connection_update_progress(gc, "Connected", 14, STEPS)
620 gaim_connection_set_state(gc, GAIM_CONNECTED)
624 function citadel_close()
625 interface_disconnect(fd or -1, gsc)
627 interface_timeroff(gc, timerhandle)
629 schedule_now = function() end
632 function citadel_send_im(who, what, flags)
634 writeline("SEXP ", who, "|-")
635 local m = get_response()
636 if (m.response ~= SEND_LISTING) then
637 serv_got_im(gc, "Citadel", "Unable to send message", GAIM_MESSAGE_ERROR, 0);
645 function citadel_fetch_pending_messages()
649 local m = get_response()
650 if (m.response ~= LISTING_FOLLOWS) then
654 local s = table.concat(m.xargs)
655 --log("got message from ", m[4], " at ", m[2], ": ", s)
656 serv_got_im(gc, m[4], s, GAIM_MESSAGE_RECV, m[2])
661 function citadel_get_info(name)
663 writeline("RBIO "..name)
664 local m = get_response()
665 if (m.response ~= LISTING_FOLLOWS) then
666 m = "That user has been boojumed."
668 m = table.concat(m.xargs, "<br>")
671 gaim_notify_userinfo(gc, name, name.."'s biography",
672 name, "Biography", m, nil, nil)
676 -----------------------------------------------------------------------------
678 -----------------------------------------------------------------------------
680 function citadel_add_buddy(name)
681 if not buddies[name] then
683 lazyqueue(update_buddy_status)
684 lazyqueue(save_buddy_list)
688 function citadel_remove_buddy(name)
689 if buddies[name] then
691 lazyqueue(save_buddy_list)
695 function citadel_alias_buddy(name)
696 if buddies[name] then
697 lazyqueue(save_buddy_list)
701 function citadel_group_buddy(name, oldgroup, newgroup)
702 if buddies[name] then
703 lazyqueue(save_buddy_list)
707 function citadel_timer()
709 lazyqueue(update_buddy_status)
712 -----------------------------------------------------------------------------
714 -----------------------------------------------------------------------------
718 local m = get_response()
719 if (m.response == (ASYNC_MSG+ASYNC_GEXP)) then
720 citadel_fetch_pending_messages()