]> code.citadel.org Git - citadel.git/blob - gaim-citadel/citadel.lua
Added everyone's-a-buddy mode. Removed the copious tracing. Added the ability
[citadel.git] / gaim-citadel / citadel.lua
1 -- citadel.lua
2 -- Gaim Citadel plugin.
3 --
4 -- © 2006 David Given.
5 -- This code is licensed under the GPL v2. See the file COPYING in this
6 -- directory for the full license text.
7 --
8 -- $Id:citadel.lua 4326 2006-02-18 12:26:22Z hjalfi $
9
10 -----------------------------------------------------------------------------
11 --                                 GLOBALS                                 --
12 -----------------------------------------------------------------------------
13
14 local _
15 local username, servername, port
16 local ga, gc
17 local fd, gsc
18 local timerhandle
19 local noblist
20 local buddies = {}
21
22 -----------------------------------------------------------------------------
23 --                                CONSTANTS                                --
24 -----------------------------------------------------------------------------
25
26 -- Special values returned as Citadel's response codes.
27
28 local LISTING_FOLLOWS         = 100
29 local CIT_OK                  = 200
30 local MORE_DATA               = 300
31 local SEND_LISTING            = 400
32 local ERROR                   = 500
33 local BINARY_FOLLOWS          = 600
34 local SEND_BINARY             = 700
35 local START_CHAT_MODE         = 800
36
37 local INTERNAL_ERROR          = 10
38 local TOO_BIG                 = 11
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
49 local NOT_HERE                = 60
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
57
58 local ASYNC_MSG               = 900
59 local ASYNC_GEXP              = 02
60
61 -- Other Citadel settings.
62
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
68
69 -----------------------------------------------------------------------------
70 --                                UTILITIES                                --
71 -----------------------------------------------------------------------------
72
73 --local stderr = io.stderr
74
75 local function log(...)
76         local s = {}
77         for _, i in ipairs(arg) do
78                 table.insert(s, tostring(i))
79         end
80         s = table.concat(s)
81         gaim_debug_info("citadel", (string.gsub(s, "%%", "%%")))
82 end
83
84 local function unexpectederror()
85         error("The Citadel server said something unexpected. Giving up.")
86 end
87
88 local function warning(...)
89         local s = {}
90         for _, i in ipairs(arg) do
91                 table.insert(s, tostring(i))
92         end
93         gaim_connection_notice(gc, s)
94 end
95
96 local olderror = error
97 error = function(e)
98         log("error: ", e)
99         log("traceback: ", debug.traceback())
100         olderror(e)
101 end
102
103 -----------------------------------------------------------------------------
104 --                                SCHEDULER                                --
105 -----------------------------------------------------------------------------
106
107 local taskqueue = {}
108 local idle
109 local inscheduler = false
110
111 local yield = coroutine.yield
112
113 local function schedule_now()
114         if not inscheduler then
115                 inscheduler = true
116                 
117                 while taskqueue[1] do
118                         -- Pull the first task off the queue, creating it if necessary.
119                 
120                         local task = taskqueue[1]
121                         if (type(task) == "function") then
122                                 task = coroutine.create(task)
123                                 taskqueue[1] = task
124                         end
125                         
126                         -- Run it.
127                         
128                         local s, e = coroutine.resume(task)
129                         if not s then
130                                 log("error: ", e)
131                                 log("traceback: ", debug.traceback())
132                                 gaim_connection_error(gc, e)
133                         end
134         
135                         -- If it's not dead, then it must have yielded --- return back to C.
136                                 
137                         if (coroutine.status(task) ~= "dead") then
138                                 break
139                         end
140                         
141                         -- Otherwise, remove it from the queue and go again.
142                         
143                         table.remove(taskqueue, 1)
144                 end
145                 
146                 inscheduler = false
147         end
148 end
149
150 local function queue(func)
151         table.insert(taskqueue, func)
152 --[[
153         table.insert(taskqueue, function()
154                 local i, e = pcall(func)
155                 if not i then
156                         log("coroutine died with error! ", e)
157                         gaim_connection_error(gc, e)
158                 end
159         end)
160 --]]
161 end
162
163 local queued = {}
164 local function lazyqueue(func)
165         if not queued[func] then
166                 queued[func] = true
167                 queue(
168                         function()
169                                 queued[func] = nil
170                                 func()
171                         end)
172         end
173 end
174
175 -----------------------------------------------------------------------------
176 --                             INPUT MANGLING                              --
177 -----------------------------------------------------------------------------
178
179 local inputbuffer = ""
180
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!
183
184 local function readline()
185         -- Always yield at least once. Otherwise, Lua hogs all the CPU time.
186
187         yield()
188
189         while true do
190                 if fd then
191                         -- Read some data from the remote server, if any's
192                         -- available.
193
194                         local i = interface_readdata(fd, gsc)
195                         if not i then
196                                 error("Unexpected disconnection from Citadel server")
197                         end
198
199                         inputbuffer = inputbuffer..i
200
201                         -- Have we read a complete line of text?
202
203                         local s, e, l = string.find(inputbuffer, "^([^\n]*)\n")
204                         if l then
205                                 -- If so, return it.
206
207                                 inputbuffer = string.sub(inputbuffer, e+1)
208                                 return l
209                         end
210                 end
211
212                 -- Otherwise, wait some more.
213         
214                 yield()
215         end
216 end
217
218 local function unpack_citadel_data_line(s, a)
219         a = a or {}
220         for i in string.gfind(s, "([^|]*)|?") do
221                 table.insert(a, i)
222         end
223         return a
224 end
225
226 -- Read in an parse a packet from the Citadel server.
227
228 local function get_response()
229         local message = {}
230
231         -- The first line of a message is of the format:
232         --   123 String|String|String
233         --
234         -- The 123 is a response code.
235                 
236         local s = readline()
237         message.response = tonumber(string.sub(s, 1, 3))
238         
239         s = string.sub(s, 5)
240         unpack_citadel_data_line(s, message)
241         
242         -- If the response code is LISTING_FOLLOWS, then there's more data
243         -- coming.
244         
245         if (message.response == LISTING_FOLLOWS) then
246                 message.xargs = {}
247                 
248                 while true do
249                         s = readline()
250                         if (s == "000") then
251                                 break
252                         end
253                         --log("Got xarg: ", s)
254                         table.insert(message.xargs, s)
255                 end
256         end
257         
258         -- If the response code is BINARY_FOLLOWS, there's a big binary chunk
259         -- coming --- which we don't support.
260         
261         if (message.response == BINARY_FOLLOWS) then
262                 error("Server sent a binary chunk, which we don't support yet")
263         end
264         
265         return message
266 end
267
268 -----------------------------------------------------------------------------
269 --                            OUTPUT MANGLING                              --
270 -----------------------------------------------------------------------------
271
272 local function writeline(...)
273         local s = table.concat(arg)
274         
275         log("send: ", s)
276         interface_writedata(fd, gsc, s)
277         interface_writedata(fd, gsc, "\n")
278 end
279
280 -----------------------------------------------------------------------------
281 --                           PRESENCE MANAGEMENT                           --
282 -----------------------------------------------------------------------------
283
284 local function cant_save_buddy_list()
285         warning("Unable to send buddy list to server.")
286 end
287
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()
293                 return
294         end
295
296         -- Search and destroy any old buddy list.
297
298         writeline("MSGS ALL|0|1")
299         m = get_response()
300         if (m.response ~= START_CHAT_MODE) then
301                 cant_save_buddy_list()
302                 return
303         end
304
305         writeline("subj|"..CITADEL_BUDDY_MSG)
306         writeline("000")
307         m = nil
308         while true do
309                 local s = readline()
310                 if (s == "000") then
311                         break
312                 end
313                 if (not m) and (s ~= "000") then
314                         m = s
315                 end
316         end
317
318         if m then
319                 writeline("DELE "..m)
320                 m = get_response()
321                 if (m.response ~= CIT_OK) then
322                         cant_save_buddy_list()
323                         return
324                 end
325         end
326
327         -- Save our buddy list.
328         
329         writeline("ENT0 1||0|1|"..CITADEL_BUDDY_MSG.."|")
330         m = get_response()
331         if (m.response ~= SEND_LISTING) then
332                 cant_save_buddy_list()
333                 return
334         end
335
336         for name, _ in pairs(buddies) do
337                 local b = gaim_find_buddy(ga, name)
338                 if b then
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)
343                 end
344         end
345         writeline("000")
346
347         -- Go back to the lobby.
348         
349         writeline("GOTO "..WAITING_ROOM)
350         get_response()
351 end
352
353 local function update_buddy_status()
354         writeline("RWHO")
355         local m = get_response()
356         if (m.response ~= LISTING_FOLLOWS) then
357                 return
358         end
359         log("attempting to scan and update buddies")
360
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
366                 end
367         end
368
369         -- Anyone who's not online is offline.
370
371         for s, _ in pairs(buddies) do
372                 if not onlinebuddies[s] then
373                         serv_got_update(gc, s, false, 0, 0, 0, 0)
374                 end
375         end
376
377         -- Anyone who's online is, er, online.
378
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.
382
383                 if noblist then
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")
387                                 if buddy then
388                                         -- buddy is not garbage collected! This must succeed!
389                                         gaim_blist_add_buddy(buddy, nil, group, nil)
390                                 end
391                         end
392                 end
393
394                 serv_got_update(gc, s, true, 0, 0, 0, 0)
395         end
396 end
397
398 -----------------------------------------------------------------------------
399 --                               ENTRYPOINTS                               --
400 -----------------------------------------------------------------------------
401
402 function citadel_schedule_now()
403         schedule_now()
404 end
405
406 function citadel_input()
407         -- If there's no task, create one to handle this input.
408         
409         if not taskqueue[1] then
410                 queue(idle)
411         end
412 end
413
414 function citadel_setfd(_fd)
415         fd = _fd
416         log("fd = ", tonumber(fd))
417 end
418
419 function citadel_setgsc(_gsc)
420         gsc = tolua.cast(_gsc, "GaimSslConnection")
421         log("gsc registered")
422 end
423
424 function citadel_connect(_ga)
425         ga = tolua.cast(_ga, "GaimAccount")
426         gc = gaim_account_get_connection(ga)
427         
428         queue(function()
429                 local STEPS = 13
430
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)
435                 
436                 log("connect to ", username, " on server ", servername, " port ", port)
437                 
438                 -- Make connection.
439                 
440                 gaim_connection_update_progress(gc, "Connecting", 1, STEPS)
441                 local i = interface_connect(ga, gc, servername, port)
442                 if (i ~= 0) then
443                         error("Unable to create socket")
444                 end
445                 
446                 local m = get_response()
447                 if (m.response ~= CIT_OK) then
448                         error("Unexpected response from server")
449                 end
450                 
451                 -- Switch to TLS mode, if desired.
452                 
453                 if gaim_account_get_bool(ga, "use_tls", true) then
454                         gaim_connection_update_progress(gc, "Requesting TLS", 2, STEPS)
455                         writeline("STLS")
456                         m = get_response()
457                         if (m.response ~= 200) then
458                                 error("This Citadel server does not support TLS.")
459                         end
460
461                         -- This will always work. If the handshake fails, Lua will be
462                         -- shot and we don't need to worry about cleaning up.
463                                                 
464                         gaim_connection_update_progress(gc, "TLS handshake", 3, STEPS)
465                         interface_tlson(gc, ga, fd)
466
467                         -- Wait for the gsc to be hooked up.
468
469                         while not gsc do
470                                 yield()
471                         end
472                 end
473                 
474                 -- Send username.
475                 
476                 gaim_connection_update_progress(gc, "Sending username", 4, STEPS)
477                 writeline("USER "..username)
478                 m = get_response()
479                 if (m.response == (ERROR+NO_SUCH_USER)) then
480                         error("There is no user with name '", username, "' on this server.")
481                 end
482                 if (m.response ~= MORE_DATA) then
483                         unexpectederror()
484                 end
485                 
486                 -- Send password.
487                 
488                 gaim_connection_update_progress(gc, "Sending password", 5, STEPS)
489                 writeline("PASS "..gaim_account_get_password(ga))
490                 m = get_response()
491                 if (m.response ~= CIT_OK) then
492                         error("Incorrect password.")
493                 end
494                 
495                 -- Tell Citadel who we are.
496                 
497                 gaim_connection_update_progress(gc, "Setting up", 6, STEPS)
498                 writeline("IDEN 226|0|0.2|Gaim Citadel plugin|")
499                 m = get_response()
500                 
501                 -- Set asynchronous mode.
502                 
503                 gaim_connection_update_progress(gc, "Setting up", 7, STEPS)
504                 writeline("ASYN 1")
505                 m = get_response()
506                 if (m.response ~= CIT_OK) then
507                         error("This Citadel server does not support instant messaging.")
508                 end
509                 
510                 (function()
511                         -- Switch to private configuration room.
512
513                         gaim_connection_update_progress(gc, "Setting up", 8, STEPS)
514                         writeline("GOTO "..CITADEL_CONFIG_ROOM.."||1")
515                         m = get_response()
516                         if (m.response ~= CIT_OK) then
517                                 warning("Unable to fetch buddy list from server.")
518                                 return
519                         end
520
521                         -- Look for our preferences.
522
523                         gaim_connection_update_progress(gc, "Setting up", 9, STEPS)
524                         writeline("MSGS ALL|0|1")
525                         m = get_response()
526                         if (m.response ~= START_CHAT_MODE) then
527                                 warning("Unable to fetch buddy list from server.")
528                                 return
529                         end
530
531                         writeline("subj|"..CITADEL_BUDDY_MSG)
532                         writeline("000")
533                         m = nil
534                         while true do
535                                 local s = readline()
536                                 if (s == "000") then
537                                         break
538                                 end
539                                 if (not m) and (s ~= "000") then
540                                         m = s
541                                 end
542                         end
543
544                         log("preference message in #", m)
545                         if not m then
546                                 return
547                         end
548                         
549                         gaim_connection_update_progress(gc, "Setting up", 10, STEPS)
550                         writeline("MSG0 "..m)
551                         while true do
552                                 local s = readline()
553                                 if (s == "000") then
554                                         return
555                                 end
556                                 if (s == "text") then
557                                         break
558                                 end
559                         end
560                         while true do
561                                 local s = readline()
562                                 if (s == "000") then
563                                         break
564                                 end
565                                 
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)
571                                         if buddy then
572                                                 -- buddy is not garbage collected! This must succeed!
573                                                 gaim_blist_add_buddy(buddy, nil, group, nil)
574                                         end
575                                 end
576                         end
577                 end)()
578
579                 -- Update buddy list with who's online.
580
581                 gaim_connection_update_progress(gc, "Setting up", 11, STEPS)
582                 update_buddy_status()
583
584                 -- Go back to the Lobby.
585
586                 gaim_connection_update_progress(gc, "Setting up", 12, STEPS)
587                 writeline("GOTO "..WAITING_ROOM)
588                 get_response()
589
590                 -- Switch on the timer.
591                 
592                 timerhandle = interface_timeron(gc, 
593                         gaim_account_get_int(ga, "interval", CITADEL_POLL_INTERVAL)*1000)
594                         
595                 -- Done!
596                 
597                 gaim_connection_update_progress(gc, "Connected", 13, STEPS)
598                 gaim_connection_set_state(gc, GAIM_CONNECTED)
599         end)
600 end
601
602 function citadel_close()
603         interface_disconnect(fd or -1, gsc)
604         if timerhandle then
605                 interface_timeroff(gc, timerhandle)
606         end
607         schedule_now = function() end
608 end
609
610 function citadel_send_im(who, what, flags)
611         queue(function()
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);
616                         return
617                 end
618                 writeline(what)
619                 writeline("000")
620         end)
621 end
622
623 function citadel_fetch_pending_messages()
624         queue(function()
625                 while true do
626                         writeline("GEXP")
627                         local m = get_response()
628                         if (m.response ~= LISTING_FOLLOWS) then
629                                 break
630                         end
631
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])
635                 end
636         end)
637 end
638
639 function citadel_get_info(name)
640         queue(function()
641                 writeline("RBIO "..name)
642                 local m = get_response()
643                 if (m.response ~= LISTING_FOLLOWS) then
644                         m = "That user has been boojumed."
645                 else
646                         m = table.concat(m.xargs, "<br>")
647                 end
648
649                 gaim_notify_userinfo(gc, name, name.."'s biography",
650                         name, "Biography", m, nil, nil)
651         end)
652 end
653
654 -----------------------------------------------------------------------------
655 --                                BUDDY LIST                               --
656 -----------------------------------------------------------------------------
657
658 function citadel_add_buddy(name)
659         if not buddies[name] then
660                 buddies[name] = true
661                 lazyqueue(update_buddy_status)
662                 lazyqueue(save_buddy_list)
663         end
664 end
665
666 function citadel_remove_buddy(name)
667         if buddies[name] then
668                 buddies[name] = nil
669                 lazyqueue(save_buddy_list)
670         end
671 end
672
673 function citadel_alias_buddy(name)
674         if buddies[name] then
675                 lazyqueue(save_buddy_list)
676         end
677 end
678
679 function citadel_group_buddy(name, oldgroup, newgroup)
680         if buddies[name] then
681                 lazyqueue(save_buddy_list)
682         end
683 end
684
685 function citadel_timer()
686         log("tick!")
687         lazyqueue(update_buddy_status)
688 end
689
690 -----------------------------------------------------------------------------
691 --                                   IDLE                                  --
692 -----------------------------------------------------------------------------
693
694 idle = function()
695         queue(function()
696                 local m = get_response()
697                 if (m.response == (ASYNC_MSG+ASYNC_GEXP)) then
698                         citadel_fetch_pending_messages()
699                 end
700         end)
701 end