source: protocols/purple/purple.c @ 36ee8c6

Last change on this file since 36ee8c6 was 36ee8c6, checked in by Antoine Pietri <antoine.pietri@…>, at 2015-01-25T20:45:16Z

purple: add topic and name_hint to groupchats

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