Fix signal registration.
[doldaconnect.git] / clients / gaim / gaim-dolcon.c
1 /*
2  *  Dolda Connect - Modular multiuser Direct Connect-style client
3  *  Copyright (C) 2005 Fredrik Tolf (fredrik@dolda2000.com)
4  *  
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *  
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *  
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program; if not, write to the Free Software
17  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 */
19
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23 #include <string.h>
24 #include <doldaconnect/uilib.h>
25 #include <doldaconnect/uimisc.h>
26 #include <doldaconnect/utils.h>
27 #include <gaim.h>
28 #include <plugin.h>
29 #include <version.h>
30 #include <accountopt.h>
31 #include <roomlist.h>
32 #include <util.h>
33 #include <errno.h>
34
35 struct conndata {
36     int fd;
37     int readhdl, writehdl;
38     GaimConnection *gc;
39     GaimRoomlist *roomlist;
40 };
41
42 static struct conndata *inuse = NULL;
43 static GaimPlugin *me;
44
45 static void dcfdcb(struct conndata *data, int fd, GaimInputCondition condition);
46
47 static void updatewrite(struct conndata *data)
48 {
49     if(dc_wantwrite()) {
50         if(data->writehdl == -1)
51             data->writehdl = gaim_input_add(data->fd, GAIM_INPUT_WRITE, (void (*)(void *, int, GaimInputCondition))dcfdcb, data);
52     } else {
53         if(data->writehdl != -1) {
54             gaim_input_remove(data->writehdl);
55             data->writehdl = -1;
56         }
57     }
58 }
59
60 static void disconnected(struct conndata *data)
61 {
62     if(inuse == data)
63         inuse = NULL;
64     if(data->readhdl != -1) {
65         gaim_input_remove(data->readhdl);
66         data->readhdl = -1;
67     }
68     if(data->writehdl != -1) {
69         gaim_input_remove(data->writehdl);
70         data->writehdl = -1;
71     }
72     data->fd = -1;
73 }
74
75 static int loginconv(int type, wchar_t *text, char **resp, struct conndata *data)
76 {
77     switch(type) {
78     case DC_LOGIN_CONV_NOECHO:
79         if(data->gc->account->password == NULL) {
80             updatewrite(data);
81             return(1);
82         } else {
83             *resp = sstrdup(data->gc->account->password);
84             updatewrite(data);
85             return(0);
86         }
87     default:
88         updatewrite(data);
89         return(1);
90     }
91 }
92
93 static gboolean gi_chatjoincb(GaimConversation *conv, const char *user, GaimConvChatBuddyFlags flags, void *uudata)
94 {
95     GaimConnection *c;
96     
97     if((c = gaim_conversation_get_gc(conv)) == NULL)
98         return(FALSE);
99     if(c->prpl == me)
100         return(TRUE);
101     return(FALSE);
102 }
103
104 static gboolean gi_chatleavecb(GaimConversation *conv, const char *user, const char *reason, void *uudata)
105 {
106     GaimConnection *c;
107     
108     if((c = gaim_conversation_get_gc(conv)) == NULL)
109         return(FALSE);
110     if(c->prpl == me)
111         return(TRUE);
112     return(FALSE);
113 }
114
115 static void regsigs(void)
116 {
117     static GaimPlugin *regged = NULL;
118     
119     if(regged != me) {
120         gaim_signal_connect(gaim_conversations_get_handle(), "chat-buddy-joining", me, GAIM_CALLBACK(gi_chatjoincb), NULL);
121         gaim_signal_connect(gaim_conversations_get_handle(), "chat-buddy-leaving", me, GAIM_CALLBACK(gi_chatleavecb), NULL);
122         regged = me;
123     }
124 }
125
126 static void newpeercb(struct dc_fnetpeer *peer)
127 {
128     struct conndata *data;
129     GaimConversation *conv;
130     char *buf;
131     
132     data = peer->fn->udata;
133     if((conv = gaim_find_chat(data->gc, peer->fn->id)) != NULL) {
134         buf = sprintf2("%s", icswcstombs(peer->nick, "UTF-8", NULL));
135         gaim_conv_chat_add_user(GAIM_CONV_CHAT(conv), buf, NULL, GAIM_CBFLAGS_NONE, TRUE);
136         free(buf);
137     }
138 }
139
140 static void delpeercb(struct dc_fnetpeer *peer)
141 {
142     struct conndata *data;
143     GaimConversation *conv;
144     char *buf;
145     
146     data = peer->fn->udata;
147     if((conv = gaim_find_chat(data->gc, peer->fn->id)) != NULL) {
148         buf = sprintf2("%s", icswcstombs(peer->nick, "UTF-8", NULL));
149         gaim_conv_chat_remove_user(GAIM_CONV_CHAT(conv), buf, NULL);
150         free(buf);
151     }
152 }
153
154 static void chpeercb(struct dc_fnetpeer *peer)
155 {
156 }
157
158 static void fillpeerlist(struct dc_fnetnode *fn, int resp, struct conndata *data)
159 {
160 }
161
162 static void getfnlistcb(int resp, struct conndata *data)
163 {
164     struct dc_fnetnode *fn;
165     
166     for(fn = dc_fnetnodes; fn != NULL; fn = fn->next)
167     {
168         dc_getpeerlistasync(fn, (void (*)(struct dc_fnetnode *, int, void *))fillpeerlist, data);
169         fn->udata = data;
170         fn->newpeercb = newpeercb;
171         fn->delpeercb = delpeercb;
172         fn->chpeercb = chpeercb;
173     }
174 }
175
176 static void logincb(int err, wchar_t *reason, struct conndata *data)
177 {
178     if(err != DC_LOGIN_ERR_SUCCESS) {
179         dc_disconnect();
180         disconnected(data);
181         gaim_connection_error(data->gc, "Invalid login");
182         return;
183     }
184     gaim_connection_set_state(data->gc, GAIM_CONNECTED);
185     dc_queuecmd(NULL, NULL, L"notify", L"fn:chat", L"on", L"fn:act", L"on", L"fn:peer", L"on", NULL);
186     dc_getfnlistasync((void (*)(int, void *))getfnlistcb, data);
187 }
188
189 static void dcfdcb(struct conndata *data, int fd, GaimInputCondition condition)
190 {
191     struct dc_response *resp;
192     struct dc_intresp *ires;
193     struct dc_fnetnode *fn;
194     GaimConversation *conv;
195     char *peer, *msg;
196     
197     if(((condition & GAIM_INPUT_READ) && dc_handleread()) || ((condition & GAIM_INPUT_WRITE) && dc_handlewrite())) {
198         disconnected(data);
199         gaim_connection_error(data->gc, "Server has disconnected");
200         return;
201     }
202     while((resp = dc_getresp()) != NULL) {
203         if(!wcscmp(resp->cmdname, L".connect")) {
204             if(resp->code != 201) {
205                 dc_disconnect();
206                 disconnected(data);
207                 gaim_connection_error(data->gc, "Server refused connection");
208                 return;
209             } else if(dc_checkprotocol(resp, DC_LATEST)) {
210                 dc_disconnect();
211                 disconnected(data);
212                 gaim_connection_error(data->gc, "Server protocol revision mismatch");
213                 return;
214             } else {
215                 gaim_connection_update_progress(data->gc, "Authenticating", 2, 3);
216                 dc_loginasync(NULL, 1, (int (*)(int, wchar_t *, char **, void *))loginconv, (void (*)(int, wchar_t *, void *))logincb, data);
217             }
218         } else if(!wcscmp(resp->cmdname, L".notify")) {
219             dc_uimisc_handlenotify(resp);
220             switch(resp->code) {
221             case 600:
222                 if((ires = dc_interpret(resp)) != NULL)
223                 {
224                     if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL)
225                     {
226                         if(ires->argv[1].val.num)
227                         {
228                             /* XXX: Handle different rooms */
229                             if((conv = gaim_find_chat(data->gc, fn->id)) != NULL)
230                             {
231                                 peer = icwcstombs(ires->argv[3].val.str, "UTF-8");
232                                 msg = g_markup_escape_text(icswcstombs(ires->argv[4].val.str, "UTF-8", NULL), -1);
233                                 serv_got_chat_in(data->gc, gaim_conv_chat_get_id(GAIM_CONV_CHAT(conv)), peer, 0, msg, time(NULL));
234                                 g_free(msg);
235                                 free(peer);
236                             }
237                         } else {
238                             peer = sprintf2("%i:%s", fn->id, icswcstombs(ires->argv[3].val.str, "UTF-8", NULL));
239                             msg = g_markup_escape_text(icswcstombs(ires->argv[4].val.str, "UTF-8", NULL), -1);
240                             if(!gaim_account_get_bool(data->gc->account, "represspm", FALSE) || (gaim_find_conversation_with_account(GAIM_CONV_TYPE_IM, peer, data->gc->account) != NULL))
241                                 serv_got_im(data->gc, peer, msg, 0, time(NULL));
242                             g_free(msg);
243                             free(peer);
244                         }
245                     }
246                     dc_freeires(ires);
247                 }
248                 break;
249             case 601:
250             case 602:
251             case 603:
252                 break;
253             case 604:
254                 if((ires = dc_interpret(resp)) != NULL)
255                 {
256                     if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL)
257                     {
258                         fn->udata = data;
259                         fn->newpeercb = newpeercb;
260                         fn->delpeercb = delpeercb;
261                         fn->chpeercb = chpeercb;
262                     }
263                     dc_freeires(ires);
264                 }
265                 break;
266             case 605:
267                 break;
268             }
269         }
270         dc_freeresp(resp);
271     }
272     updatewrite(data);
273 }
274
275 static int gi_sendchat(GaimConnection *gc, int id, const char *what, GaimMessageFlags flags)
276 {
277     struct conndata *data;
278     struct dc_fnetnode *fn;
279     wchar_t *wwhat;
280     
281     data = gc->proto_data;
282     if((fn = dc_findfnetnode(id)) == NULL)
283         return(-EINVAL);
284     /* XXX: Handle chat rooms */
285     if((wwhat = icmbstowcs((char *)what, "UTF-8")) == NULL)
286         return(-errno);
287     dc_queuecmd(NULL, NULL, L"sendchat", L"%i", fn->id, L"1", L"", L"%ls", wwhat, NULL);
288     free(wwhat);
289     updatewrite(data);
290     return(0);
291 }
292
293 static int gi_sendim(GaimConnection *gc, const char *who, const char *what, GaimMessageFlags flags)
294 {
295     struct conndata *data;
296     struct dc_fnetnode *fn;
297     struct dc_fnetpeer *peer;
298     wchar_t *wwho, *wwhat, *p;
299     int en, id;
300     
301     data = gc->proto_data;
302     if((wwho = icmbstowcs((char *)who, "UTF-8")) == NULL)
303         return(-errno);
304     if((p = wcschr(wwho, L':')) == NULL) {
305         free(wwho);
306         return(-ESRCH);
307     }
308     *(p++) = L'\0';
309     id = wcstol(wwho, NULL, 10);
310     if((fn = dc_findfnetnode(id)) == NULL) {
311         free(wwho);
312         return(-ESRCH);
313     }
314     for(peer = fn->peers; peer != NULL; peer = peer->next) {
315         if(!wcscmp(peer->nick, p))
316             break;
317     }
318     if(peer == NULL) {
319         free(wwho);
320         return(-ESRCH);
321     }
322     if((wwhat = icmbstowcs((char *)what, "UTF-8")) == NULL) {
323         en = errno;
324         free(wwho);
325         return(-en);
326     }
327     dc_queuecmd(NULL, NULL, L"sendchat", L"%i", fn->id, L"0", L"%ls", peer->nick, L"%ls", wwhat, NULL);
328     free(wwho);
329     free(wwhat);
330     updatewrite(data);
331     return(1);
332 }
333
334 static const char *gi_listicon(GaimAccount *a, GaimBuddy *b)
335 {
336     return("dolcon");
337 }
338
339 static char *gi_statustext(GaimBuddy *b)
340 {
341     GaimPresence *p;
342
343     p = gaim_buddy_get_presence(b);
344     if (gaim_presence_is_online(p) && !gaim_presence_is_available(p))
345         return(g_strdup("Away"));
346     return(NULL);
347 }
348
349 static void gi_tiptext(GaimBuddy *b, GaimNotifyUserInfo *inf, gboolean full)
350 {
351     /* Nothing for now */
352 }
353
354 static GList *gi_statustypes(GaimAccount *act)
355 {
356     GList *ret;
357     
358     ret = NULL;
359     ret = g_list_append(ret, gaim_status_type_new(GAIM_STATUS_AVAILABLE, "avail", NULL, TRUE));
360     ret = g_list_append(ret, gaim_status_type_new(GAIM_STATUS_AWAY, "away", NULL, TRUE)); /* Coming up in ADC */
361     ret = g_list_append(ret, gaim_status_type_new(GAIM_STATUS_OFFLINE, "offline", NULL, TRUE));
362     return(ret);
363 }
364
365 static struct conndata *newconndata(void)
366 {
367     struct conndata *new;
368     
369     new = smalloc(sizeof(*new));
370     memset(new, 0, sizeof(*new));
371     new->fd = -1;
372     new->readhdl = new->writehdl = -1;
373     return(new);
374 }
375
376 static void freeconndata(struct conndata *data)
377 {
378     if(data->roomlist != NULL)
379         gaim_roomlist_unref(data->roomlist);
380     if(inuse == data)
381         inuse = NULL;
382     if(data->readhdl != -1)
383         gaim_input_remove(data->readhdl);
384     if(data->writehdl != -1)
385         gaim_input_remove(data->writehdl);
386     if(data->fd >= 0)
387         dc_disconnect();
388     free(data);
389 }
390
391 static void gi_login(GaimAccount *act)
392 {
393     GaimConnection *gc;
394     struct conndata *data;
395     
396     gc = gaim_account_get_connection(act);
397     gc->proto_data = data = newconndata();
398     data->gc = gc;
399     if(inuse != NULL) {
400         gaim_connection_error(gc, "Dolda Connect library already in use");
401         return;
402     }
403     gaim_connection_update_progress(gc, "Connecting", 1, 3);
404     if((data->fd = dc_connect((char *)gaim_account_get_string(act, "server", "localhost"))) < 0)
405     {
406         gaim_connection_error(gc, "Could not connect to server");
407         return;
408     }
409     data->readhdl = gaim_input_add(data->fd, GAIM_INPUT_READ, (void (*)(void *, int, GaimInputCondition))dcfdcb, data);
410     updatewrite(data);
411     inuse = data;
412 }
413
414 static void gi_close(GaimConnection *gc)
415 {
416     struct conndata *data;
417     
418     data = gc->proto_data;
419     freeconndata(data);
420 }
421
422 static GaimRoomlist *gi_getlist(GaimConnection *gc)
423 {
424     struct conndata *data;
425     GList *fields;
426     GaimRoomlist *rl;
427     GaimRoomlistField *f;
428     GaimRoomlistRoom *r;
429     struct dc_fnetnode *fn;
430     
431     data = gc->proto_data;
432     if(data->roomlist != NULL)
433         gaim_roomlist_unref(data->roomlist);
434     data->roomlist = rl = gaim_roomlist_new(gaim_connection_get_account(gc));
435     fields = NULL;
436     f = gaim_roomlist_field_new(GAIM_ROOMLIST_FIELD_INT, "", "id", TRUE);
437     fields = g_list_append(fields, f);
438     f = gaim_roomlist_field_new(GAIM_ROOMLIST_FIELD_INT, "Users", "users", FALSE);
439     fields = g_list_append(fields, f);
440     gaim_roomlist_set_fields(rl, fields);
441     for(fn = dc_fnetnodes; fn != NULL; fn = fn->next) {
442         if(fn->state != DC_FNN_STATE_EST)
443             continue;
444         r = gaim_roomlist_room_new(GAIM_ROOMLIST_ROOMTYPE_ROOM, icswcstombs(fn->name, "UTF-8", NULL), NULL);
445         gaim_roomlist_room_add_field(rl, r, GINT_TO_POINTER(fn->id));
446         gaim_roomlist_room_add_field(rl, r, GINT_TO_POINTER(fn->numusers));
447         gaim_roomlist_room_add(rl, r);
448     }
449     gaim_roomlist_set_in_progress(rl, FALSE);
450     return(rl);
451 }
452
453 static void gi_cancelgetlist(GaimRoomlist *rl)
454 {
455     GaimConnection *gc;
456     struct conndata *data;
457     
458     if((gc = gaim_account_get_connection(rl->account)) == NULL)
459         return;
460     data = gc->proto_data;
461     gaim_roomlist_set_in_progress(rl, FALSE);
462     if(data->roomlist == rl) {
463         data->roomlist = NULL;
464         gaim_roomlist_unref(rl);
465     }
466 }
467
468 static void gi_joinchat(GaimConnection *gc, GHashTable *chatdata)
469 {
470     struct conndata *data;
471     struct dc_fnetnode *fn;
472     GaimConversation *conv;
473     struct dc_fnetpeer *peer;
474     char *buf;
475     GList *ul, *fl, *c;
476     
477     regsigs();
478     data = gc->proto_data;
479     if((fn = dc_findfnetnode(GPOINTER_TO_INT(g_hash_table_lookup(chatdata, "id")))) == NULL)
480         return;
481     if(gaim_find_chat(gc, fn->id) != NULL)
482         return;
483     conv = serv_got_joined_chat(data->gc, fn->id, icswcstombs(fn->name, "UTF-8", NULL));
484     ul = fl = NULL;
485     for(peer = fn->peers; peer != NULL; peer = peer->next) {
486         buf = icwcstombs(peer->nick, "UTF-8");
487         ul = g_list_append(ul, buf);
488         fl = g_list_append(fl, GINT_TO_POINTER(0));
489     }
490     gaim_conv_chat_add_users(GAIM_CONV_CHAT(conv), ul, NULL, fl, FALSE);
491     for(c = ul; c != NULL; c = c->next)
492         free(c->data);
493     g_list_free(ul);
494     g_list_free(fl);
495 }
496
497 static char *gi_cbname(GaimConnection *gc, int id, const char *who)
498 {
499     return(g_strdup_printf("%i:%s", id, who));
500 }
501
502 static GaimPluginProtocolInfo protinfo = {
503     .options            = OPT_PROTO_PASSWORD_OPTIONAL,
504     .icon_spec          = NO_BUDDY_ICONS,
505     .list_icon          = gi_listicon,
506     .status_text        = gi_statustext,
507     .tooltip_text       = gi_tiptext,
508     .status_types       = gi_statustypes,
509     .login              = gi_login,
510     .close              = gi_close,
511     .roomlist_get_list  = gi_getlist,
512     .roomlist_cancel    = gi_cancelgetlist,
513     .join_chat          = gi_joinchat,
514     .chat_send          = gi_sendchat,
515     .send_im            = gi_sendim,
516     .get_cb_real_name   = gi_cbname,
517 };
518
519 static GaimPluginInfo info = {
520     .magic              = GAIM_PLUGIN_MAGIC,
521     .major_version      = GAIM_MAJOR_VERSION,
522     .minor_version      = GAIM_MINOR_VERSION,
523     .type               = GAIM_PLUGIN_PROTOCOL,
524     .priority           = GAIM_PRIORITY_DEFAULT,
525     .id                 = "prpl-dolcon",
526     .name               = "Dolda Connect",
527     .version            = VERSION,
528     .summary            = "Dolda Connect chat plugin",
529     .description        = "Allows Gaim to be used as a chat user interface for the Dolda Connect daemon",
530     .author             = "Fredrik Tolf <fredrik@dolda2000.com>",
531     .homepage           = "http://www.dolda2000.com/~fredrik/doldaconnect/",
532     .extra_info         = &protinfo
533 };
534
535 static void init(GaimPlugin *pl)
536 {
537     GaimAccountOption *opt;
538     
539     dc_init();
540     opt = gaim_account_option_string_new("Server", "server", "");
541     protinfo.protocol_options = g_list_append(protinfo.protocol_options, opt);
542     opt = gaim_account_option_bool_new("Do not pop up private messages automatically", "represspm", FALSE);
543     protinfo.protocol_options = g_list_append(protinfo.protocol_options, opt);
544     me = pl;
545 }
546
547 GAIM_INIT_PLUGIN(dolcon, init, info);