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