source: protocols/purple/purple.c @ 1388d30

Last change on this file since 1388d30 was eb4c81a, checked in by Wilmer van der Gaast <wilmer@…>, at 2012-01-30T22:23:45Z

Try to show the user_split info for a protocol in "help purple $PROTOCOL".
This in response to #898. I could also try to make each split a separate
setting but that'd make the Jabber module very ugly for example, requiring
the user to split up his/her JID. Let's hope people read this help info..

  • Property mode set to 100644
File size: 37.9 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  libpurple module - Main file                                             *
5*                                                                           *
6*  Copyright 2009-2010 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( tolower( *a ) != 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                conv->ui_data = gc;
835                gc->data = conv;
836               
837                /* libpurple brokenness: Whatever. Show that we join right away,
838                   there's no clear "This is you!" signaling in _add_users so
839                   don't even try. */
840                imcb_chat_add_buddy( gc, gc->ic->acc->user );
841        }
842}
843
844void prplcb_conv_free( PurpleConversation *conv )
845{
846        struct groupchat *gc = conv->ui_data;
847       
848        imcb_chat_free( gc );
849}
850
851void prplcb_conv_add_users( PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals )
852{
853        struct groupchat *gc = conv->ui_data;
854        GList *b;
855       
856        for( b = cbuddies; b; b = b->next )
857        {
858                PurpleConvChatBuddy *pcb = b->data;
859               
860                imcb_chat_add_buddy( gc, pcb->name );
861        }
862}
863
864void prplcb_conv_del_users( PurpleConversation *conv, GList *cbuddies )
865{
866        struct groupchat *gc = conv->ui_data;
867        GList *b;
868       
869        for( b = cbuddies; b; b = b->next )
870                imcb_chat_remove_buddy( gc, b->data, "" );
871}
872
873void prplcb_conv_chat_msg( PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime )
874{
875        struct groupchat *gc = conv->ui_data;
876        PurpleBuddy *buddy;
877       
878        /* ..._SEND means it's an outgoing message, no need to echo those. */
879        if( flags & PURPLE_MESSAGE_SEND )
880                return;
881       
882        buddy = purple_find_buddy( conv->account, who );
883        if( buddy != NULL )
884                who = purple_buddy_get_name( buddy );
885       
886        imcb_chat_msg( gc, who, (char*) message, 0, mtime );
887}
888
889static void prplcb_conv_im( PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime )
890{
891        struct im_connection *ic = purple_ic_by_pa( conv->account );
892        PurpleBuddy *buddy;
893       
894        /* ..._SEND means it's an outgoing message, no need to echo those. */
895        if( flags & PURPLE_MESSAGE_SEND )
896                return;
897       
898        buddy = purple_find_buddy( conv->account, who );
899        if( buddy != NULL )
900                who = purple_buddy_get_name( buddy );
901       
902        imcb_buddy_msg( ic, (char*) who, (char*) message, 0, mtime );
903}
904
905/* No, this is not a ui_op but a signal. */
906static void prplcb_buddy_typing( PurpleAccount *account, const char *who, gpointer null )
907{
908        PurpleConversation *conv;
909        PurpleConvIm *im;
910        int state;
911       
912        if( ( conv = purple_find_conversation_with_account( PURPLE_CONV_TYPE_IM, who, account ) ) == NULL )
913                return;
914       
915        im = PURPLE_CONV_IM(conv);
916        switch( purple_conv_im_get_typing_state( im ) )
917        {
918        case PURPLE_TYPING:
919                state = OPT_TYPING;
920                break;
921        case PURPLE_TYPED:
922                state = OPT_THINKING;
923                break;
924        default:
925                state = 0;
926        }
927       
928        imcb_buddy_typing( purple_ic_by_pa( account ), who, state );
929}
930
931static PurpleConversationUiOps bee_conv_uiops = 
932{
933        prplcb_conv_new,           /* create_conversation  */
934        prplcb_conv_free,          /* destroy_conversation */
935        prplcb_conv_chat_msg,      /* write_chat           */
936        prplcb_conv_im,            /* write_im             */
937        NULL,                      /* write_conv           */
938        prplcb_conv_add_users,     /* chat_add_users       */
939        NULL,                      /* chat_rename_user     */
940        prplcb_conv_del_users,     /* chat_remove_users    */
941        NULL,                      /* chat_update_user     */
942        NULL,                      /* present              */
943        NULL,                      /* has_focus            */
944        NULL,                      /* custom_smiley_add    */
945        NULL,                      /* custom_smiley_write  */
946        NULL,                      /* custom_smiley_close  */
947        NULL,                      /* send_confirm         */
948};
949
950struct prplcb_request_action_data
951{
952        void *user_data, *bee_data;
953        PurpleRequestActionCb yes, no;
954        int yes_i, no_i;
955};
956
957static void prplcb_request_action_yes( void *data )
958{
959        struct prplcb_request_action_data *pqad = data;
960       
961        if( pqad->yes )
962                pqad->yes( pqad->user_data, pqad->yes_i );
963        g_free( pqad );
964}
965
966static void prplcb_request_action_no( void *data )
967{
968        struct prplcb_request_action_data *pqad = data;
969       
970        if( pqad->no )
971                pqad->no( pqad->user_data, pqad->no_i );
972        g_free( pqad );
973}
974
975static void *prplcb_request_action( const char *title, const char *primary, const char *secondary,
976                                    int default_action, PurpleAccount *account, const char *who,
977                                    PurpleConversation *conv, void *user_data, size_t action_count,
978                                    va_list actions )
979{
980        struct prplcb_request_action_data *pqad; 
981        int i;
982        char *q;
983       
984        pqad = g_new0( struct prplcb_request_action_data, 1 );
985       
986        for( i = 0; i < action_count; i ++ )
987        {
988                char *caption;
989                void *fn;
990               
991                caption = va_arg( actions, char* );
992                fn = va_arg( actions, void* );
993               
994                if( strstr( caption, "Accept" ) || strstr( caption, "OK" ) )
995                {
996                        pqad->yes = fn;
997                        pqad->yes_i = i;
998                }
999                else if( strstr( caption, "Reject" ) || strstr( caption, "Cancel" ) )
1000                {
1001                        pqad->no = fn;
1002                        pqad->no_i = i;
1003                }
1004        }
1005       
1006        pqad->user_data = user_data;
1007       
1008        /* TODO: IRC stuff here :-( */
1009        q = g_strdup_printf( "Request: %s\n\n%s\n\n%s", title, primary, secondary );
1010        pqad->bee_data = query_add( local_bee->ui_data, purple_ic_by_pa( account ), q,
1011                prplcb_request_action_yes, prplcb_request_action_no, g_free, pqad );
1012       
1013        g_free( q );
1014       
1015        return pqad;
1016}
1017
1018/*
1019static void prplcb_request_test()
1020{
1021        fprintf( stderr, "bla\n" );
1022}
1023*/
1024
1025static PurpleRequestUiOps bee_request_uiops =
1026{
1027        NULL,
1028        NULL,
1029        prplcb_request_action,
1030        NULL,
1031        NULL,
1032        NULL,
1033        NULL,
1034};
1035
1036static void prplcb_privacy_permit_added( PurpleAccount *account, const char *name )
1037{
1038        struct im_connection *ic = purple_ic_by_pa( account );
1039       
1040        if( !g_slist_find_custom( ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp ) )
1041                ic->permit = g_slist_prepend( ic->permit, g_strdup( name ) );
1042}
1043
1044static void prplcb_privacy_permit_removed( PurpleAccount *account, const char *name )
1045{
1046        struct im_connection *ic = purple_ic_by_pa( account );
1047        void *n;
1048       
1049        n = g_slist_find_custom( ic->permit, name, (GCompareFunc) ic->acc->prpl->handle_cmp );
1050        ic->permit = g_slist_remove( ic->permit, n );
1051}
1052
1053static void prplcb_privacy_deny_added( PurpleAccount *account, const char *name )
1054{
1055        struct im_connection *ic = purple_ic_by_pa( account );
1056       
1057        if( !g_slist_find_custom( ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp ) )
1058                ic->deny = g_slist_prepend( ic->deny, g_strdup( name ) );
1059}
1060
1061static void prplcb_privacy_deny_removed( PurpleAccount *account, const char *name )
1062{
1063        struct im_connection *ic = purple_ic_by_pa( account );
1064        void *n;
1065       
1066        n = g_slist_find_custom( ic->deny, name, (GCompareFunc) ic->acc->prpl->handle_cmp );
1067        ic->deny = g_slist_remove( ic->deny, n );
1068}
1069
1070static PurplePrivacyUiOps bee_privacy_uiops =
1071{
1072        prplcb_privacy_permit_added,
1073        prplcb_privacy_permit_removed,
1074        prplcb_privacy_deny_added,
1075        prplcb_privacy_deny_removed,
1076};
1077
1078static void prplcb_debug_print( PurpleDebugLevel level, const char *category, const char *arg_s )
1079{
1080        fprintf( stderr, "DEBUG %s: %s", category, arg_s );
1081}
1082
1083static PurpleDebugUiOps bee_debug_uiops =
1084{
1085        prplcb_debug_print,
1086};
1087
1088static guint prplcb_ev_timeout_add( guint interval, GSourceFunc func, gpointer udata )
1089{
1090        return b_timeout_add( interval, (b_event_handler) func, udata );
1091}
1092
1093static guint prplcb_ev_input_add( int fd, PurpleInputCondition cond, PurpleInputFunction func, gpointer udata )
1094{
1095        return b_input_add( fd, cond | B_EV_FLAG_FORCE_REPEAT, (b_event_handler) func, udata );
1096}
1097
1098static gboolean prplcb_ev_remove( guint id )
1099{
1100        b_event_remove( (gint) id );
1101        return TRUE;
1102}
1103
1104static PurpleEventLoopUiOps glib_eventloops = 
1105{
1106        prplcb_ev_timeout_add,
1107        prplcb_ev_remove,
1108        prplcb_ev_input_add,
1109        prplcb_ev_remove,
1110};
1111
1112static void *prplcb_notify_email( PurpleConnection *gc, const char *subject, const char *from,
1113                                  const char *to, const char *url )
1114{
1115        struct im_connection *ic = purple_ic_by_gc( gc );
1116       
1117        imcb_log( ic, "Received e-mail from %s for %s: %s <%s>", from, to, subject, url );
1118       
1119        return NULL;
1120}
1121
1122static void *prplcb_notify_userinfo( PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info )
1123{
1124        struct im_connection *ic = purple_ic_by_gc( gc );
1125        GString *info = g_string_new( "" );
1126        GList *l = purple_notify_user_info_get_entries( user_info );
1127        char *key;
1128        const char *value;
1129        int n;
1130       
1131        while( l )
1132        {
1133                PurpleNotifyUserInfoEntry *e = l->data;
1134               
1135                switch( purple_notify_user_info_entry_get_type( e ) )
1136                {
1137                case PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR:
1138                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_HEADER:
1139                        key = g_strdup( purple_notify_user_info_entry_get_label( e ) );
1140                        value = purple_notify_user_info_entry_get_value( e );
1141                       
1142                        if( key )
1143                        {
1144                                strip_html( key );
1145                                g_string_append_printf( info, "%s: ", key );
1146                               
1147                                if( value )
1148                                {
1149                                        n = strlen( value ) - 1;
1150                                        while( isspace( value[n] ) )
1151                                                n --;
1152                                        g_string_append_len( info, value, n + 1 );
1153                                }
1154                                g_string_append_c( info, '\n' );
1155                                g_free( key );
1156                        }
1157                       
1158                        break;
1159                case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK:
1160                        g_string_append( info, "------------------------\n" );
1161                        break;
1162                }
1163               
1164                l = l->next;
1165        }
1166       
1167        imcb_log( ic, "User %s info:\n%s", who, info->str );
1168        g_string_free( info, TRUE );
1169       
1170        return NULL;
1171}
1172
1173static PurpleNotifyUiOps bee_notify_uiops =
1174{
1175        NULL,
1176        prplcb_notify_email,
1177        NULL,
1178        NULL,
1179        NULL,
1180        NULL,
1181        prplcb_notify_userinfo,
1182};
1183
1184static void *prplcb_account_request_authorize( PurpleAccount *account, const char *remote_user,
1185        const char *id, const char *alias, const char *message, gboolean on_list,
1186        PurpleAccountRequestAuthorizationCb authorize_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data )
1187{
1188        struct im_connection *ic = purple_ic_by_pa( account );
1189        char *q;
1190       
1191        if( alias )
1192                q = g_strdup_printf( "%s (%s) wants to add you to his/her contact "
1193                                     "list. (%s)", alias, remote_user, message );
1194        else
1195                q = g_strdup_printf( "%s wants to add you to his/her contact "
1196                                     "list. (%s)", remote_user, message );
1197       
1198        imcb_ask_with_free( ic, q, user_data, authorize_cb, deny_cb, NULL );
1199        g_free( q );
1200       
1201        return NULL;
1202}
1203
1204static PurpleAccountUiOps bee_account_uiops =
1205{
1206        NULL,
1207        NULL,
1208        NULL,
1209        prplcb_account_request_authorize,
1210        NULL,
1211};
1212
1213extern PurpleXferUiOps bee_xfer_uiops;
1214
1215static void purple_ui_init()
1216{
1217        purple_connections_set_ui_ops( &bee_conn_uiops );
1218        purple_blist_set_ui_ops( &bee_blist_uiops );
1219        purple_conversations_set_ui_ops( &bee_conv_uiops );
1220        purple_request_set_ui_ops( &bee_request_uiops );
1221        purple_privacy_set_ui_ops( &bee_privacy_uiops );
1222        purple_notify_set_ui_ops( &bee_notify_uiops );
1223        purple_accounts_set_ui_ops( &bee_account_uiops );
1224        purple_xfers_set_ui_ops( &bee_xfer_uiops );
1225       
1226        if( getenv( "BITLBEE_DEBUG" ) )
1227                purple_debug_set_ui_ops( &bee_debug_uiops );
1228}
1229
1230void purple_initmodule()
1231{
1232        struct prpl funcs;
1233        GList *prots;
1234        GString *help;
1235        char *dir;
1236       
1237        if( B_EV_IO_READ != PURPLE_INPUT_READ ||
1238            B_EV_IO_WRITE != PURPLE_INPUT_WRITE )
1239        {
1240                /* FIXME FIXME FIXME FIXME FIXME :-) */
1241                exit( 1 );
1242        }
1243       
1244        dir = g_strdup_printf( "%s/purple", global.conf->configdir );
1245        purple_util_set_user_dir( dir );
1246        g_free( dir );
1247       
1248        purple_debug_set_enabled( FALSE );
1249        purple_core_set_ui_ops( &bee_core_uiops );
1250        purple_eventloop_set_ui_ops( &glib_eventloops );
1251        if( !purple_core_init( "BitlBee") )
1252        {
1253                /* Initializing the core failed. Terminate. */
1254                fprintf( stderr, "libpurple initialization failed.\n" );
1255                abort();
1256        }
1257       
1258        if( proxytype != PROXY_NONE )
1259        {
1260                PurpleProxyInfo *pi = purple_global_proxy_get_info();
1261                switch( proxytype )
1262                {
1263                case PROXY_SOCKS4:
1264                        purple_proxy_info_set_type( pi, PURPLE_PROXY_SOCKS4 );
1265                        break;
1266                case PROXY_SOCKS5:
1267                        purple_proxy_info_set_type( pi, PURPLE_PROXY_SOCKS5 );
1268                        break;
1269                case PROXY_HTTP:
1270                        purple_proxy_info_set_type( pi, PURPLE_PROXY_HTTP );
1271                        break;
1272                } 
1273                purple_proxy_info_set_host( pi, proxyhost );
1274                purple_proxy_info_set_port( pi, proxyport );
1275                purple_proxy_info_set_username( pi, proxyuser );
1276                purple_proxy_info_set_password( pi, proxypass );
1277        }
1278       
1279        purple_set_blist( purple_blist_new() );
1280       
1281        /* No, really. So far there were ui_ops for everything, but now suddenly
1282           one needs to use signals for typing notification stuff. :-( */
1283        purple_signal_connect( purple_conversations_get_handle(), "buddy-typing",
1284                               &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL );
1285        purple_signal_connect( purple_conversations_get_handle(), "buddy-typed",
1286                               &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL );
1287        purple_signal_connect( purple_conversations_get_handle(), "buddy-typing-stopped",
1288                               &funcs, PURPLE_CALLBACK(prplcb_buddy_typing), NULL );
1289       
1290        memset( &funcs, 0, sizeof( funcs ) );
1291        funcs.login = purple_login;
1292        funcs.init = purple_init;
1293        funcs.logout = purple_logout;
1294        funcs.buddy_msg = purple_buddy_msg;
1295        funcs.away_states = purple_away_states;
1296        funcs.set_away = purple_set_away;
1297        funcs.add_buddy = purple_add_buddy;
1298        funcs.remove_buddy = purple_remove_buddy;
1299        funcs.add_permit = purple_add_permit;
1300        funcs.add_deny = purple_add_deny;
1301        funcs.rem_permit = purple_rem_permit;
1302        funcs.rem_deny = purple_rem_deny;
1303        funcs.get_info = purple_get_info;
1304        funcs.keepalive = purple_keepalive;
1305        funcs.send_typing = purple_send_typing;
1306        funcs.handle_cmp = g_strcasecmp;
1307        /* TODO(wilmer): Set these only for protocols that support them? */
1308        funcs.chat_msg = purple_chat_msg;
1309        funcs.chat_with = purple_chat_with;
1310        funcs.chat_invite = purple_chat_invite;
1311        funcs.chat_leave = purple_chat_leave;
1312        funcs.chat_join = purple_chat_join;
1313        funcs.transfer_request = purple_transfer_request;
1314       
1315        help = g_string_new( "BitlBee libpurple module supports the following IM protocols:\n" );
1316       
1317        /* Add a protocol entry to BitlBee's structures for every protocol
1318           supported by this libpurple instance. */     
1319        for( prots = purple_plugins_get_protocols(); prots; prots = prots->next )
1320        {
1321                PurplePlugin *prot = prots->data;
1322                struct prpl *ret;
1323               
1324                /* If we already have this one (as a native module), don't
1325                   add a libpurple duplicate. */
1326                if( find_protocol( prot->info->id ) )
1327                        continue;
1328               
1329                ret = g_memdup( &funcs, sizeof( funcs ) );
1330                ret->name = ret->data = prot->info->id;
1331                if( strncmp( ret->name, "prpl-", 5 ) == 0 )
1332                        ret->name += 5;
1333                register_protocol( ret );
1334               
1335                g_string_append_printf( help, "\n* %s (%s)", ret->name, prot->info->name );
1336               
1337                /* libpurple doesn't define a protocol called OSCAR, but we
1338                   need it to be compatible with normal BitlBee. */
1339                if( g_strcasecmp( prot->info->id, "prpl-aim" ) == 0 )
1340                {
1341                        ret = g_memdup( &funcs, sizeof( funcs ) );
1342                        ret->name = "oscar";
1343                        ret->data = prot->info->id;
1344                        register_protocol( ret );
1345                }
1346        }
1347       
1348        g_string_append( help, "\n\nFor used protocols, more information about available "
1349                         "settings can be found using \x02help purple <protocol name>\x02 "
1350                         "(create an account using that protocol first!)" );
1351       
1352        /* Add a simple dynamically-generated help item listing all
1353           the supported protocols. */
1354        help_add_mem( &global.help, "purple", help->str );
1355        g_string_free( help, TRUE );
1356}
Note: See TracBrowser for help on using the repository browser.