source: protocols/purple/purple.c @ 9767d03

Last change on this file since 9767d03 was c17d0af, checked in by dequis <dx@…>, at 2018-07-05T07:03:35Z

purple: Add "bitlbee-set-account-password" signal

Replacement API for purple_account_set_password() to be called by prpls
that wish to store updated passwords or oauth tokens, since our password
storage doesn't get notified of calls to purple_account_set_password().

Added so that the hangouts plugin can stop using that awful hack for the
oauth refresh token

  • Property mode set to 100644
File size: 57.2 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
33#if !PURPLE_VERSION_CHECK(2, 12, 0)
34#define PURPLE_MESSAGE_REMOTE_SEND 0x10000
35#endif
36
37GSList *purple_connections;
38
39/* This makes me VERY sad... :-( But some libpurple callbacks come in without
40   any context so this is the only way to get that. Don't want to support
41   libpurple in daemon mode anyway. */
42static bee_t *local_bee;
43
44static char *set_eval_display_name(set_t *set, char *value);
45
46void purple_request_input_callback(guint id, struct im_connection *ic,
47                                   const char *message, const char *who);
48
49void purple_transfer_cancel_all(struct im_connection *ic);
50
51/* purple_request_input specific stuff */
52typedef void (*ri_callback_t)(gpointer, const gchar *);
53
54struct request_input_data {
55        ri_callback_t data_callback;
56        void *user_data;
57        struct im_connection *ic;
58        char *buddy;
59        guint id;
60};
61
62struct purple_roomlist_data {
63        GSList *chats;
64        gint topic;
65        gboolean initialized;
66};
67
68
69struct im_connection *purple_ic_by_pa(PurpleAccount *pa)
70{
71        GSList *i;
72        struct purple_data *pd;
73
74        for (i = purple_connections; i; i = i->next) {
75                pd = ((struct im_connection *) i->data)->proto_data;
76                if (pd->account == pa) {
77                        return i->data;
78                }
79        }
80
81        return NULL;
82}
83
84static struct im_connection *purple_ic_by_gc(PurpleConnection *gc)
85{
86        return purple_ic_by_pa(purple_connection_get_account(gc));
87}
88
89static gboolean purple_menu_cmp(const char *a, const char *b)
90{
91        while (*a && *b) {
92                while (*a == '_') {
93                        a++;
94                }
95                while (*b == '_') {
96                        b++;
97                }
98                if (g_ascii_tolower(*a) != g_ascii_tolower(*b)) {
99                        return FALSE;
100                }
101
102                a++;
103                b++;
104        }
105
106        return (*a == '\0' && *b == '\0');
107}
108
109static char *purple_get_account_prpl_id(account_t *acc)
110{
111        /* "oscar" is how non-purple bitlbee calls it,
112         * and it might be icq or aim, depending on the username */
113        if (g_strcmp0(acc->prpl->name, "oscar") == 0) {
114                return (g_ascii_isdigit(acc->user[0])) ? "prpl-icq" : "prpl-aim";
115        }
116
117        return acc->prpl->data;
118}
119
120static gboolean purple_account_should_set_nick(account_t *acc)
121{
122        /* whitelist of protocols that tend to have numeric or meaningless usernames, and should
123         * always offer the 'alias' as a nick.  this is just so that users don't have to do
124         * 'account whatever set nick_format %full_name'
125         */
126        char *whitelist[] = {
127                "prpl-hangouts",
128                "prpl-eionrobb-funyahoo-plusplus",
129                "prpl-icq",
130                "prpl-line",
131                NULL,
132        };
133        char **p;
134
135        for (p = whitelist; *p; p++) {
136                if (g_strcmp0(acc->prpl->data, *p) == 0) {
137                        return TRUE;
138                }
139        }
140
141        return FALSE;
142}
143
144static void purple_init(account_t *acc)
145{
146        char *prpl_id = purple_get_account_prpl_id(acc);
147        PurplePlugin *prpl = purple_plugins_find_with_id(prpl_id);
148        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
149        PurpleAccount *pa;
150        GList *i, *st;
151        set_t *s;
152        char help_title[64];
153        GString *help;
154        static gboolean dir_fixed = FALSE;
155
156        /* Layer violation coming up: Making an exception for libpurple here.
157           Dig in the IRC state a bit to get a username. Ideally we should
158           check if s/he identified but this info doesn't seem *that* important.
159           It's just that fecking libpurple can't *not* store this shit.
160
161           Remember that libpurple is not really meant to be used on public
162           servers anyway! */
163        if (!dir_fixed) {
164                PurpleCertificatePool *pool;
165                irc_t *irc = acc->bee->ui_data;
166                char *dir;
167
168                dir = g_strdup_printf("%s/purple/%s", global.conf->configdir, irc->user->nick);
169                purple_util_set_user_dir(dir);
170                g_free(dir);
171
172                purple_blist_load();
173                purple_prefs_load();
174
175                if (proxytype == PROXY_SOCKS4A) {
176                        /* do this here after loading prefs. yes, i know, it sucks */
177                        purple_prefs_set_bool("/purple/proxy/socks4_remotedns", TRUE);
178                }
179
180                /* re-create the certificate cache directory */
181                pool = purple_certificate_find_pool("x509", "tls_peers");
182                dir = purple_certificate_pool_mkpath(pool, NULL);
183                purple_build_dir(dir, 0700);
184                g_free(dir);
185
186                dir_fixed = TRUE;
187        }
188
189        help = g_string_new("");
190        g_string_printf(help, "BitlBee libpurple module %s (%s).\n\nSupported settings:",
191                        (char *) acc->prpl->name, prpl->info->name);
192
193        if (pi->user_splits) {
194                GList *l;
195                g_string_append_printf(help, "\n* username: Username");
196                for (l = pi->user_splits; l; l = l->next) {
197                        g_string_append_printf(help, "%c%s",
198                                               purple_account_user_split_get_separator(l->data),
199                                               purple_account_user_split_get_text(l->data));
200                }
201        }
202
203        /* Convert all protocol_options into per-account setting variables. */
204        for (i = pi->protocol_options; i; i = i->next) {
205                PurpleAccountOption *o = i->data;
206                const char *name;
207                char *def = NULL;
208                set_eval eval = NULL;
209                void *eval_data = NULL;
210                GList *io = NULL;
211                GSList *opts = NULL;
212
213                name = purple_account_option_get_setting(o);
214
215                switch (purple_account_option_get_type(o)) {
216                case PURPLE_PREF_STRING:
217                        def = g_strdup(purple_account_option_get_default_string(o));
218
219                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
220                                               name, purple_account_option_get_text(o),
221                                               "string", def);
222
223                        break;
224
225                case PURPLE_PREF_INT:
226                        def = g_strdup_printf("%d", purple_account_option_get_default_int(o));
227                        eval = set_eval_int;
228
229                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
230                                               name, purple_account_option_get_text(o),
231                                               "integer", def);
232
233                        break;
234
235                case PURPLE_PREF_BOOLEAN:
236                        if (purple_account_option_get_default_bool(o)) {
237                                def = g_strdup("true");
238                        } else {
239                                def = g_strdup("false");
240                        }
241                        eval = set_eval_bool;
242
243                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
244                                               name, purple_account_option_get_text(o),
245                                               "boolean", def);
246
247                        break;
248
249                case PURPLE_PREF_STRING_LIST:
250                        def = g_strdup(purple_account_option_get_default_list_value(o));
251
252                        g_string_append_printf(help, "\n* %s (%s), %s, default: %s",
253                                               name, purple_account_option_get_text(o),
254                                               "list", def);
255                        g_string_append(help, "\n  Possible values: ");
256
257                        for (io = purple_account_option_get_list(o); io; io = io->next) {
258                                PurpleKeyValuePair *kv = io->data;
259                                opts = g_slist_append(opts, kv->value);
260                                /* TODO: kv->value is not a char*, WTF? */
261                                if (strcmp(kv->value, kv->key) != 0) {
262                                        g_string_append_printf(help, "%s (%s), ", (char *) kv->value, kv->key);
263                                } else {
264                                        g_string_append_printf(help, "%s, ", (char *) kv->value);
265                                }
266                        }
267                        g_string_truncate(help, help->len - 2);
268                        eval = set_eval_list;
269                        eval_data = opts;
270
271                        break;
272
273                default:
274                        /** No way to talk to the user right now, invent one when
275                        this becomes important.
276                        irc_rootmsg( acc->irc, "Setting with unknown type: %s (%d) Expect stuff to break..\n",
277                                     name, purple_account_option_get_type( o ) );
278                        */
279                        g_string_append_printf(help, "\n* [%s] UNSUPPORTED (type %d)",
280                                               name, purple_account_option_get_type(o));
281                        name = NULL;
282                }
283
284                if (name != NULL) {
285                        s = set_add(&acc->set, name, def, eval, acc);
286                        s->flags |= ACC_SET_OFFLINE_ONLY;
287                        s->eval_data = eval_data;
288                        g_free(def);
289                }
290        }
291
292        g_snprintf(help_title, sizeof(help_title), "purple %s", (char *) acc->prpl->name);
293        help_add_mem(&global.help, help_title, help->str);
294        g_string_free(help, TRUE);
295
296        s = set_add(&acc->set, "display_name", NULL, set_eval_display_name, acc);
297        s->flags |= ACC_SET_ONLINE_ONLY;
298
299        if (pi->options & OPT_PROTO_MAIL_CHECK) {
300                s = set_add(&acc->set, "mail_notifications", "false", set_eval_bool, acc);
301                s->flags |= ACC_SET_OFFLINE_ONLY;
302
303                s = set_add(&acc->set, "mail_notifications_handle", NULL, NULL, acc);
304                s->flags |= ACC_SET_OFFLINE_ONLY | SET_NULL_OK;
305        }
306
307        if (strcmp(prpl->info->name, "Gadu-Gadu") == 0) {
308                s = set_add(&acc->set, "gg_sync_contacts", "true", set_eval_bool, acc);
309        }
310
311        if (g_strcmp0(prpl->info->id, "prpl-line") == 0) {
312                s = set_add(&acc->set, "line-auth-token", NULL, NULL, acc);
313                s->flags |= SET_HIDDEN;
314        }
315
316        /* Go through all away states to figure out if away/status messages
317           are possible. */
318        pa = purple_account_new(acc->user, prpl_id);
319        for (st = purple_account_get_status_types(pa); st; st = st->next) {
320                PurpleStatusPrimitive prim = purple_status_type_get_primitive(st->data);
321
322                if (prim == PURPLE_STATUS_AVAILABLE) {
323                        if (purple_status_type_get_attr(st->data, "message")) {
324                                acc->flags |= ACC_FLAG_STATUS_MESSAGE;
325                        }
326                } else if (prim != PURPLE_STATUS_OFFLINE) {
327                        if (purple_status_type_get_attr(st->data, "message")) {
328                                acc->flags |= ACC_FLAG_AWAY_MESSAGE;
329                        }
330                }
331        }
332        purple_accounts_remove(pa);
333}
334
335static void purple_sync_settings(account_t *acc, PurpleAccount *pa)
336{
337        PurplePlugin *prpl = purple_plugins_find_with_id(pa->protocol_id);
338        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
339        GList *i;
340
341        for (i = pi->protocol_options; i; i = i->next) {
342                PurpleAccountOption *o = i->data;
343                const char *name;
344                set_t *s;
345
346                name = purple_account_option_get_setting(o);
347                s = set_find(&acc->set, name);
348                if (s->value == NULL) {
349                        continue;
350                }
351
352                switch (purple_account_option_get_type(o)) {
353                case PURPLE_PREF_STRING:
354                case PURPLE_PREF_STRING_LIST:
355                        purple_account_set_string(pa, name, set_getstr(&acc->set, name));
356                        break;
357
358                case PURPLE_PREF_INT:
359                        purple_account_set_int(pa, name, set_getint(&acc->set, name));
360                        break;
361
362                case PURPLE_PREF_BOOLEAN:
363                        purple_account_set_bool(pa, name, set_getbool(&acc->set, name));
364                        break;
365
366                default:
367                        break;
368                }
369        }
370
371        if (pi->options & OPT_PROTO_MAIL_CHECK) {
372                purple_account_set_check_mail(pa, set_getbool(&acc->set, "mail_notifications"));
373        }
374
375        if (g_strcmp0(prpl->info->id, "prpl-line") == 0) {
376                const char *name = "line-auth-token";
377                purple_account_set_string(pa, name, set_getstr(&acc->set, name));
378        }
379}
380
381static void purple_login(account_t *acc)
382{
383        struct im_connection *ic = imcb_new(acc);
384        struct purple_data *pd;
385
386        if ((local_bee != NULL && local_bee != acc->bee) ||
387            (global.conf->runmode == RUNMODE_DAEMON && !getenv("BITLBEE_DEBUG"))) {
388                imcb_error(ic,  "Daemon mode detected. Do *not* try to use libpurple in daemon mode! "
389                           "Please use inetd or ForkDaemon mode instead.");
390                imc_logout(ic, FALSE);
391                return;
392        }
393        local_bee = acc->bee;
394
395        /* For now this is needed in the _connected() handlers if using
396           GLib event handling, to make sure we're not handling events
397           on dead connections. */
398        purple_connections = g_slist_prepend(purple_connections, ic);
399
400        ic->proto_data = pd = g_new0(struct purple_data, 1);
401        pd->account = purple_account_new(acc->user, purple_get_account_prpl_id(acc));
402        pd->input_requests = g_hash_table_new_full(g_direct_hash, g_direct_equal,
403                                                   NULL, g_free);
404        pd->next_request_id = 0;
405        purple_account_set_password(pd->account, acc->pass);
406        purple_sync_settings(acc, pd->account);
407
408        if (purple_account_should_set_nick(acc)) {
409                pd->flags = PURPLE_OPT_SHOULD_SET_NICK;
410        }
411
412        purple_account_set_enabled(pd->account, "BitlBee", TRUE);
413
414        if (set_getbool(&acc->set, "mail_notifications") && set_getstr(&acc->set, "mail_notifications_handle")) {
415                imcb_add_buddy(ic, set_getstr(&acc->set, "mail_notifications_handle"), NULL);
416        }
417}
418
419static void purple_logout(struct im_connection *ic)
420{
421        struct purple_data *pd = ic->proto_data;
422
423        if (!pd) {
424                return;
425        }
426
427        while (ic->groupchats) {
428                imcb_chat_free(ic->groupchats->data);
429        }
430
431        if (pd->filetransfers) {
432                purple_transfer_cancel_all(ic);
433        }
434
435        purple_account_set_enabled(pd->account, "BitlBee", FALSE);
436        purple_connections = g_slist_remove(purple_connections, ic);
437        purple_accounts_remove(pd->account);
438        imcb_chat_list_free(ic);
439        g_free(pd->chat_list_server);
440        g_hash_table_destroy(pd->input_requests);
441        g_free(pd);
442}
443
444static int purple_buddy_msg(struct im_connection *ic, char *who, char *message, int flags)
445{
446        PurpleConversation *conv;
447        struct purple_data *pd = ic->proto_data;
448
449        if (!strncmp(who, PURPLE_REQUEST_HANDLE, sizeof(PURPLE_REQUEST_HANDLE) - 1)) {
450                guint request_id = atoi(who + sizeof(PURPLE_REQUEST_HANDLE));
451                purple_request_input_callback(request_id, ic, message, who);
452                return 1;
453        }
454
455        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM,
456                                                          who, pd->account)) == NULL) {
457                conv = purple_conversation_new(PURPLE_CONV_TYPE_IM,
458                                               pd->account, who);
459        }
460
461        purple_conv_im_send(purple_conversation_get_im_data(conv), message);
462
463        return 1;
464}
465
466static GList *purple_away_states(struct im_connection *ic)
467{
468        struct purple_data *pd = ic->proto_data;
469        GList *st, *ret = NULL;
470
471        for (st = purple_account_get_status_types(pd->account); st; st = st->next) {
472                PurpleStatusPrimitive prim = purple_status_type_get_primitive(st->data);
473                if (prim != PURPLE_STATUS_AVAILABLE && prim != PURPLE_STATUS_OFFLINE) {
474                        ret = g_list_append(ret, (void *) purple_status_type_get_name(st->data));
475                }
476        }
477
478        return ret;
479}
480
481static void purple_set_away(struct im_connection *ic, char *state_txt, char *message)
482{
483        struct purple_data *pd = ic->proto_data;
484        GList *status_types = purple_account_get_status_types(pd->account), *st;
485        PurpleStatusType *pst = NULL;
486        GList *args = NULL;
487
488        for (st = status_types; st; st = st->next) {
489                pst = st->data;
490
491                if (state_txt == NULL &&
492                    purple_status_type_get_primitive(pst) == PURPLE_STATUS_AVAILABLE) {
493                        break;
494                }
495
496                if (state_txt != NULL &&
497                    g_strcasecmp(state_txt, purple_status_type_get_name(pst)) == 0) {
498                        break;
499                }
500        }
501
502        if (message && purple_status_type_get_attr(pst, "message")) {
503                args = g_list_append(args, "message");
504                args = g_list_append(args, message);
505        }
506
507        purple_account_set_status_list(pd->account,
508                                       st ? purple_status_type_get_id(pst) : "away",
509                                       TRUE, args);
510
511        g_list_free(args);
512}
513
514static char *set_eval_display_name(set_t *set, char *value)
515{
516        account_t *acc = set->data;
517        struct im_connection *ic = acc->ic;
518
519        if (ic) {
520                imcb_log(ic, "Changing display_name not currently supported with libpurple!");
521        }
522
523        return NULL;
524}
525
526/* Bad bad gadu-gadu, not saving buddy list by itself */
527static void purple_gg_buddylist_export(PurpleConnection *gc)
528{
529        struct im_connection *ic = purple_ic_by_gc(gc);
530
531        if (set_getstr(&ic->acc->set, "gg_sync_contacts")) {
532                GList *actions = gc->prpl->info->actions(gc->prpl, gc);
533                GList *p;
534                for (p = g_list_first(actions); p; p = p->next) {
535                        if (((PurplePluginAction *) p->data) &&
536                            purple_menu_cmp(((PurplePluginAction *) p->data)->label,
537                                            "Upload buddylist to Server") == 0) {
538                                PurplePluginAction action;
539                                action.plugin = gc->prpl;
540                                action.context = gc;
541                                action.user_data = NULL;
542                                ((PurplePluginAction *) p->data)->callback(&action);
543                                break;
544                        }
545                }
546                g_list_free(actions);
547        }
548}
549
550static void purple_gg_buddylist_import(PurpleConnection *gc)
551{
552        struct im_connection *ic = purple_ic_by_gc(gc);
553
554        if (set_getstr(&ic->acc->set, "gg_sync_contacts")) {
555                GList *actions = gc->prpl->info->actions(gc->prpl, gc);
556                GList *p;
557                for (p = g_list_first(actions); p; p = p->next) {
558                        if (((PurplePluginAction *) p->data) &&
559                            purple_menu_cmp(((PurplePluginAction *) p->data)->label,
560                                            "Download buddylist from Server") == 0) {
561                                PurplePluginAction action;
562                                action.plugin = gc->prpl;
563                                action.context = gc;
564                                action.user_data = NULL;
565                                ((PurplePluginAction *) p->data)->callback(&action);
566                                break;
567                        }
568                }
569                g_list_free(actions);
570        }
571}
572
573static void purple_add_buddy(struct im_connection *ic, char *who, char *group)
574{
575        PurpleBuddy *pb;
576        PurpleGroup *pg = NULL;
577        struct purple_data *pd = ic->proto_data;
578
579        if (group && !(pg = purple_find_group(group))) {
580                pg = purple_group_new(group);
581                purple_blist_add_group(pg, NULL);
582        }
583
584        pb = purple_buddy_new(pd->account, who, NULL);
585        purple_blist_add_buddy(pb, NULL, pg, NULL);
586        purple_account_add_buddy(pd->account, pb);
587
588        purple_gg_buddylist_export(pd->account->gc);
589}
590
591static void purple_remove_buddy(struct im_connection *ic, char *who, char *group)
592{
593        PurpleBuddy *pb;
594        struct purple_data *pd = ic->proto_data;
595
596        pb = purple_find_buddy(pd->account, who);
597        if (pb != NULL) {
598                PurpleGroup *group;
599
600                group = purple_buddy_get_group(pb);
601                purple_account_remove_buddy(pd->account, pb, group);
602
603                purple_blist_remove_buddy(pb);
604        }
605
606        purple_gg_buddylist_export(pd->account->gc);
607}
608
609static void purple_add_permit(struct im_connection *ic, char *who)
610{
611        struct purple_data *pd = ic->proto_data;
612
613        purple_privacy_permit_add(pd->account, who, FALSE);
614}
615
616static void purple_add_deny(struct im_connection *ic, char *who)
617{
618        struct purple_data *pd = ic->proto_data;
619
620        purple_privacy_deny_add(pd->account, who, FALSE);
621}
622
623static void purple_rem_permit(struct im_connection *ic, char *who)
624{
625        struct purple_data *pd = ic->proto_data;
626
627        purple_privacy_permit_remove(pd->account, who, FALSE);
628}
629
630static void purple_rem_deny(struct im_connection *ic, char *who)
631{
632        struct purple_data *pd = ic->proto_data;
633
634        purple_privacy_deny_remove(pd->account, who, FALSE);
635}
636
637static void purple_get_info(struct im_connection *ic, char *who)
638{
639        struct purple_data *pd = ic->proto_data;
640
641        serv_get_info(purple_account_get_connection(pd->account), who);
642}
643
644static void purple_keepalive(struct im_connection *ic)
645{
646}
647
648static int purple_send_typing(struct im_connection *ic, char *who, int flags)
649{
650        PurpleTypingState state = PURPLE_NOT_TYPING;
651        struct purple_data *pd = ic->proto_data;
652
653        if (flags & OPT_TYPING) {
654                state = PURPLE_TYPING;
655        } else if (flags & OPT_THINKING) {
656                state = PURPLE_TYPED;
657        }
658
659        serv_send_typing(purple_account_get_connection(pd->account), who, state);
660
661        return 1;
662}
663
664static void purple_chat_msg(struct groupchat *gc, char *message, int flags)
665{
666        PurpleConversation *pc = gc->data;
667
668        purple_conv_chat_send(purple_conversation_get_chat_data(pc), message);
669}
670
671struct groupchat *purple_chat_with(struct im_connection *ic, char *who)
672{
673        /* No, "of course" this won't work this way. Or in fact, it almost
674           does, but it only lets you send msgs to it, you won't receive
675           any. Instead, we have to click the virtual menu item.
676        PurpleAccount *pa = ic->proto_data;
677        PurpleConversation *pc;
678        PurpleConvChat *pcc;
679        struct groupchat *gc;
680
681        gc = imcb_chat_new( ic, "BitlBee-libpurple groupchat" );
682        gc->data = pc = purple_conversation_new( PURPLE_CONV_TYPE_CHAT, pa, "BitlBee-libpurple groupchat" );
683        pc->ui_data = gc;
684
685        pcc = PURPLE_CONV_CHAT( pc );
686        purple_conv_chat_add_user( pcc, ic->acc->user, "", 0, TRUE );
687        purple_conv_chat_invite_user( pcc, who, "Please join my chat", FALSE );
688        //purple_conv_chat_add_user( pcc, who, "", 0, TRUE );
689        */
690
691        /* There went my nice afternoon. :-( */
692
693        struct purple_data *pd = ic->proto_data;
694        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
695        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
696        PurpleBuddy *pb = purple_find_buddy(pd->account, who);
697        PurpleMenuAction *mi;
698        GList *menu;
699
700        void (*callback)(PurpleBlistNode *, gpointer); /* FFFFFFFFFFFFFUUUUUUUUUUUUUU */
701
702        if (!pb || !pi || !pi->blist_node_menu) {
703                return NULL;
704        }
705
706        menu = pi->blist_node_menu(&pb->node);
707        while (menu) {
708                mi = menu->data;
709                if (purple_menu_cmp(mi->label, "initiate chat") ||
710                    purple_menu_cmp(mi->label, "initiate conference")) {
711                        break;
712                }
713                menu = menu->next;
714        }
715
716        if (menu == NULL) {
717                return NULL;
718        }
719
720        /* Call the fucker. */
721        callback = (void *) mi->callback;
722        callback(&pb->node, mi->data);
723
724        return NULL;
725}
726
727void purple_chat_invite(struct groupchat *gc, char *who, char *message)
728{
729        PurpleConversation *pc = gc->data;
730        PurpleConvChat *pcc = PURPLE_CONV_CHAT(pc);
731        struct purple_data *pd = gc->ic->proto_data;
732
733        serv_chat_invite(purple_account_get_connection(pd->account),
734                         purple_conv_chat_get_id(pcc),
735                         message && *message ? message : "Please join my chat",
736                         who);
737}
738
739void purple_chat_set_topic(struct groupchat *gc, char *topic)
740{
741        PurpleConversation *pc = gc->data;
742        PurpleConvChat *pcc = PURPLE_CONV_CHAT(pc);
743        struct purple_data *pd = gc->ic->proto_data;
744        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
745        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
746
747        if (pi->set_chat_topic) {
748                pi->set_chat_topic(purple_account_get_connection(pd->account),
749                                   purple_conv_chat_get_id(pcc),
750                                   topic);
751        }
752}
753
754void purple_chat_kick(struct groupchat *gc, char *who, const char *message)
755{
756        PurpleConversation *pc = gc->data;
757        char *str = g_strdup_printf("kick %s %s", who, message);
758
759        purple_conversation_do_command(pc, str, NULL, NULL);
760        g_free(str);
761}
762
763void purple_chat_leave(struct groupchat *gc)
764{
765        PurpleConversation *pc = gc->data;
766
767        purple_conversation_destroy(pc);
768}
769
770struct groupchat *purple_chat_join(struct im_connection *ic, const char *room, const char *nick, const char *password,
771                                   set_t **sets)
772{
773        struct purple_data *pd = ic->proto_data;
774        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
775        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
776        GHashTable *chat_hash;
777        PurpleConversation *conv;
778        struct groupchat *gc;
779        GList *info, *l;
780        GString *missing_settings = NULL;
781
782        if (!pi->chat_info || !pi->chat_info_defaults ||
783            !(info = pi->chat_info(purple_account_get_connection(pd->account)))) {
784                imcb_error(ic, "Joining chatrooms not supported by this protocol");
785                return NULL;
786        }
787
788        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
789                                                          room, pd->account))) {
790                purple_conversation_destroy(conv);
791        }
792
793        chat_hash = pi->chat_info_defaults(
794                purple_account_get_connection(pd->account), room
795        );
796
797        for (l = info; l; l = l->next) {
798                struct proto_chat_entry *pce = l->data;
799
800                if (strcmp(pce->identifier, "handle") == 0) {
801                        g_hash_table_replace(chat_hash, "handle", g_strdup(nick));
802                } else if (strcmp(pce->identifier, "password") == 0) {
803                        g_hash_table_replace(chat_hash, "password", g_strdup(password));
804                } else if (strcmp(pce->identifier, "passwd") == 0) {
805                        g_hash_table_replace(chat_hash, "passwd", g_strdup(password));
806                } else {
807                        char *key, *value;
808
809                        key = g_strdup_printf("purple_%s", pce->identifier);
810                        str_reject_chars(key, " -", '_');
811
812                        if ((value = set_getstr(sets, key))) {
813                                /* sync from bitlbee to the prpl */
814                                g_hash_table_replace(chat_hash, (char *) pce->identifier, g_strdup(value));
815                        } else if ((value = g_hash_table_lookup(chat_hash, pce->identifier))) {
816                                /* if the bitlbee one was empty, sync from prpl to bitlbee */
817                                set_setstr(sets, key, value);
818                        }
819
820                        g_free(key);
821                }
822
823                if (pce->required && !g_hash_table_lookup(chat_hash, pce->identifier)) {
824                        if (!missing_settings) {
825                                missing_settings = g_string_sized_new(32);
826                        }
827                        g_string_append_printf(missing_settings, "%s, ", pce->identifier);
828                }
829
830                g_free(pce);
831        }
832
833        g_list_free(info);
834
835        if (missing_settings) {
836                /* remove the ", " from the end */
837                g_string_truncate(missing_settings, missing_settings->len - 2);
838
839                imcb_error(ic, "Can't join %s. The following settings are required: %s", room, missing_settings->str);
840
841                g_string_free(missing_settings, TRUE);
842                g_hash_table_destroy(chat_hash);
843                return NULL;
844        }
845
846        /* do this before serv_join_chat to handle cases where prplcb_conv_new is called immediately (not async) */
847        gc = imcb_chat_new(ic, room);
848
849        serv_join_chat(purple_account_get_connection(pd->account), chat_hash);
850
851        g_hash_table_destroy(chat_hash);
852
853        return gc;
854}
855
856void purple_chat_list(struct im_connection *ic, const char *server)
857{
858        PurpleRoomlist *list;
859        struct purple_data *pd = ic->proto_data;
860        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
861        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
862
863        if (!pi || !pi->roomlist_get_list) {
864                imcb_log(ic, "Room listing unsupported by this purple plugin");
865                return;
866        }
867
868        g_free(pd->chat_list_server);
869        pd->chat_list_server = (server && *server) ? g_strdup(server) : NULL;
870
871        list = purple_roomlist_get_list(pd->account->gc);
872
873        if (list) {
874                struct purple_roomlist_data *rld = list->ui_data;
875                rld->initialized = TRUE;
876
877                purple_roomlist_ref(list);
878        }
879}
880
881/* handles either prpl->chat_(add|free)_settings depending on the value of 'add' */
882static void purple_chat_update_settings(account_t *acc, set_t **head, gboolean add)
883{
884        PurplePlugin *prpl = purple_plugins_find_with_id((char *) acc->prpl->data);
885        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
886        GList *info, *l;
887
888        if (!pi->chat_info || !pi->chat_info_defaults) {
889                return;
890        }
891
892        /* hack / leap of faith: pass a NULL here because we don't have a connection yet.
893         * i reviewed all the built-in prpls and a bunch of third-party ones and none
894         * of them seem to need this parameter at all, so... i hope it never crashes */
895        info = pi->chat_info(NULL);
896
897        for (l = info; l; l = l->next) {
898                struct proto_chat_entry *pce = l->data;
899                char *key;
900
901                if (strcmp(pce->identifier, "handle") == 0 ||
902                    strcmp(pce->identifier, "password") == 0 ||
903                    strcmp(pce->identifier, "passwd") == 0) {
904                        /* skip these, they are handled above */
905                        g_free(pce);
906                        continue;
907                }
908
909                key = g_strdup_printf("purple_%s", pce->identifier);
910                str_reject_chars(key, " -", '_');
911
912                if (add) {
913                        set_add(head, key, NULL, NULL, NULL);
914                } else {
915                        set_del(head, key);
916                }
917
918                g_free(key);
919                g_free(pce);
920        }
921
922        g_list_free(NULL);
923        g_list_free(info);
924}
925
926static void purple_chat_add_settings(account_t *acc, set_t **head)
927{
928        purple_chat_update_settings(acc, head, TRUE);
929}
930
931static void purple_chat_free_settings(account_t *acc, set_t **head)
932{
933        purple_chat_update_settings(acc, head, FALSE);
934}
935
936void purple_transfer_request(struct im_connection *ic, file_transfer_t *ft, char *handle);
937
938static void purple_ui_init();
939
940GHashTable *prplcb_ui_info()
941{
942        static GHashTable *ret;
943
944        if (ret == NULL) {
945                ret = g_hash_table_new(g_str_hash, g_str_equal);
946                g_hash_table_insert(ret, "name", "BitlBee");
947                g_hash_table_insert(ret, "version", BITLBEE_VERSION);
948        }
949
950        return ret;
951}
952
953static PurpleCoreUiOps bee_core_uiops =
954{
955        NULL,                      /* ui_prefs_init */
956        NULL,                      /* debug_ui_init */
957        purple_ui_init,            /* ui_init */
958        NULL,                      /* quit */
959        prplcb_ui_info,            /* get_ui_info */
960};
961
962static void prplcb_conn_progress(PurpleConnection *gc, const char *text, size_t step, size_t step_count)
963{
964        struct im_connection *ic = purple_ic_by_gc(gc);
965
966        imcb_log(ic, "%s", text);
967}
968
969static void prplcb_conn_connected(PurpleConnection *gc)
970{
971        struct im_connection *ic = purple_ic_by_gc(gc);
972        struct purple_data *pd = ic->proto_data;
973        const char *dn, *token;
974        set_t *s;
975
976        imcb_connected(ic);
977
978        if ((dn = purple_connection_get_display_name(gc)) &&
979            (s = set_find(&ic->acc->set, "display_name"))) {
980                g_free(s->value);
981                s->value = g_strdup(dn);
982        }
983
984        // user list needs to be requested for Gadu-Gadu
985        purple_gg_buddylist_import(gc);
986
987        /* more awful hacks, because clearly we didn't have enough of those */
988        if ((s = set_find(&ic->acc->set, "line-auth-token")) &&
989            (token = purple_account_get_string(pd->account, "line-auth-token", NULL))) {
990                g_free(s->value);
991                s->value = g_strdup(token);
992        }
993
994        ic->flags |= OPT_DOES_HTML;
995}
996
997static void prplcb_conn_disconnected(PurpleConnection *gc)
998{
999        struct im_connection *ic = purple_ic_by_gc(gc);
1000
1001        if (ic != NULL) {
1002                imc_logout(ic, !gc->wants_to_die);
1003        }
1004}
1005
1006static void prplcb_conn_notice(PurpleConnection *gc, const char *text)
1007{
1008        struct im_connection *ic = purple_ic_by_gc(gc);
1009
1010        if (ic != NULL) {
1011                imcb_log(ic, "%s", text);
1012        }
1013}
1014
1015static void prplcb_conn_report_disconnect_reason(PurpleConnection *gc, PurpleConnectionError reason, const char *text)
1016{
1017        struct im_connection *ic = purple_ic_by_gc(gc);
1018
1019        /* PURPLE_CONNECTION_ERROR_NAME_IN_USE means concurrent login,
1020           should probably handle that. */
1021        if (ic != NULL) {
1022                imcb_error(ic, "%s", text);
1023        }
1024}
1025
1026static PurpleConnectionUiOps bee_conn_uiops =
1027{
1028        prplcb_conn_progress,                    /* connect_progress */
1029        prplcb_conn_connected,                   /* connected */
1030        prplcb_conn_disconnected,                /* disconnected */
1031        prplcb_conn_notice,                      /* notice */
1032        NULL,                                    /* report_disconnect */
1033        NULL,                                    /* network_connected */
1034        NULL,                                    /* network_disconnected */
1035        prplcb_conn_report_disconnect_reason,    /* report_disconnect_reason */
1036};
1037
1038static void prplcb_blist_update(PurpleBuddyList *list, PurpleBlistNode *node)
1039{
1040        if (node->type == PURPLE_BLIST_BUDDY_NODE) {
1041                PurpleBuddy *bud = (PurpleBuddy *) node;
1042                PurpleGroup *group = purple_buddy_get_group(bud);
1043                struct im_connection *ic = purple_ic_by_pa(bud->account);
1044                struct purple_data *pd = ic->proto_data;
1045                PurpleStatus *as;
1046                int flags = 0;
1047                char *alias = NULL;
1048
1049                if (ic == NULL) {
1050                        return;
1051                }
1052
1053                alias = bud->server_alias ? : bud->alias;
1054
1055                if (alias) {
1056                        imcb_rename_buddy(ic, bud->name, alias);
1057                        if (pd->flags & PURPLE_OPT_SHOULD_SET_NICK) {
1058                                imcb_buddy_nick_change(ic, bud->name, alias);
1059                        }
1060                }
1061
1062                if (group) {
1063                        imcb_add_buddy(ic, bud->name, purple_group_get_name(group));
1064                }
1065
1066                flags |= purple_presence_is_online(bud->presence) ? OPT_LOGGED_IN : 0;
1067                flags |= purple_presence_is_available(bud->presence) ? 0 : OPT_AWAY;
1068
1069                as = purple_presence_get_active_status(bud->presence);
1070
1071                imcb_buddy_status(ic, bud->name, flags, purple_status_get_name(as),
1072                                  purple_status_get_attr_string(as, "message"));
1073
1074                imcb_buddy_times(ic, bud->name,
1075                                 purple_presence_get_login_time(bud->presence),
1076                                 purple_presence_get_idle_time(bud->presence));
1077        }
1078}
1079
1080static void prplcb_blist_new(PurpleBlistNode *node)
1081{
1082        if (node->type == PURPLE_BLIST_BUDDY_NODE) {
1083                PurpleBuddy *bud = (PurpleBuddy *) node;
1084                struct im_connection *ic = purple_ic_by_pa(bud->account);
1085
1086                if (ic == NULL) {
1087                        return;
1088                }
1089
1090                imcb_add_buddy(ic, bud->name, NULL);
1091
1092                prplcb_blist_update(NULL, node);
1093        }
1094}
1095
1096static void prplcb_blist_remove(PurpleBuddyList *list, PurpleBlistNode *node)
1097{
1098/*
1099        PurpleBuddy *bud = (PurpleBuddy*) node;
1100
1101        if( node->type == PURPLE_BLIST_BUDDY_NODE )
1102        {
1103                struct im_connection *ic = purple_ic_by_pa( bud->account );
1104
1105                if( ic == NULL )
1106                        return;
1107
1108                imcb_remove_buddy( ic, bud->name, NULL );
1109        }
1110*/
1111}
1112
1113static PurpleBlistUiOps bee_blist_uiops =
1114{
1115        NULL,                      /* new_list */
1116        prplcb_blist_new,          /* new_node */
1117        NULL,                      /* show */
1118        prplcb_blist_update,       /* update */
1119        prplcb_blist_remove,       /* remove */
1120};
1121
1122void prplcb_conv_new(PurpleConversation *conv)
1123{
1124        if (conv->type == PURPLE_CONV_TYPE_CHAT) {
1125                struct im_connection *ic = purple_ic_by_pa(conv->account);
1126                struct groupchat *gc;
1127
1128                gc = bee_chat_by_title(ic->bee, ic, conv->name);
1129
1130                if (!gc) {
1131                        gc = imcb_chat_new(ic, conv->name);
1132                        if (conv->title != NULL) {
1133                                imcb_chat_name_hint(gc, conv->title);
1134                        }
1135                }
1136
1137                /* don't set the topic if it's just the name */
1138                if (conv->title != NULL && strcmp(conv->name, conv->title) != 0) {
1139                        imcb_chat_topic(gc, NULL, conv->title, 0);
1140                }
1141
1142                conv->ui_data = gc;
1143                gc->data = conv;
1144
1145                /* libpurple brokenness: Whatever. Show that we join right away,
1146                   there's no clear "This is you!" signaling in _add_users so
1147                   don't even try. */
1148                imcb_chat_add_buddy(gc, gc->ic->acc->user);
1149        }
1150}
1151
1152void prplcb_conv_free(PurpleConversation *conv)
1153{
1154        struct groupchat *gc = conv->ui_data;
1155
1156        imcb_chat_free(gc);
1157}
1158
1159void prplcb_conv_add_users(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
1160{
1161        struct groupchat *gc = conv->ui_data;
1162        GList *b;
1163
1164        for (b = cbuddies; b; b = b->next) {
1165                PurpleConvChatBuddy *pcb = b->data;
1166
1167                imcb_chat_add_buddy(gc, pcb->name);
1168        }
1169}
1170
1171void prplcb_conv_del_users(PurpleConversation *conv, GList *cbuddies)
1172{
1173        struct groupchat *gc = conv->ui_data;
1174        GList *b;
1175
1176        for (b = cbuddies; b; b = b->next) {
1177                imcb_chat_remove_buddy(gc, b->data, "");
1178        }
1179}
1180
1181/* Generic handler for IM or chat messages, covers write_chat, write_im and write_conv */
1182static void handle_conv_msg(PurpleConversation *conv, const char *who, const char *message_, guint32 bee_flags, time_t mtime)
1183{
1184        struct im_connection *ic = purple_ic_by_pa(conv->account);
1185        struct groupchat *gc = conv->ui_data;
1186        char *message = g_strdup(message_);
1187        PurpleBuddy *buddy;
1188
1189        buddy = purple_find_buddy(conv->account, who);
1190        if (buddy != NULL) {
1191                who = purple_buddy_get_name(buddy);
1192        }
1193
1194        if (conv->type == PURPLE_CONV_TYPE_IM) {
1195                imcb_buddy_msg(ic, who, message, bee_flags, mtime);
1196        } else if (gc) {
1197                imcb_chat_msg(gc, who, message, bee_flags, mtime);
1198        }
1199
1200        g_free(message);
1201}
1202
1203/* Handles write_im and write_chat. Removes echoes of locally sent messages.
1204 *
1205 * PURPLE_MESSAGE_DELAYED is used for chat backlogs - if a message has both
1206 * that flag and _SEND, it's a self-message from before joining the channel.
1207 * Those are safe to display. The rest (with just _SEND) may be echoes. */
1208static void prplcb_conv_msg(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime)
1209{
1210        if ((!(flags & PURPLE_MESSAGE_SEND)) ||
1211            (flags & PURPLE_MESSAGE_DELAYED) ||
1212            (flags & PURPLE_MESSAGE_REMOTE_SEND)
1213        ) {
1214                handle_conv_msg(conv, who, message, (flags & PURPLE_MESSAGE_SEND) ? OPT_SELFMESSAGE : 0, mtime);
1215        }
1216}
1217
1218/* Handles write_conv. Only passes self messages from other locations through.
1219 * That is, only writes of PURPLE_MESSAGE_SEND.
1220 * There are more events which might be handled in the future, but some are tricky.
1221 * (images look like <img id="123">, what do i do with that?) */
1222static void prplcb_conv_write(PurpleConversation *conv, const char *who, const char *alias, const char *message,
1223                              PurpleMessageFlags flags, time_t mtime)
1224{
1225        if (flags & PURPLE_MESSAGE_SEND) {
1226                handle_conv_msg(conv, who, message, OPT_SELFMESSAGE, mtime);
1227        }
1228}
1229
1230/* No, this is not a ui_op but a signal. */
1231static void prplcb_buddy_typing(PurpleAccount *account, const char *who, gpointer null)
1232{
1233        PurpleConversation *conv;
1234        PurpleConvIm *im;
1235        int state;
1236
1237        if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account)) == NULL) {
1238                return;
1239        }
1240
1241        im = PURPLE_CONV_IM(conv);
1242        switch (purple_conv_im_get_typing_state(im)) {
1243        case PURPLE_TYPING:
1244                state = OPT_TYPING;
1245                break;
1246        case PURPLE_TYPED:
1247                state = OPT_THINKING;
1248                break;
1249        default:
1250                state = 0;
1251        }
1252
1253        imcb_buddy_typing(purple_ic_by_pa(account), who, state);
1254}
1255
1256static PurpleConversationUiOps bee_conv_uiops =
1257{
1258        prplcb_conv_new,           /* create_conversation  */
1259        prplcb_conv_free,          /* destroy_conversation */
1260        prplcb_conv_msg,           /* write_chat           */
1261        prplcb_conv_msg,           /* write_im             */
1262        prplcb_conv_write,         /* write_conv           */
1263        prplcb_conv_add_users,     /* chat_add_users       */
1264        NULL,                      /* chat_rename_user     */
1265        prplcb_conv_del_users,     /* chat_remove_users    */
1266        NULL,                      /* chat_update_user     */
1267        NULL,                      /* present              */
1268        NULL,                      /* has_focus            */
1269        NULL,                      /* custom_smiley_add    */
1270        NULL,                      /* custom_smiley_write  */
1271        NULL,                      /* custom_smiley_close  */
1272        NULL,                      /* send_confirm         */
1273};
1274
1275struct prplcb_request_action_data {
1276        void *user_data, *bee_data;
1277        PurpleRequestActionCb yes, no;
1278        int yes_i, no_i;
1279};
1280
1281static void prplcb_request_action_yes(void *data)
1282{
1283        struct prplcb_request_action_data *pqad = data;
1284
1285        if (pqad->yes) {
1286                pqad->yes(pqad->user_data, pqad->yes_i);
1287        }
1288}
1289
1290static void prplcb_request_action_no(void *data)
1291{
1292        struct prplcb_request_action_data *pqad = data;
1293
1294        if (pqad->no) {
1295                pqad->no(pqad->user_data, pqad->no_i);
1296        }
1297}
1298
1299/* q->free() callback from query_del()*/
1300static void prplcb_request_action_free(void *data)
1301{
1302        struct prplcb_request_action_data *pqad = data;
1303
1304        pqad->bee_data = NULL;
1305        purple_request_close(PURPLE_REQUEST_ACTION, pqad);
1306}
1307
1308static void *prplcb_request_action(const char *title, const char *primary, const char *secondary,
1309                                   int default_action, PurpleAccount *account, const char *who,
1310                                   PurpleConversation *conv, void *user_data, size_t action_count,
1311                                   va_list actions)
1312{
1313        struct prplcb_request_action_data *pqad;
1314        int i;
1315        char *q;
1316
1317        pqad = g_new0(struct prplcb_request_action_data, 1);
1318
1319        for (i = 0; i < action_count; i++) {
1320                char *caption;
1321                void *fn;
1322
1323                caption = va_arg(actions, char*);
1324                fn = va_arg(actions, void*);
1325
1326                if (strstr(caption, "Accept") || strstr(caption, "OK")) {
1327                        pqad->yes = fn;
1328                        pqad->yes_i = i;
1329                } else if (strstr(caption, "Reject") || strstr(caption, "Cancel")) {
1330                        pqad->no = fn;
1331                        pqad->no_i = i;
1332                }
1333        }
1334
1335        pqad->user_data = user_data;
1336
1337        /* TODO: IRC stuff here :-( */
1338        q = g_strdup_printf("Request: %s\n\n%s\n\n%s", title, primary, secondary);
1339        pqad->bee_data = query_add(local_bee->ui_data, purple_ic_by_pa(account), q,
1340                                   prplcb_request_action_yes, prplcb_request_action_no,
1341                                   prplcb_request_action_free, pqad);
1342
1343        g_free(q);
1344
1345        return pqad;
1346}
1347
1348/* So it turns out some requests have no account context at all, because
1349 * libpurple hates us. This means that query_del_by_conn() won't remove those
1350 * on logout, and will segfault if the user replies. That's why this exists.
1351 */
1352static void prplcb_close_request(PurpleRequestType type, void *data)
1353{
1354        struct prplcb_request_action_data *pqad;
1355        struct request_input_data *ri;
1356        struct purple_data *pd;
1357
1358        if (!data) {
1359                return;
1360        }
1361
1362        switch (type) {
1363        case PURPLE_REQUEST_ACTION:
1364                pqad = data;
1365                /* if this is null, it's because query_del was run already */
1366                if (pqad->bee_data) {
1367                        query_del(local_bee->ui_data, pqad->bee_data);
1368                }
1369                g_free(pqad);
1370                break;
1371        case PURPLE_REQUEST_INPUT:
1372                ri = data;
1373                pd = ri->ic->proto_data;
1374                imcb_remove_buddy(ri->ic, ri->buddy, NULL);
1375                g_free(ri->buddy);
1376                g_hash_table_remove(pd->input_requests, GUINT_TO_POINTER(ri->id));
1377                break;
1378        default:
1379                g_free(data);
1380                break;
1381        }
1382
1383}
1384
1385void* prplcb_request_input(const char *title, const char *primary,
1386        const char *secondary, const char *default_value, gboolean multiline,
1387        gboolean masked, gchar *hint, const char *ok_text, GCallback ok_cb,
1388        const char *cancel_text, GCallback cancel_cb, PurpleAccount *account,
1389        const char *who, PurpleConversation *conv, void *user_data)
1390{
1391        struct im_connection *ic = purple_ic_by_pa(account);
1392        struct purple_data *pd = ic->proto_data;
1393        struct request_input_data *ri;
1394        guint id;
1395
1396        /* hack so that jabber's chat list doesn't ask for conference server twice */
1397        if (pd->chat_list_server && title && g_strcmp0(title, "Enter a Conference Server") == 0) {
1398                ((ri_callback_t) ok_cb)(user_data, pd->chat_list_server);
1399                g_free(pd->chat_list_server);
1400                pd->chat_list_server = NULL;
1401                return NULL;
1402        }
1403
1404        id = pd->next_request_id++;
1405        ri = g_new0(struct request_input_data, 1);
1406
1407        ri->id = id;
1408        ri->ic = ic;
1409        ri->buddy = g_strdup_printf("%s_%u", PURPLE_REQUEST_HANDLE, id);
1410        ri->data_callback = (ri_callback_t) ok_cb;
1411        ri->user_data = user_data;
1412        g_hash_table_insert(pd->input_requests, GUINT_TO_POINTER(id), ri);
1413
1414        imcb_add_buddy(ic, ri->buddy, NULL);
1415
1416        if (title && *title) {
1417                imcb_buddy_msg(ic, ri->buddy, title, 0, 0);
1418        }
1419
1420        if (primary && *primary) {
1421                imcb_buddy_msg(ic, ri->buddy, primary, 0, 0);
1422        }
1423
1424        if (secondary && *secondary) {
1425                imcb_buddy_msg(ic, ri->buddy, secondary, 0, 0);
1426        }
1427
1428        return ri;
1429}
1430
1431void purple_request_input_callback(guint id, struct im_connection *ic,
1432                                   const char *message, const char *who)
1433{
1434        struct purple_data *pd = ic->proto_data;
1435        struct request_input_data *ri;
1436
1437        if (!(ri = g_hash_table_lookup(pd->input_requests, GUINT_TO_POINTER(id)))) {
1438                return;
1439        }
1440
1441        ri->data_callback(ri->user_data, message);
1442
1443        purple_request_close(PURPLE_REQUEST_INPUT, ri);
1444}
1445
1446
1447static PurpleRequestUiOps bee_request_uiops =
1448{
1449        prplcb_request_input,      /* request_input */
1450        NULL,                      /* request_choice */
1451        prplcb_request_action,     /* request_action */
1452        NULL,                      /* request_fields */
1453        NULL,                      /* request_file */
1454        prplcb_close_request,      /* close_request */
1455        NULL,                      /* request_folder */
1456};
1457
1458static void prplcb_privacy_permit_added(PurpleAccount *account, const char *name)
1459{
1460        struct im_connection *ic = purple_ic_by_pa(account);
1461
1462        if (!g_slist_find_custom(ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp)) {
1463                ic->permit = g_slist_prepend(ic->permit, g_strdup(name));
1464        }
1465}
1466
1467static void prplcb_privacy_permit_removed(PurpleAccount *account, const char *name)
1468{
1469        struct im_connection *ic = purple_ic_by_pa(account);
1470        void *n;
1471
1472        n = g_slist_find_custom(ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp);
1473        ic->permit = g_slist_remove(ic->permit, n);
1474}
1475
1476static void prplcb_privacy_deny_added(PurpleAccount *account, const char *name)
1477{
1478        struct im_connection *ic = purple_ic_by_pa(account);
1479
1480        if (!g_slist_find_custom(ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp)) {
1481                ic->deny = g_slist_prepend(ic->deny, g_strdup(name));
1482        }
1483}
1484
1485static void prplcb_privacy_deny_removed(PurpleAccount *account, const char *name)
1486{
1487        struct im_connection *ic = purple_ic_by_pa(account);
1488        void *n;
1489
1490        n = g_slist_find_custom(ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp);
1491        ic->deny = g_slist_remove(ic->deny, n);
1492}
1493
1494static PurplePrivacyUiOps bee_privacy_uiops =
1495{
1496        prplcb_privacy_permit_added,       /* permit_added */
1497        prplcb_privacy_permit_removed,     /* permit_removed */
1498        prplcb_privacy_deny_added,         /* deny_added */
1499        prplcb_privacy_deny_removed,       /* deny_removed */
1500};
1501
1502static void prplcb_roomlist_create(PurpleRoomlist *list)
1503{
1504        struct purple_roomlist_data *rld;
1505
1506        list->ui_data = rld = g_new0(struct purple_roomlist_data, 1);
1507        rld->topic = -1;
1508}
1509
1510static void prplcb_roomlist_set_fields(PurpleRoomlist *list, GList *fields)
1511{
1512        gint topic = -1;
1513        GList *l;
1514        guint i;
1515        PurpleRoomlistField *field;
1516        struct purple_roomlist_data *rld = list->ui_data;
1517
1518        for (i = 0, l = fields; l; i++, l = l->next) {
1519                field = l->data;
1520
1521                /* Use the first visible string field as a fallback topic */
1522                if (i != 0 && topic < 0 && !field->hidden &&
1523                    field->type == PURPLE_ROOMLIST_FIELD_STRING) {
1524                        topic = i;
1525                }
1526
1527                if ((g_strcasecmp(field->name, "description") == 0) ||
1528                    (g_strcasecmp(field->name, "topic") == 0)) {
1529                        if (field->type == PURPLE_ROOMLIST_FIELD_STRING) {
1530                                rld->topic = i;
1531                        }
1532                }
1533        }
1534
1535        if (rld->topic < 0) {
1536                rld->topic = topic;
1537        }
1538}
1539
1540static char *prplcb_roomlist_get_room_name(PurpleRoomlist *list, PurpleRoomlistRoom *room)
1541{
1542        struct im_connection *ic = purple_ic_by_pa(list->account);
1543        struct purple_data *pd = ic->proto_data;
1544        PurplePlugin *prpl = purple_plugins_find_with_id(pd->account->protocol_id);
1545        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
1546
1547        if (pi && pi->roomlist_room_serialize) {
1548                return pi->roomlist_room_serialize(room);
1549        } else {
1550                return g_strdup(purple_roomlist_room_get_name(room));
1551        }
1552}
1553
1554static void prplcb_roomlist_add_room(PurpleRoomlist *list, PurpleRoomlistRoom *room)
1555{
1556        bee_chat_info_t *ci;
1557        char *title;
1558        const char *topic;
1559        GList *fields;
1560        struct purple_roomlist_data *rld = list->ui_data;
1561
1562        fields = purple_roomlist_room_get_fields(room);
1563        title = prplcb_roomlist_get_room_name(list, room);
1564
1565        if (rld->topic >= 0) {
1566                topic = g_list_nth_data(fields, rld->topic);
1567        } else {
1568                topic = NULL;
1569        }
1570
1571        ci = g_new(bee_chat_info_t, 1);
1572        ci->title = title;
1573        ci->topic = g_strdup(topic);
1574        rld->chats = g_slist_prepend(rld->chats, ci);
1575}
1576
1577static void prplcb_roomlist_in_progress(PurpleRoomlist *list, gboolean in_progress)
1578{
1579        struct im_connection *ic;
1580        struct purple_data *pd;
1581        struct purple_roomlist_data *rld = list->ui_data;
1582
1583        if (in_progress || !rld) {
1584                return;
1585        }
1586
1587        ic = purple_ic_by_pa(list->account);
1588        imcb_chat_list_free(ic);
1589
1590        pd = ic->proto_data;
1591        g_free(pd->chat_list_server);
1592        pd->chat_list_server = NULL;
1593
1594        ic->chatlist = g_slist_reverse(rld->chats);
1595        rld->chats = NULL;
1596
1597        imcb_chat_list_finish(ic);
1598
1599        if (rld->initialized) {
1600                purple_roomlist_unref(list);
1601        }
1602}
1603
1604static void prplcb_roomlist_destroy(PurpleRoomlist *list)
1605{
1606        g_free(list->ui_data);
1607        list->ui_data = NULL;
1608}
1609
1610static PurpleRoomlistUiOps bee_roomlist_uiops =
1611{
1612        NULL,                         /* show_with_account */
1613        prplcb_roomlist_create,       /* create */
1614        prplcb_roomlist_set_fields,   /* set_fields */
1615        prplcb_roomlist_add_room,     /* add_room */
1616        prplcb_roomlist_in_progress,  /* in_progress */
1617        prplcb_roomlist_destroy,      /* destroy */
1618};
1619
1620static void prplcb_debug_print(PurpleDebugLevel level, const char *category, const char *arg_s)
1621{
1622        fprintf(stderr, "DEBUG %s: %s", category, arg_s);
1623}
1624
1625static PurpleDebugUiOps bee_debug_uiops =
1626{
1627        prplcb_debug_print,        /* print */
1628};
1629
1630static guint prplcb_ev_timeout_add(guint interval, GSourceFunc func, gpointer udata)
1631{
1632        return b_timeout_add(interval, (b_event_handler) func, udata);
1633}
1634
1635static guint prplcb_ev_input_add(int fd, PurpleInputCondition cond, PurpleInputFunction func, gpointer udata)
1636{
1637        return b_input_add(fd, cond | B_EV_FLAG_FORCE_REPEAT, (b_event_handler) func, udata);
1638}
1639
1640static gboolean prplcb_ev_remove(guint id)
1641{
1642        b_event_remove((gint) id);
1643        return TRUE;
1644}
1645
1646static PurpleEventLoopUiOps glib_eventloops =
1647{
1648        prplcb_ev_timeout_add,     /* timeout_add */
1649        prplcb_ev_remove,          /* timeout_remove */
1650        prplcb_ev_input_add,       /* input_add */
1651        prplcb_ev_remove,          /* input_remove */
1652};
1653
1654/* Absolutely no connection context at all. Thanks purple! brb crying */
1655static void *prplcb_notify_message(PurpleNotifyMsgType type, const char *title,
1656                                   const char *primary, const char *secondary)
1657{
1658        char *text = g_strdup_printf("%s%s - %s%s%s",
1659                (type == PURPLE_NOTIFY_MSG_ERROR) ? "Error: " : "",
1660                title,
1661                primary ?: "",
1662                (primary && secondary) ? " - " : "",
1663                secondary ?: ""
1664        );
1665
1666        if (local_bee->ui->log) {
1667                local_bee->ui->log(local_bee, "purple", text);
1668        }
1669
1670        g_free(text);
1671
1672        return NULL;
1673}
1674
1675static void *prplcb_notify_email(PurpleConnection *gc, const char *subject, const char *from,
1676                                 const char *to, const char *url)
1677{
1678        struct im_connection *ic = purple_ic_by_gc(gc);
1679
1680        imcb_notify_email(ic, "Received e-mail from %s for %s: %s <%s>", from, to, subject, url);
1681
1682        return NULL;
1683}
1684
1685static void *prplcb_notify_userinfo(PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info)
1686{
1687        struct im_connection *ic = purple_ic_by_gc(gc);
1688        GString *info = g_string_new("");
1689        GList *l = purple_notify_user_info_get_entries(user_info);
1690        char *key;
1691        const char *value;
1692        int n;
1693
1694        while (l) {
1695                PurpleNotifyUserInfoEntry *e = l->data;
1696
1697                switch (purple_notify_user_info_entry_get_type(e)) {
1698                case PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR:
1699                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_HEADER:
1700                        key = g_strdup(purple_notify_user_info_entry_get_label(e));
1701                        value = purple_notify_user_info_entry_get_value(e);
1702
1703                        if (key) {
1704                                strip_html(key);
1705                                g_string_append_printf(info, "%s: ", key);
1706
1707                                if (value) {
1708                                        n = strlen(value) - 1;
1709                                        while (g_ascii_isspace(value[n])) {
1710                                                n--;
1711                                        }
1712                                        g_string_append_len(info, value, n + 1);
1713                                }
1714                                g_string_append_c(info, '\n');
1715                                g_free(key);
1716                        }
1717
1718                        break;
1719                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK:
1720                        g_string_append(info, "------------------------\n");
1721                        break;
1722                }
1723
1724                l = l->next;
1725        }
1726
1727        imcb_log(ic, "User %s info:\n%s", who, info->str);
1728        g_string_free(info, TRUE);
1729
1730        return NULL;
1731}
1732
1733static PurpleNotifyUiOps bee_notify_uiops =
1734{
1735        prplcb_notify_message,     /* notify_message */
1736        prplcb_notify_email,       /* notify_email */
1737        NULL,                      /* notify_emails */
1738        NULL,                      /* notify_formatted */
1739        NULL,                      /* notify_searchresults */
1740        NULL,                      /* notify_searchresults_new_rows */
1741        prplcb_notify_userinfo,    /* notify_userinfo */
1742};
1743
1744static void *prplcb_account_request_authorize(PurpleAccount *account, const char *remote_user,
1745                                              const char *id, const char *alias, const char *message, gboolean on_list,
1746                                              PurpleAccountRequestAuthorizationCb authorize_cb,
1747                                              PurpleAccountRequestAuthorizationCb deny_cb, void *user_data)
1748{
1749        struct im_connection *ic = purple_ic_by_pa(account);
1750        char *q;
1751
1752        if (alias) {
1753                q = g_strdup_printf("%s (%s) wants to add you to his/her contact "
1754                                    "list. (%s)", alias, remote_user, message);
1755        } else {
1756                q = g_strdup_printf("%s wants to add you to his/her contact "
1757                                    "list. (%s)", remote_user, message);
1758        }
1759
1760        imcb_ask_with_free(ic, q, user_data, authorize_cb, deny_cb, NULL);
1761        g_free(q);
1762
1763        return NULL;
1764}
1765
1766static PurpleAccountUiOps bee_account_uiops =
1767{
1768        NULL,                              /* notify_added */
1769        NULL,                              /* status_changed */
1770        NULL,                              /* request_add */
1771        prplcb_account_request_authorize,  /* request_authorize */
1772        NULL,                              /* close_account_request */
1773};
1774
1775static void *prplcb_bitlbee_set_account_password(PurpleAccount *account, char *password)
1776{
1777        struct im_connection *ic = purple_ic_by_pa(account);
1778
1779        set_setstr(&ic->acc->set, "password", password ? password : "");
1780
1781        return GINT_TO_POINTER(TRUE);
1782}
1783
1784extern PurpleXferUiOps bee_xfer_uiops;
1785
1786static void purple_ui_init()
1787{
1788        purple_connections_set_ui_ops(&bee_conn_uiops);
1789        purple_blist_set_ui_ops(&bee_blist_uiops);
1790        purple_conversations_set_ui_ops(&bee_conv_uiops);
1791        purple_request_set_ui_ops(&bee_request_uiops);
1792        purple_privacy_set_ui_ops(&bee_privacy_uiops);
1793        purple_roomlist_set_ui_ops(&bee_roomlist_uiops);
1794        purple_notify_set_ui_ops(&bee_notify_uiops);
1795        purple_accounts_set_ui_ops(&bee_account_uiops);
1796        purple_xfers_set_ui_ops(&bee_xfer_uiops);
1797
1798        if (getenv("BITLBEE_DEBUG")) {
1799                purple_debug_set_ui_ops(&bee_debug_uiops);
1800        }
1801}
1802
1803/* borrowing this semi-private function
1804 * TODO: figure out a better interface later (famous last words) */
1805gboolean plugin_info_add(struct plugin_info *info);
1806
1807void purple_initmodule()
1808{
1809        struct prpl funcs;
1810        GList *prots;
1811        GString *help;
1812        char *dir;
1813        gboolean debug_enabled = !!getenv("BITLBEE_DEBUG");
1814
1815        if (purple_get_core() != NULL) {
1816                log_message(LOGLVL_ERROR, "libpurple already initialized. "
1817                            "Please use inetd or ForkDaemon mode instead.");
1818                return;
1819        }
1820
1821        g_return_if_fail((int) B_EV_IO_READ == (int) PURPLE_INPUT_READ);
1822        g_return_if_fail((int) B_EV_IO_WRITE == (int) PURPLE_INPUT_WRITE);
1823
1824        dir = g_strdup_printf("%s/purple", global.conf->configdir);
1825        purple_util_set_user_dir(dir);
1826        g_free(dir);
1827
1828        dir = g_strdup_printf("%s/purple", global.conf->plugindir);
1829        purple_plugins_add_search_path(dir);
1830        g_free(dir);
1831
1832        purple_debug_set_enabled(debug_enabled);
1833        purple_core_set_ui_ops(&bee_core_uiops);
1834        purple_eventloop_set_ui_ops(&glib_eventloops);
1835        if (!purple_core_init("BitlBee")) {
1836                /* Initializing the core failed. Terminate. */
1837                fprintf(stderr, "libpurple initialization failed.\n");
1838                abort();
1839        }
1840        purple_debug_set_enabled(FALSE);
1841
1842        if (proxytype != PROXY_NONE) {
1843                PurpleProxyInfo *pi = purple_global_proxy_get_info();
1844                switch (proxytype) {
1845                case PROXY_SOCKS4A:
1846                case PROXY_SOCKS4:
1847                        purple_proxy_info_set_type(pi, PURPLE_PROXY_SOCKS4);
1848                        break;
1849                case PROXY_SOCKS5:
1850                        purple_proxy_info_set_type(pi, PURPLE_PROXY_SOCKS5);
1851                        break;
1852                case PROXY_HTTP:
1853                        purple_proxy_info_set_type(pi, PURPLE_PROXY_HTTP);
1854                        break;
1855                }
1856                purple_proxy_info_set_host(pi, proxyhost);
1857                purple_proxy_info_set_port(pi, proxyport);
1858                purple_proxy_info_set_username(pi, proxyuser);
1859                purple_proxy_info_set_password(pi, proxypass);
1860        }
1861
1862        purple_set_blist(purple_blist_new());
1863
1864        /* No, really. So far there were ui_ops for everything, but now suddenly
1865           one needs to use signals for typing notification stuff. :-( */
1866        purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
1867                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1868        purple_signal_connect(purple_conversations_get_handle(), "buddy-typed",
1869                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1870        purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
1871                              &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL);
1872
1873        /* "bitlbee-set-account-password" signal:
1874         * Replacement API for purple_account_set_password() to be called by
1875         * prpls that wish to store updated passwords or oauth tokens, since
1876         * our password storage doesn't get notified of calls to
1877         * purple_account_set_password().
1878         *
1879         * When used with purple_signal_emit_return_1() returns:
1880         *  - GINT_TO_POINTER(TRUE) if implemented by this version of bitlbee
1881         *  - NULL otherwise.
1882         *
1883         * Originally made for the hangouts plugin.
1884         */
1885
1886        purple_signal_register(purple_accounts_get_handle(), "bitlbee-set-account-password",
1887                               purple_marshal_POINTER__POINTER_POINTER,
1888                               purple_value_new(PURPLE_TYPE_BOOLEAN), 2,
1889                               purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_ACCOUNT),
1890                               purple_value_new(PURPLE_TYPE_STRING));
1891
1892        purple_signal_connect(purple_accounts_get_handle(), "bitlbee-set-account-password",
1893                              &funcs, PURPLE_CALLBACK(prplcb_bitlbee_set_account_password), NULL);
1894
1895        memset(&funcs, 0, sizeof(funcs));
1896        funcs.login = purple_login;
1897        funcs.init = purple_init;
1898        funcs.logout = purple_logout;
1899        funcs.buddy_msg = purple_buddy_msg;
1900        funcs.away_states = purple_away_states;
1901        funcs.set_away = purple_set_away;
1902        funcs.add_buddy = purple_add_buddy;
1903        funcs.remove_buddy = purple_remove_buddy;
1904        funcs.add_permit = purple_add_permit;
1905        funcs.add_deny = purple_add_deny;
1906        funcs.rem_permit = purple_rem_permit;
1907        funcs.rem_deny = purple_rem_deny;
1908        funcs.get_info = purple_get_info;
1909        funcs.keepalive = purple_keepalive;
1910        funcs.send_typing = purple_send_typing;
1911        funcs.handle_cmp = g_strcasecmp;
1912        /* TODO(wilmer): Set these only for protocols that support them? */
1913        funcs.chat_msg = purple_chat_msg;
1914        funcs.chat_with = purple_chat_with;
1915        funcs.chat_invite = purple_chat_invite;
1916        funcs.chat_topic = purple_chat_set_topic;
1917        funcs.chat_kick = purple_chat_kick;
1918        funcs.chat_leave = purple_chat_leave;
1919        funcs.chat_join = purple_chat_join;
1920        funcs.chat_list = purple_chat_list;
1921        funcs.chat_add_settings = purple_chat_add_settings;
1922        funcs.chat_free_settings = purple_chat_free_settings;
1923        funcs.transfer_request = purple_transfer_request;
1924
1925        help = g_string_new("BitlBee libpurple module supports the following IM protocols:\n");
1926
1927        /* Add a protocol entry to BitlBee's structures for every protocol
1928           supported by this libpurple instance. */
1929        for (prots = purple_plugins_get_protocols(); prots; prots = prots->next) {
1930                PurplePlugin *prot = prots->data;
1931                PurplePluginProtocolInfo *pi = prot->info->extra_info;
1932                struct prpl *ret;
1933                struct plugin_info *info;
1934
1935                /* If we already have this one (as a native module), don't
1936                   add a libpurple duplicate. */
1937                if (find_protocol(prot->info->id)) {
1938                        continue;
1939                }
1940
1941                ret = g_memdup(&funcs, sizeof(funcs));
1942                ret->name = ret->data = prot->info->id;
1943                if (strncmp(ret->name, "prpl-", 5) == 0) {
1944                        ret->name += 5;
1945                }
1946
1947                if (pi->options & OPT_PROTO_NO_PASSWORD) {
1948                        ret->options |= PRPL_OPT_NO_PASSWORD;
1949                }
1950
1951                if (pi->options & OPT_PROTO_PASSWORD_OPTIONAL) {
1952                        ret->options |= PRPL_OPT_PASSWORD_OPTIONAL;
1953                }
1954
1955                register_protocol(ret);
1956
1957                g_string_append_printf(help, "\n* %s (%s)", ret->name, prot->info->name);
1958
1959                /* libpurple doesn't define a protocol called OSCAR, but we
1960                   need it to be compatible with normal BitlBee. */
1961                if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) {
1962                        ret = g_memdup(&funcs, sizeof(funcs));
1963                        ret->name = "oscar";
1964                        /* purple_get_account_prpl_id() determines the actual protocol ID (icq/aim) */
1965                        ret->data = NULL;
1966                        register_protocol(ret);
1967                }
1968
1969                info = g_new0(struct plugin_info, 1);
1970                info->abiver = BITLBEE_ABI_VERSION_CODE;
1971                info->name = ret->name;
1972                info->version = prot->info->version;
1973                info->description = prot->info->description;
1974                info->author = prot->info->author;
1975                info->url = prot->info->homepage;
1976
1977                plugin_info_add(info);
1978        }
1979
1980        g_string_append(help, "\n\nFor used protocols, more information about available "
1981                        "settings can be found using \x02help purple <protocol name>\x02 "
1982                        "(create an account using that protocol first!)");
1983
1984        /* Add a simple dynamically-generated help item listing all
1985           the supported protocols. */
1986        help_add_mem(&global.help, "purple", help->str);
1987        g_string_free(help, TRUE);
1988}
Note: See TracBrowser for help on using the repository browser.