source: protocols/purple/purple.c @ e805611

Last change on this file since e805611 was 552da22, checked in by GitHub <noreply@…>, at 2023-04-01T20:29:42Z

Use g_memdup2() with glib versions >= 2.68.0 (#168)

  • Use g_memdup2() with glib versions >= 2.68.0

g_memdup() was deprecated in glib 2.68.0. The new function is
g_memdup2(). Still support building with older versions of glib via
macros.

Signed-off-by: David Cantrell <dcantrell@…>

  • Fall back to g_memdup when g_memdup2 is not available, rather than defining custom macro

Signed-off-by: David Cantrell <dcantrell@…>
Co-authored-by: Jelmer Vernooij <jelmer@…>

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