source: protocols/purple/purple.c @ 1fa5109

Last change on this file since 1fa5109 was fd213fe, checked in by Antoine Pietri <antoine.pietri@…>, at 2015-01-26T13:20:20Z

purple: add NULL guard around topic and name hint

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