source: protocols/purple/purple.c @ 8b91a1f

Last change on this file since 8b91a1f was 56985aa, checked in by dequis <dx@…>, at 2015-03-03T23:18:43Z

Revert "purple: cleanup, remove one usage of static local_bee"

This reverts commit 5ff46180e5378acd6d103d9314175c78530bda7e.

Turns out that libpurple really doesn't provide any context at all for
some queries.

Also, not going to say "Shouldn't affect anything" again. I'm getting
good at writing code that looks good, but actually breaks stuff.
That's not good. At all.

  • Property mode set to 100644
File size: 38.3 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  libpurple module - Main file                                             *
5*                                                                           *
6*  Copyright 2009-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
24#include "bitlbee.h"
25#include "bpurple.h"
26#include "help.h"
27
28#include <stdarg.h>
29
30#include <glib.h>
31#include <purple.h>
32
33GSList *purple_connections;
34
35/* This makes me VERY sad... :-( But some libpurple callbacks come in without
36   any context so this is the only way to get that. Don't want to support
37   libpurple in daemon mode anyway. */
38static bee_t *local_bee;
39
40static char *set_eval_display_name(set_t *set, char *value);
41
42struct im_connection *purple_ic_by_pa(PurpleAccount *pa)
43{
44        GSList *i;
45        struct purple_data *pd;
46
47        for (i = purple_connections; i; i = i->next) {
48                pd = ((struct im_connection *) i->data)->proto_data;
49                if (pd->account == pa) {
50                        return i->data;
51                }
52        }
53
54        return NULL;
55}
56
57static struct im_connection *purple_ic_by_gc(PurpleConnection *gc)
58{
59        return purple_ic_by_pa(purple_connection_get_account(gc));
60}
61
62static gboolean purple_menu_cmp(const char *a, const char *b)
63{
64        while (*a && *b) {
65                while (*a == '_') {
66                        a++;
67                }
68                while (*b == '_') {
69                        b++;
70                }
71                if (g_ascii_tolower(*a) != g_ascii_tolower(*b)) {
72                        return FALSE;
73                }
74
75                a++;
76                b++;
77        }
78
79        return (*a == '\0' && *b == '\0');
80}
81
82static void purple_init(account_t *acc)
83{
84        PurplePlugin *prpl = purple_plugins_find_with_id((char *) acc->prpl->data);
85        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
86        PurpleAccount *pa;
87        GList *i, *st;
88        set_t *s;
89        char help_title[64];
90        GString *help;
91        static gboolean dir_fixed = FALSE;
92
93        /* Layer violation coming up: Making an exception for libpurple here.
94           Dig in the IRC state a bit to get a username. Ideally we should
95           check if s/he identified but this info doesn't seem *that* important.
96           It's just that fecking libpurple can't *not* store this shit.
97
98           Remember that libpurple is not really meant to be used on public
99           servers anyway! */
100        if (!dir_fixed) {
101                irc_t *irc = acc->bee->ui_data;
102                char *dir;
103
104                dir = g_strdup_printf("%s/purple/%s", global.conf->configdir, irc->user->nick);
105                purple_util_set_user_dir(dir);
106                g_free(dir);
107
108                purple_blist_load();
109                purple_prefs_load();
110                dir_fixed = TRUE;
111        }
112
113        help = g_string_new("");
114        g_string_printf(help, "BitlBee libpurple module %s (%s).\n\nSupported settings:",
115                        (char *) acc->prpl->name, prpl->info->name);
116
117        if (pi->user_splits) {
118                GList *l;
119                g_string_append_printf(help, "\n* username: Username");
120                for (l = pi->user_splits; l; l = l->next) {
121                        g_string_append_printf(help, "%c%s",
122                                               purple_account_user_split_get_separator(l->data),
123                                               purple_account_user_split_get_text(l->data));
124                }
125        }
126
127        /* Convert all protocol_options into per-account setting variables. */
128        for (i = pi->protocol_options; i; i = i->next) {
129                PurpleAccountOption *o = i->data;
130                const char *name;
131                char *def = NULL;
132                set_eval eval = NULL;
133                void *eval_data = NULL;
134                GList *io = NULL;
135                GSList *opts = NULL;
136
137                name = purple_account_option_get_setting(o);
138
139                switch (purple_account_option_get_type(o)) {
140                case PURPLE_PREF_STRING:
141                        def = g_strdup(purple_account_option_get_default_string(o));
142
143                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
144                                               name, purple_account_option_get_text(o),
145                                               "string", def);
146
147                        break;
148
149                case PURPLE_PREF_INT:
150                        def = g_strdup_printf("%d", purple_account_option_get_default_int(o));
151                        eval = set_eval_int;
152
153                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
154                                               name, purple_account_option_get_text(o),
155                                               "integer", def);
156
157                        break;
158
159                case PURPLE_PREF_BOOLEAN:
160                        if (purple_account_option_get_default_bool(o)) {
161                                def = g_strdup("true");
162                        } else {
163                                def = g_strdup("false");
164                        }
165                        eval = set_eval_bool;
166
167                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
168                                               name, purple_account_option_get_text(o),
169                                               "boolean", def);
170
171                        break;
172
173                case PURPLE_PREF_STRING_LIST:
174                        def = g_strdup(purple_account_option_get_default_list_value(o));
175
176                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
177                                               name, purple_account_option_get_text(o),
178                                               "list", def);
179                        g_string_append(help, "\n  Possible values: ");
180
181                        for (io = purple_account_option_get_list(o); io; io = io->next) {
182                                PurpleKeyValuePair *kv = io->data;
183                                opts = g_slist_append(opts, kv->value);
184                                /* TODO: kv->value is not a char*, WTF? */
185                                if (strcmp(kv->value, kv->key) != 0) {
186                                        g_string_append_printf(help, "%s (%s), ", (char *) kv->value, kv->key);
187                                } else {
188                                        g_string_append_printf(help, "%s, ", (char *) kv->value);
189                                }
190                        }
191                        g_string_truncate(help, help->len - 2);
192                        eval = set_eval_list;
193                        eval_data = opts;
194
195                        break;
196
197                default:
198                        /** No way to talk to the user right now, invent one when
199                        this becomes important.
200                        irc_rootmsg( acc->irc, "Setting with unknown type: %s (%d) Expect stuff to break..\n",
201                                     name, purple_account_option_get_type( o ) );
202                        */
203                        g_string_append_printf(help, "\n* [%s] UNSUPPORTED (type %d)",
204                                               name, purple_account_option_get_type(o));
205                        name = NULL;
206                }
207
208                if (name != NULL) {
209                        s = set_add(&acc->set, name, def, eval, acc);
210                        s->flags |= ACC_SET_OFFLINE_ONLY;
211                        s->eval_data = eval_data;
212                        g_free(def);
213                }
214        }
215
216        g_snprintf(help_title, sizeof(help_title), "purple %s", (char *) acc->prpl->name);
217        help_add_mem(&global.help, help_title, help->str);
218        g_string_free(help, TRUE);
219
220        s = set_add(&acc->set, "display_name", NULL, set_eval_display_name, acc);
221        s->flags |= ACC_SET_ONLINE_ONLY;
222
223        if (pi->options & OPT_PROTO_MAIL_CHECK) {
224                s = set_add(&acc->set, "mail_notifications", "false", set_eval_bool, acc);
225                s->flags |= ACC_SET_OFFLINE_ONLY;
226        }
227
228        if (strcmp(prpl->info->name, "Gadu-Gadu") == 0) {
229                s = set_add(&acc->set, "gg_sync_contacts", "true", set_eval_bool, acc);
230        }
231
232        /* Go through all away states to figure out if away/status messages
233           are possible. */
234        pa = purple_account_new(acc->user, (char *) acc->prpl->data);
235        for (st = purple_account_get_status_types(pa); st; st = st->next) {
236                PurpleStatusPrimitive prim = purple_status_type_get_primitive(st->data);
237
238                if (prim == PURPLE_STATUS_AVAILABLE) {
239                        if (purple_status_type_get_attr(st->data, "message")) {
240                                acc->flags |= ACC_FLAG_STATUS_MESSAGE;
241                        }
242                } else if (prim != PURPLE_STATUS_OFFLINE) {
243                        if (purple_status_type_get_attr(st->data, "message")) {
244                                acc->flags |= ACC_FLAG_AWAY_MESSAGE;
245                        }
246                }
247        }
248        purple_accounts_remove(pa);
249}
250
251static void purple_sync_settings(account_t *acc, PurpleAccount *pa)
252{
253        PurplePlugin *prpl = purple_plugins_find_with_id(pa->protocol_id);
254        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
255        GList *i;
256
257        for (i = pi->protocol_options; i; i = i->next) {
258                PurpleAccountOption *o = i->data;
259                const char *name;
260                set_t *s;
261
262                name = purple_account_option_get_setting(o);
263                s = set_find(&acc->set, name);
264                if (s->value == NULL) {
265                        continue;
266                }
267
268                switch (purple_account_option_get_type(o)) {
269                case PURPLE_PREF_STRING:
270                case PURPLE_PREF_STRING_LIST:
271                        purple_account_set_string(pa, name, set_getstr(&acc->set, name));
272                        break;
273
274                case PURPLE_PREF_INT:
275                        purple_account_set_int(pa, name, set_getint(&acc->set, name));
276                        break;
277
278                case PURPLE_PREF_BOOLEAN:
279                        purple_account_set_bool(pa, name, set_getbool(&acc->set, name));
280                        break;
281
282                default:
283                        break;
284                }
285        }
286
287        if (pi->options & OPT_PROTO_MAIL_CHECK) {
288                purple_account_set_check_mail(pa, set_getbool(&acc->set, "mail_notifications"));
289        }
290}
291
292static void purple_login(account_t *acc)
293{
294        struct im_connection *ic = imcb_new(acc);
295        struct purple_data *pd;
296
297        if ((local_bee != NULL && local_bee != acc->bee) ||
298            (global.conf->runmode == RUNMODE_DAEMON && !getenv("BITLBEE_DEBUG"))) {
299                imcb_error(ic,  "Daemon mode detected. Do *not* try to use libpurple in daemon mode! "
300                           "Please use inetd or ForkDaemon mode instead.");
301                imc_logout(ic, FALSE);
302                return;
303        }
304        local_bee = acc->bee;
305
306        /* For now this is needed in the _connected() handlers if using
307           GLib event handling, to make sure we're not handling events
308           on dead connections. */
309        purple_connections = g_slist_prepend(purple_connections, ic);
310
311        ic->proto_data = pd = g_new0(struct purple_data, 1);
312        pd->account = purple_account_new(acc->user, (char *) acc->prpl->data);
313        purple_account_set_password(pd->account, acc->pass);
314        purple_sync_settings(acc, pd->account);
315
316        purple_account_set_enabled(pd->account, "BitlBee", TRUE);
317}
318
319static void purple_logout(struct im_connection *ic)
320{
321        struct purple_data *pd = ic->proto_data;
322
323        purple_account_set_enabled(pd->account, "BitlBee", FALSE);
324        purple_connections = g_slist_remove(purple_connections, ic);
325        purple_accounts_remove(pd->account);
326        g_free(pd);
327}
328
329static int purple_buddy_msg(struct im_connection *ic, char *who, char *message, int flags)
330{
331        PurpleConversation *conv;
332        struct purple_data *pd = ic->proto_data;
333
334        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM,
335                                                          who, pd->account)) == NULL) {
336                conv = purple_conversation_new(PURPLE_CONV_TYPE_IM,
337                                               pd->account, who);
338        }
339
340        purple_conv_im_send(purple_conversation_get_im_data(conv), message);
341
342        return 1;
343}
344
345static GList *purple_away_states(struct im_connection *ic)
346{
347        struct purple_data *pd = ic->proto_data;
348        GList *st, *ret = NULL;
349
350        for (st = purple_account_get_status_types(pd->account); st; st = st->next) {
351                PurpleStatusPrimitive prim = purple_status_type_get_primitive(st->data);
352                if (prim != PURPLE_STATUS_AVAILABLE && prim != PURPLE_STATUS_OFFLINE) {
353                        ret = g_list_append(ret, (void *) purple_status_type_get_name(st->data));
354                }
355        }
356
357        return ret;
358}
359
360static void purple_set_away(struct im_connection *ic, char *state_txt, char *message)
361{
362        struct purple_data *pd = ic->proto_data;
363        GList *status_types = purple_account_get_status_types(pd->account), *st;
364        PurpleStatusType *pst = NULL;
365        GList *args = NULL;
366
367        for (st = status_types; st; st = st->next) {
368                pst = st->data;
369
370                if (state_txt == NULL &&
371                    purple_status_type_get_primitive(pst) == PURPLE_STATUS_AVAILABLE) {
372                        break;
373                }
374
375                if (state_txt != NULL &&
376                    g_strcasecmp(state_txt, purple_status_type_get_name(pst)) == 0) {
377                        break;
378                }
379        }
380
381        if (message && purple_status_type_get_attr(pst, "message")) {
382                args = g_list_append(args, "message");
383                args = g_list_append(args, message);
384        }
385
386        purple_account_set_status_list(pd->account,
387                                       st ? purple_status_type_get_id(pst) : "away",
388                                       TRUE, args);
389
390        g_list_free(args);
391}
392
393static char *set_eval_display_name(set_t *set, char *value)
394{
395        account_t *acc = set->data;
396        struct im_connection *ic = acc->ic;
397
398        if (ic) {
399                imcb_log(ic, "Changing display_name not currently supported with libpurple!");
400        }
401
402        return NULL;
403}
404
405/* Bad bad gadu-gadu, not saving buddy list by itself */
406static void purple_gg_buddylist_export(PurpleConnection *gc)
407{
408        struct im_connection *ic = purple_ic_by_gc(gc);
409
410        if (set_getstr(&ic->acc->set, "gg_sync_contacts")) {
411                GList *actions = gc->prpl->info->actions(gc->prpl, gc);
412                GList *p;
413                for (p = g_list_first(actions); p; p = p->next) {
414                        if (((PurplePluginAction *) p->data) &&
415                            purple_menu_cmp(((PurplePluginAction *) p->data)->label,
416                                            "Upload buddylist to Server") == 0) {
417                                PurplePluginAction action;
418                                action.plugin = gc->prpl;
419                                action.context = gc;
420                                action.user_data = NULL;
421                                ((PurplePluginAction *) p->data)->callback(&action);
422                                break;
423                        }
424                }
425                g_list_free(actions);
426        }
427}
428
429static void purple_gg_buddylist_import(PurpleConnection *gc)
430{
431        struct im_connection *ic = purple_ic_by_gc(gc);
432
433        if (set_getstr(&ic->acc->set, "gg_sync_contacts")) {
434                GList *actions = gc->prpl->info->actions(gc->prpl, gc);
435                GList *p;
436                for (p = g_list_first(actions); p; p = p->next) {
437                        if (((PurplePluginAction *) p->data) &&
438                            purple_menu_cmp(((PurplePluginAction *) p->data)->label,
439                                            "Download buddylist from Server") == 0) {
440                                PurplePluginAction action;
441                                action.plugin = gc->prpl;
442                                action.context = gc;
443                                action.user_data = NULL;
444                                ((PurplePluginAction *) p->data)->callback(&action);
445                                break;
446                        }
447                }
448                g_list_free(actions);
449        }
450}
451
452static void purple_add_buddy(struct im_connection *ic, char *who, char *group)
453{
454        PurpleBuddy *pb;
455        PurpleGroup *pg = NULL;
456        struct purple_data *pd = ic->proto_data;
457
458        if (group && !(pg = purple_find_group(group))) {
459                pg = purple_group_new(group);
460                purple_blist_add_group(pg, NULL);
461        }
462
463        pb = purple_buddy_new(pd->account, who, NULL);
464        purple_blist_add_buddy(pb, NULL, pg, NULL);
465        purple_account_add_buddy(pd->account, pb);
466
467        purple_gg_buddylist_export(pd->account->gc);
468}
469
470static void purple_remove_buddy(struct im_connection *ic, char *who, char *group)
471{
472        PurpleBuddy *pb;
473        struct purple_data *pd = ic->proto_data;
474
475        pb = purple_find_buddy(pd->account, who);
476        if (pb != NULL) {
477                PurpleGroup *group;
478
479                group = purple_buddy_get_group(pb);
480                purple_account_remove_buddy(pd->account, pb, group);
481
482                purple_blist_remove_buddy(pb);
483        }
484
485        purple_gg_buddylist_export(pd->account->gc);
486}
487
488static void purple_add_permit(struct im_connection *ic, char *who)
489{
490        struct purple_data *pd = ic->proto_data;
491
492        purple_privacy_permit_add(pd->account, who, FALSE);
493}
494
495static void purple_add_deny(struct im_connection *ic, char *who)
496{
497        struct purple_data *pd = ic->proto_data;
498
499        purple_privacy_deny_add(pd->account, who, FALSE);
500}
501
502static void purple_rem_permit(struct im_connection *ic, char *who)
503{
504        struct purple_data *pd = ic->proto_data;
505
506        purple_privacy_permit_remove(pd->account, who, FALSE);
507}
508
509static void purple_rem_deny(struct im_connection *ic, char *who)
510{
511        struct purple_data *pd = ic->proto_data;
512
513        purple_privacy_deny_remove(pd->account, who, FALSE);
514}
515
516static void purple_get_info(struct im_connection *ic, char *who)
517{
518        struct purple_data *pd = ic->proto_data;
519
520        serv_get_info(purple_account_get_connection(pd->account), who);
521}
522
523static void purple_keepalive(struct im_connection *ic)
524{
525}
526
527static int purple_send_typing(struct im_connection *ic, char *who, int flags)
528{
529        PurpleTypingState state = PURPLE_NOT_TYPING;
530        struct purple_data *pd = ic->proto_data;
531
532        if (flags & OPT_TYPING) {
533                state = PURPLE_TYPING;
534        } else if (flags & OPT_THINKING) {
535                state = PURPLE_TYPED;
536        }
537
538        serv_send_typing(purple_account_get_connection(pd->account), who, state);
539
540        return 1;
541}
542
543static void purple_chat_msg(struct groupchat *gc, char *message, int flags)
544{
545        PurpleConversation *pc = gc->data;
546
547        purple_conv_chat_send(purple_conversation_get_chat_data(pc), message);
548}
549
550struct groupchat *purple_chat_with(struct im_connection *ic, char *who)
551{
552        /* No, "of course" this won't work this way. Or in fact, it almost
553           does, but it only lets you send msgs to it, you won't receive
554           any. Instead, we have to click the virtual menu item.
555        PurpleAccount *pa = ic->proto_data;
556        PurpleConversation *pc;
557        PurpleConvChat *pcc;
558        struct groupchat *gc;
559
560        gc = imcb_chat_new( ic, "BitlBee-libpurple groupchat" );
561        gc->data = pc = purple_conversation_new( PURPLE_CONV_TYPE_CHAT, pa, "BitlBee-libpurple groupchat" );
562        pc->ui_data = gc;
563
564        pcc = PURPLE_CONV_CHAT( pc );
565        purple_conv_chat_add_user( pcc, ic->acc->user, "", 0, TRUE );
566        purple_conv_chat_invite_user( pcc, who, "Please join my chat", FALSE );
567        //purple_conv_chat_add_user( pcc, who, "", 0, TRUE );
568        */
569
570        /* There went my nice afternoon. :-( */
571
572        struct purple_data *pd = ic->proto_data;
573        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
574        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
575        PurpleBuddy *pb = purple_find_buddy(pd->account, who);
576        PurpleMenuAction *mi;
577        GList *menu;
578
579        void (*callback)(PurpleBlistNode *, gpointer); /* FFFFFFFFFFFFFUUUUUUUUUUUUUU */
580
581        if (!pb || !pi || !pi->blist_node_menu) {
582                return NULL;
583        }
584
585        menu = pi->blist_node_menu(&pb->node);
586        while (menu) {
587                mi = menu->data;
588                if (purple_menu_cmp(mi->label, "initiate chat") ||
589                    purple_menu_cmp(mi->label, "initiate conference")) {
590                        break;
591                }
592                menu = menu->next;
593        }
594
595        if (menu == NULL) {
596                return NULL;
597        }
598
599        /* Call the fucker. */
600        callback = (void *) mi->callback;
601        callback(&pb->node, menu->data);
602
603        return NULL;
604}
605
606void purple_chat_invite(struct groupchat *gc, char *who, char *message)
607{
608        PurpleConversation *pc = gc->data;
609        PurpleConvChat *pcc = PURPLE_CONV_CHAT(pc);
610        struct purple_data *pd = gc->ic->proto_data;
611
612        serv_chat_invite(purple_account_get_connection(pd->account),
613                         purple_conv_chat_get_id(pcc),
614                         message && *message ? message : "Please join my chat",
615                         who);
616}
617
618void purple_chat_kick(struct groupchat *gc, char *who, const char *message)
619{
620        PurpleConversation *pc = gc->data;
621        char *str = g_strdup_printf("kick %s %s", who, message);
622
623        purple_conversation_do_command(pc, str, NULL, NULL);
624        g_free(str);
625}
626
627void purple_chat_leave(struct groupchat *gc)
628{
629        PurpleConversation *pc = gc->data;
630
631        purple_conversation_destroy(pc);
632}
633
634struct groupchat *purple_chat_join(struct im_connection *ic, const char *room, const char *nick, const char *password,
635                                   set_t **sets)
636{
637        struct purple_data *pd = ic->proto_data;
638        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
639        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
640        GHashTable *chat_hash;
641        PurpleConversation *conv;
642        GList *info, *l;
643
644        if (!pi->chat_info || !pi->chat_info_defaults ||
645            !(info = pi->chat_info(purple_account_get_connection(pd->account)))) {
646                imcb_error(ic, "Joining chatrooms not supported by this protocol");
647                return NULL;
648        }
649
650        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
651                                                          room, pd->account))) {
652                purple_conversation_destroy(conv);
653        }
654
655        chat_hash = pi->chat_info_defaults(
656                purple_account_get_connection(pd->account), room
657        );
658
659        for (l = info; l; l = l->next) {
660                struct proto_chat_entry *pce = l->data;
661
662                if (strcmp(pce->identifier, "handle") == 0) {
663                        g_hash_table_replace(chat_hash, "handle", g_strdup(nick));
664                } else if (strcmp(pce->identifier, "password") == 0) {
665                        g_hash_table_replace(chat_hash, "password", g_strdup(password));
666                } else if (strcmp(pce->identifier, "passwd") == 0) {
667                        g_hash_table_replace(chat_hash, "passwd", g_strdup(password));
668                }
669        }
670
671        serv_join_chat(purple_account_get_connection(pd->account), chat_hash);
672
673        return NULL;
674}
675
676void purple_transfer_request(struct im_connection *ic, file_transfer_t *ft, char *handle);
677
678static void purple_ui_init();
679
680GHashTable *prplcb_ui_info()
681{
682        static GHashTable *ret;
683
684        if (ret == NULL) {
685                ret = g_hash_table_new(g_str_hash, g_str_equal);
686                g_hash_table_insert(ret, "name", "BitlBee");
687                g_hash_table_insert(ret, "version", BITLBEE_VERSION);
688        }
689
690        return ret;
691}
692
693static PurpleCoreUiOps bee_core_uiops =
694{
695        NULL,
696        NULL,
697        purple_ui_init,
698        NULL,
699        prplcb_ui_info,
700};
701
702static void prplcb_conn_progress(PurpleConnection *gc, const char *text, size_t step, size_t step_count)
703{
704        struct im_connection *ic = purple_ic_by_gc(gc);
705
706        imcb_log(ic, "%s", text);
707}
708
709static void prplcb_conn_connected(PurpleConnection *gc)
710{
711        struct im_connection *ic = purple_ic_by_gc(gc);
712        const char *dn;
713        set_t *s;
714
715        imcb_connected(ic);
716
717        if ((dn = purple_connection_get_display_name(gc)) &&
718            (s = set_find(&ic->acc->set, "display_name"))) {
719                g_free(s->value);
720                s->value = g_strdup(dn);
721        }
722
723        // user list needs to be requested for Gadu-Gadu
724        purple_gg_buddylist_import(gc);
725
726        if (gc->flags & PURPLE_CONNECTION_HTML) {
727                ic->flags |= OPT_DOES_HTML;
728        }
729}
730
731static void prplcb_conn_disconnected(PurpleConnection *gc)
732{
733        struct im_connection *ic = purple_ic_by_gc(gc);
734
735        if (ic != NULL) {
736                imc_logout(ic, !gc->wants_to_die);
737        }
738}
739
740static void prplcb_conn_notice(PurpleConnection *gc, const char *text)
741{
742        struct im_connection *ic = purple_ic_by_gc(gc);
743
744        if (ic != NULL) {
745                imcb_log(ic, "%s", text);
746        }
747}
748
749static void prplcb_conn_report_disconnect_reason(PurpleConnection *gc, PurpleConnectionError reason, const char *text)
750{
751        struct im_connection *ic = purple_ic_by_gc(gc);
752
753        /* PURPLE_CONNECTION_ERROR_NAME_IN_USE means concurrent login,
754           should probably handle that. */
755        if (ic != NULL) {
756                imcb_error(ic, "%s", text);
757        }
758}
759
760static PurpleConnectionUiOps bee_conn_uiops =
761{
762        prplcb_conn_progress,
763        prplcb_conn_connected,
764        prplcb_conn_disconnected,
765        prplcb_conn_notice,
766        NULL,
767        NULL,
768        NULL,
769        prplcb_conn_report_disconnect_reason,
770};
771
772static void prplcb_blist_update(PurpleBuddyList *list, PurpleBlistNode *node)
773{
774        if (node->type == PURPLE_BLIST_BUDDY_NODE) {
775                PurpleBuddy *bud = (PurpleBuddy *) node;
776                PurpleGroup *group = purple_buddy_get_group(bud);
777                struct im_connection *ic = purple_ic_by_pa(bud->account);
778                PurpleStatus *as;
779                int flags = 0;
780
781                if (ic == NULL) {
782                        return;
783                }
784
785                if (bud->server_alias) {
786                        imcb_rename_buddy(ic, bud->name, bud->server_alias);
787                } else if (bud->alias) {
788                        imcb_rename_buddy(ic, bud->name, bud->alias);
789                }
790
791                if (group) {
792                        imcb_add_buddy(ic, bud->name, purple_group_get_name(group));
793                }
794
795                flags |= purple_presence_is_online(bud->presence) ? OPT_LOGGED_IN : 0;
796                flags |= purple_presence_is_available(bud->presence) ? 0 : OPT_AWAY;
797
798                as = purple_presence_get_active_status(bud->presence);
799
800                imcb_buddy_status(ic, bud->name, flags, purple_status_get_name(as),
801                                  purple_status_get_attr_string(as, "message"));
802
803                imcb_buddy_times(ic, bud->name,
804                                 purple_presence_get_login_time(bud->presence),
805                                 purple_presence_get_idle_time(bud->presence));
806        }
807}
808
809static void prplcb_blist_new(PurpleBlistNode *node)
810{
811        if (node->type == PURPLE_BLIST_BUDDY_NODE) {
812                PurpleBuddy *bud = (PurpleBuddy *) node;
813                struct im_connection *ic = purple_ic_by_pa(bud->account);
814
815                if (ic == NULL) {
816                        return;
817                }
818
819                imcb_add_buddy(ic, bud->name, NULL);
820
821                prplcb_blist_update(NULL, node);
822        }
823}
824
825static void prplcb_blist_remove(PurpleBuddyList *list, PurpleBlistNode *node)
826{
827/*
828        PurpleBuddy *bud = (PurpleBuddy*) node;
829
830        if( node->type == PURPLE_BLIST_BUDDY_NODE )
831        {
832                struct im_connection *ic = purple_ic_by_pa( bud->account );
833
834                if( ic == NULL )
835                        return;
836
837                imcb_remove_buddy( ic, bud->name, NULL );
838        }
839*/
840}
841
842static PurpleBlistUiOps bee_blist_uiops =
843{
844        NULL,
845        prplcb_blist_new,
846        NULL,
847        prplcb_blist_update,
848        prplcb_blist_remove,
849};
850
851void prplcb_conv_new(PurpleConversation *conv)
852{
853        if (conv->type == PURPLE_CONV_TYPE_CHAT) {
854                struct im_connection *ic = purple_ic_by_pa(conv->account);
855                struct groupchat *gc;
856
857                gc = imcb_chat_new(ic, conv->name);
858                if (conv->title != NULL) {
859                        imcb_chat_name_hint(gc, conv->title);
860                        imcb_chat_topic(gc, NULL, conv->title, 0);
861                }
862
863                conv->ui_data = gc;
864                gc->data = conv;
865
866                /* libpurple brokenness: Whatever. Show that we join right away,
867                   there's no clear "This is you!" signaling in _add_users so
868                   don't even try. */
869                imcb_chat_add_buddy(gc, gc->ic->acc->user);
870        }
871}
872
873void prplcb_conv_free(PurpleConversation *conv)
874{
875        struct groupchat *gc = conv->ui_data;
876
877        imcb_chat_free(gc);
878}
879
880void prplcb_conv_add_users(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
881{
882        struct groupchat *gc = conv->ui_data;
883        GList *b;
884
885        for (b = cbuddies; b; b = b->next) {
886                PurpleConvChatBuddy *pcb = b->data;
887
888                imcb_chat_add_buddy(gc, pcb->name);
889        }
890}
891
892void prplcb_conv_del_users(PurpleConversation *conv, GList *cbuddies)
893{
894        struct groupchat *gc = conv->ui_data;
895        GList *b;
896
897        for (b = cbuddies; b; b = b->next) {
898                imcb_chat_remove_buddy(gc, b->data, "");
899        }
900}
901
902void prplcb_conv_chat_msg(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags,
903                          time_t mtime)
904{
905        struct groupchat *gc = conv->ui_data;
906        PurpleBuddy *buddy;
907
908        /* ..._SEND means it's an outgoing message, no need to echo those. */
909        if (flags & PURPLE_MESSAGE_SEND) {
910                return;
911        }
912
913        buddy = purple_find_buddy(conv->account, who);
914        if (buddy != NULL) {
915                who = purple_buddy_get_name(buddy);
916        }
917
918        imcb_chat_msg(gc, who, (char *) message, 0, mtime);
919}
920
921static void prplcb_conv_im(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags,
922                           time_t mtime)
923{
924        struct im_connection *ic = purple_ic_by_pa(conv->account);
925        PurpleBuddy *buddy;
926
927        /* ..._SEND means it's an outgoing message, no need to echo those. */
928        if (flags & PURPLE_MESSAGE_SEND) {
929                return;
930        }
931
932        buddy = purple_find_buddy(conv->account, who);
933        if (buddy != NULL) {
934                who = purple_buddy_get_name(buddy);
935        }
936
937        imcb_buddy_msg(ic, (char *) who, (char *) message, 0, mtime);
938}
939
940/* No, this is not a ui_op but a signal. */
941static void prplcb_buddy_typing(PurpleAccount *account, const char *who, gpointer null)
942{
943        PurpleConversation *conv;
944        PurpleConvIm *im;
945        int state;
946
947        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account)) == NULL) {
948                return;
949        }
950
951        im = PURPLE_CONV_IM(conv);
952        switch (purple_conv_im_get_typing_state(im)) {
953        case PURPLE_TYPING:
954                state = OPT_TYPING;
955                break;
956        case PURPLE_TYPED:
957                state = OPT_THINKING;
958                break;
959        default:
960                state = 0;
961        }
962
963        imcb_buddy_typing(purple_ic_by_pa(account), who, state);
964}
965
966static PurpleConversationUiOps bee_conv_uiops =
967{
968        prplcb_conv_new,           /* create_conversation  */
969        prplcb_conv_free,          /* destroy_conversation */
970        prplcb_conv_chat_msg,      /* write_chat           */
971        prplcb_conv_im,            /* write_im             */
972        NULL,                      /* write_conv           */
973        prplcb_conv_add_users,     /* chat_add_users       */
974        NULL,                      /* chat_rename_user     */
975        prplcb_conv_del_users,     /* chat_remove_users    */
976        NULL,                      /* chat_update_user     */
977        NULL,                      /* present              */
978        NULL,                      /* has_focus            */
979        NULL,                      /* custom_smiley_add    */
980        NULL,                      /* custom_smiley_write  */
981        NULL,                      /* custom_smiley_close  */
982        NULL,                      /* send_confirm         */
983};
984
985struct prplcb_request_action_data {
986        void *user_data, *bee_data;
987        PurpleRequestActionCb yes, no;
988        int yes_i, no_i;
989};
990
991static void prplcb_request_action_yes(void *data)
992{
993        struct prplcb_request_action_data *pqad = data;
994
995        if (pqad->yes) {
996                pqad->yes(pqad->user_data, pqad->yes_i);
997        }
998        g_free(pqad);
999}
1000
1001static void prplcb_request_action_no(void *data)
1002{
1003        struct prplcb_request_action_data *pqad = data;
1004
1005        if (pqad->no) {
1006                pqad->no(pqad->user_data, pqad->no_i);
1007        }
1008        g_free(pqad);
1009}
1010
1011static void *prplcb_request_action(const char *title, const char *primary, const char *secondary,
1012                                   int default_action, PurpleAccount *account, const char *who,
1013                                   PurpleConversation *conv, void *user_data, size_t action_count,
1014                                   va_list actions)
1015{
1016        struct prplcb_request_action_data *pqad;
1017        int i;
1018        char *q;
1019
1020        pqad = g_new0(struct prplcb_request_action_data, 1);
1021
1022        for (i = 0; i < action_count; i++) {
1023                char *caption;
1024                void *fn;
1025
1026                caption = va_arg(actions, char*);
1027                fn = va_arg(actions, void*);
1028
1029                if (strstr(caption, "Accept") || strstr(caption, "OK")) {
1030                        pqad->yes = fn;
1031                        pqad->yes_i = i;
1032                } else if (strstr(caption, "Reject") || strstr(caption, "Cancel")) {
1033                        pqad->no = fn;
1034                        pqad->no_i = i;
1035                }
1036        }
1037
1038        pqad->user_data = user_data;
1039
1040        /* TODO: IRC stuff here :-( */
1041        q = g_strdup_printf("Request: %s\n\n%s\n\n%s", title, primary, secondary);
1042        pqad->bee_data = query_add(local_bee->ui_data, purple_ic_by_pa(account), q,
1043                                   prplcb_request_action_yes, prplcb_request_action_no, g_free, pqad);
1044
1045        g_free(q);
1046
1047        return pqad;
1048}
1049
1050/*
1051static void prplcb_request_test()
1052{
1053        fprintf( stderr, "bla\n" );
1054}
1055*/
1056
1057static PurpleRequestUiOps bee_request_uiops =
1058{
1059        NULL,
1060        NULL,
1061        prplcb_request_action,
1062        NULL,
1063        NULL,
1064        NULL,
1065        NULL,
1066};
1067
1068static void prplcb_privacy_permit_added(PurpleAccount *account, const char *name)
1069{
1070        struct im_connection *ic = purple_ic_by_pa(account);
1071
1072        if (!g_slist_find_custom(ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp)) {
1073                ic->permit = g_slist_prepend(ic->permit, g_strdup(name));
1074        }
1075}
1076
1077static void prplcb_privacy_permit_removed(PurpleAccount *account, const char *name)
1078{
1079        struct im_connection *ic = purple_ic_by_pa(account);
1080        void *n;
1081
1082        n = g_slist_find_custom(ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp);
1083        ic->permit = g_slist_remove(ic->permit, n);
1084}
1085
1086static void prplcb_privacy_deny_added(PurpleAccount *account, const char *name)
1087{
1088        struct im_connection *ic = purple_ic_by_pa(account);
1089
1090        if (!g_slist_find_custom(ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp)) {
1091                ic->deny = g_slist_prepend(ic->deny, g_strdup(name));
1092        }
1093}
1094
1095static void prplcb_privacy_deny_removed(PurpleAccount *account, const char *name)
1096{
1097        struct im_connection *ic = purple_ic_by_pa(account);
1098        void *n;
1099
1100        n = g_slist_find_custom(ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp);
1101        ic->deny = g_slist_remove(ic->deny, n);
1102}
1103
1104static PurplePrivacyUiOps bee_privacy_uiops =
1105{
1106        prplcb_privacy_permit_added,
1107        prplcb_privacy_permit_removed,
1108        prplcb_privacy_deny_added,
1109        prplcb_privacy_deny_removed,
1110};
1111
1112static void prplcb_debug_print(PurpleDebugLevel level, const char *category, const char *arg_s)
1113{
1114        fprintf(stderr, "DEBUG %s: %s", category, arg_s);
1115}
1116
1117static PurpleDebugUiOps bee_debug_uiops =
1118{
1119        prplcb_debug_print,
1120};
1121
1122static guint prplcb_ev_timeout_add(guint interval, GSourceFunc func, gpointer udata)
1123{
1124        return b_timeout_add(interval, (b_event_handler) func, udata);
1125}
1126
1127static guint prplcb_ev_input_add(int fd, PurpleInputCondition cond, PurpleInputFunction func, gpointer udata)
1128{
1129        return b_input_add(fd, cond | B_EV_FLAG_FORCE_REPEAT, (b_event_handler) func, udata);
1130}
1131
1132static gboolean prplcb_ev_remove(guint id)
1133{
1134        b_event_remove((gint) id);
1135        return TRUE;
1136}
1137
1138static PurpleEventLoopUiOps glib_eventloops =
1139{
1140        prplcb_ev_timeout_add,
1141        prplcb_ev_remove,
1142        prplcb_ev_input_add,
1143        prplcb_ev_remove,
1144};
1145
1146static void *prplcb_notify_email(PurpleConnection *gc, const char *subject, const char *from,
1147                                 const char *to, const char *url)
1148{
1149        struct im_connection *ic = purple_ic_by_gc(gc);
1150
1151        imcb_log(ic, "Received e-mail from %s for %s: %s <%s>", from, to, subject, url);
1152
1153        return NULL;
1154}
1155
1156static void *prplcb_notify_userinfo(PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info)
1157{
1158        struct im_connection *ic = purple_ic_by_gc(gc);
1159        GString *info = g_string_new("");
1160        GList *l = purple_notify_user_info_get_entries(user_info);
1161        char *key;
1162        const char *value;
1163        int n;
1164
1165        while (l) {
1166                PurpleNotifyUserInfoEntry *e = l->data;
1167
1168                switch (purple_notify_user_info_entry_get_type(e)) {
1169                case PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR:
1170                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_HEADER:
1171                        key = g_strdup(purple_notify_user_info_entry_get_label(e));
1172                        value = purple_notify_user_info_entry_get_value(e);
1173
1174                        if (key) {
1175                                strip_html(key);
1176                                g_string_append_printf(info, "%s: ", key);
1177
1178                                if (value) {
1179                                        n = strlen(value) - 1;
1180                                        while (g_ascii_isspace(value[n])) {
1181                                                n--;
1182                                        }
1183                                        g_string_append_len(info, value, n + 1);
1184                                }
1185                                g_string_append_c(info, '\n');
1186                                g_free(key);
1187                        }
1188
1189                        break;
1190                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK:
1191                        g_string_append(info, "------------------------\n");
1192                        break;
1193                }
1194
1195                l = l->next;
1196        }
1197
1198        imcb_log(ic, "User %s info:\n%s", who, info->str);
1199        g_string_free(info, TRUE);
1200
1201        return NULL;
1202}
1203
1204static PurpleNotifyUiOps bee_notify_uiops =
1205{
1206        NULL,
1207        prplcb_notify_email,
1208        NULL,
1209        NULL,
1210        NULL,
1211        NULL,
1212        prplcb_notify_userinfo,
1213};
1214
1215static void *prplcb_account_request_authorize(PurpleAccount *account, const char *remote_user,
1216                                              const char *id, const char *alias, const char *message, gboolean on_list,
1217                                              PurpleAccountRequestAuthorizationCb authorize_cb,
1218                                              PurpleAccountRequestAuthorizationCb deny_cb, void *user_data)
1219{
1220        struct im_connection *ic = purple_ic_by_pa(account);
1221        char *q;
1222
1223        if (alias) {
1224                q = g_strdup_printf("%s (%s) wants to add you to his/her contact "
1225                                    "list. (%s)", alias, remote_user, message);
1226        } else {
1227                q = g_strdup_printf("%s wants to add you to his/her contact "
1228                                    "list. (%s)", remote_user, message);
1229        }
1230
1231        imcb_ask_with_free(ic, q, user_data, authorize_cb, deny_cb, NULL);
1232        g_free(q);
1233
1234        return NULL;
1235}
1236
1237static PurpleAccountUiOps bee_account_uiops =
1238{
1239        NULL,
1240        NULL,
1241        NULL,
1242        prplcb_account_request_authorize,
1243        NULL,
1244};
1245
1246extern PurpleXferUiOps bee_xfer_uiops;
1247
1248static void purple_ui_init()
1249{
1250        purple_connections_set_ui_ops(&bee_conn_uiops);
1251        purple_blist_set_ui_ops(&bee_blist_uiops);
1252        purple_conversations_set_ui_ops(&bee_conv_uiops);
1253        purple_request_set_ui_ops(&bee_request_uiops);
1254        purple_privacy_set_ui_ops(&bee_privacy_uiops);
1255        purple_notify_set_ui_ops(&bee_notify_uiops);
1256        purple_accounts_set_ui_ops(&bee_account_uiops);
1257        purple_xfers_set_ui_ops(&bee_xfer_uiops);
1258
1259        if (getenv("BITLBEE_DEBUG")) {
1260                purple_debug_set_ui_ops(&bee_debug_uiops);
1261        }
1262}
1263
1264void purple_initmodule()
1265{
1266        struct prpl funcs;
1267        GList *prots;
1268        GString *help;
1269        char *dir;
1270
1271        if (B_EV_IO_READ != PURPLE_INPUT_READ ||
1272            B_EV_IO_WRITE != PURPLE_INPUT_WRITE) {
1273                /* FIXME FIXME FIXME FIXME FIXME :-) */
1274                exit(1);
1275        }
1276
1277        dir = g_strdup_printf("%s/purple", global.conf->configdir);
1278        purple_util_set_user_dir(dir);
1279        g_free(dir);
1280
1281        purple_debug_set_enabled(FALSE);
1282        purple_core_set_ui_ops(&bee_core_uiops);
1283        purple_eventloop_set_ui_ops(&glib_eventloops);
1284        if (!purple_core_init("BitlBee")) {
1285                /* Initializing the core failed. Terminate. */
1286                fprintf(stderr, "libpurple initialization failed.\n");
1287                abort();
1288        }
1289
1290        if (proxytype != PROXY_NONE) {
1291                PurpleProxyInfo *pi = purple_global_proxy_get_info();
1292                switch (proxytype) {
1293                case PROXY_SOCKS4:
1294                        purple_proxy_info_set_type(pi, PURPLE_PROXY_SOCKS4);
1295                        break;
1296                case PROXY_SOCKS5:
1297                        purple_proxy_info_set_type(pi, PURPLE_PROXY_SOCKS5);
1298                        break;
1299                case PROXY_HTTP:
1300                        purple_proxy_info_set_type(pi, PURPLE_PROXY_HTTP);
1301                        break;
1302                }
1303                purple_proxy_info_set_host(pi, proxyhost);
1304                purple_proxy_info_set_port(pi, proxyport);
1305                purple_proxy_info_set_username(pi, proxyuser);
1306                purple_proxy_info_set_password(pi, proxypass);
1307        }
1308
1309        purple_set_blist(purple_blist_new());
1310
1311        /* No, really. So far there were ui_ops for everything, but now suddenly
1312           one needs to use signals for typing notification stuff. :-( */
1313        purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
1314                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1315        purple_signal_connect(purple_conversations_get_handle(), "buddy-typed",
1316                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1317        purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
1318                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1319
1320        memset(&funcs, 0, sizeof(funcs));
1321        funcs.login = purple_login;
1322        funcs.init = purple_init;
1323        funcs.logout = purple_logout;
1324        funcs.buddy_msg = purple_buddy_msg;
1325        funcs.away_states = purple_away_states;
1326        funcs.set_away = purple_set_away;
1327        funcs.add_buddy = purple_add_buddy;
1328        funcs.remove_buddy = purple_remove_buddy;
1329        funcs.add_permit = purple_add_permit;
1330        funcs.add_deny = purple_add_deny;
1331        funcs.rem_permit = purple_rem_permit;
1332        funcs.rem_deny = purple_rem_deny;
1333        funcs.get_info = purple_get_info;
1334        funcs.keepalive = purple_keepalive;
1335        funcs.send_typing = purple_send_typing;
1336        funcs.handle_cmp = g_strcasecmp;
1337        /* TODO(wilmer): Set these only for protocols that support them? */
1338        funcs.chat_msg = purple_chat_msg;
1339        funcs.chat_with = purple_chat_with;
1340        funcs.chat_invite = purple_chat_invite;
1341        funcs.chat_kick = purple_chat_kick;
1342        funcs.chat_leave = purple_chat_leave;
1343        funcs.chat_join = purple_chat_join;
1344        funcs.transfer_request = purple_transfer_request;
1345
1346        help = g_string_new("BitlBee libpurple module supports the following IM protocols:\n");
1347
1348        /* Add a protocol entry to BitlBee's structures for every protocol
1349           supported by this libpurple instance. */
1350        for (prots = purple_plugins_get_protocols(); prots; prots = prots->next) {
1351                PurplePlugin *prot = prots->data;
1352                struct prpl *ret;
1353
1354                /* If we already have this one (as a native module), don't
1355                   add a libpurple duplicate. */
1356                if (find_protocol(prot->info->id)) {
1357                        continue;
1358                }
1359
1360                ret = g_memdup(&funcs, sizeof(funcs));
1361                ret->name = ret->data = prot->info->id;
1362                if (strncmp(ret->name, "prpl-", 5) == 0) {
1363                        ret->name += 5;
1364                }
1365                register_protocol(ret);
1366
1367                g_string_append_printf(help, "\n* %s (%s)", ret->name, prot->info->name);
1368
1369                /* libpurple doesn't define a protocol called OSCAR, but we
1370                   need it to be compatible with normal BitlBee. */
1371                if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) {
1372                        ret = g_memdup(&funcs, sizeof(funcs));
1373                        ret->name = "oscar";
1374                        ret->data = prot->info->id;
1375                        register_protocol(ret);
1376                }
1377        }
1378
1379        g_string_append(help, "\n\nFor used protocols, more information about available "
1380                        "settings can be found using \x02help purple <protocol name>\x02 "
1381                        "(create an account using that protocol first!)");
1382
1383        /* Add a simple dynamically-generated help item listing all
1384           the supported protocols. */
1385        help_add_mem(&global.help, "purple", help->str);
1386        g_string_free(help, TRUE);
1387}
Note: See TracBrowser for help on using the repository browser.