source: protocols/purple/purple.c @ 4dc6b8d

Last change on this file since 4dc6b8d was 4dc6b8d, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-03-12T01:05:21Z

Added support for PURPLE_PREF_STRING_LIST style settings, this makes the
QQ module (and maybe others) work.

  • Property mode set to 100644
File size: 19.5 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 irc_t *local_irc;
38
39static struct im_connection *purple_ic_by_pa( PurpleAccount *pa )
40{
41        GSList *i;
42       
43        for( i = purple_connections; i; i = i->next )
44                if( ((struct im_connection *)i->data)->proto_data == pa )
45                        return i->data;
46       
47        return NULL;
48}
49
50static struct im_connection *purple_ic_by_gc( PurpleConnection *gc )
51{
52        return purple_ic_by_pa( purple_connection_get_account( gc ) );
53}
54
55static void purple_init( account_t *acc )
56{
57        PurplePlugin *prpl = purple_plugins_find_with_id( (char*) acc->prpl->data );
58        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
59        PurpleAccount *pa;
60        GList *i, *st;
61        set_t *s;
62       
63        /* Convert all protocol_options into per-account setting variables. */
64        for( i = pi->protocol_options; i; i = i->next )
65        {
66                PurpleAccountOption *o = i->data;
67                const char *name;
68                char *def = NULL;
69                set_eval eval = NULL;
70                void *eval_data = NULL;
71                GList *io = NULL;
72                GSList *opts = NULL;
73               
74                name = purple_account_option_get_setting( o );
75               
76                switch( purple_account_option_get_type( o ) )
77                {
78                case PURPLE_PREF_STRING:
79                        def = g_strdup( purple_account_option_get_default_string( o ) );
80                        break;
81               
82                case PURPLE_PREF_INT:
83                        def = g_strdup_printf( "%d", purple_account_option_get_default_int( o ) );
84                        eval = set_eval_int;
85                        break;
86               
87                case PURPLE_PREF_BOOLEAN:
88                        if( purple_account_option_get_default_bool( o ) )
89                                def = g_strdup( "true" );
90                        else
91                                def = g_strdup( "false" );
92                        eval = set_eval_bool;
93                        break;
94               
95                case PURPLE_PREF_STRING_LIST:
96                        def = g_strdup( purple_account_option_get_default_list_value( o ) );
97                        for( io = purple_account_option_get_list( o ); io; io = io->next )
98                        {
99                                PurpleKeyValuePair *kv = io->data;
100                                opts = g_slist_append( opts, kv->key );
101                        }
102                        eval = set_eval_list;
103                        eval_data = opts;
104                        break;
105                       
106                default:
107                        irc_usermsg( acc->irc, "Setting with unknown type: %s (%d) Expect stuff to break..\n",
108                                     name, purple_account_option_get_type( o ) );
109                        name = NULL;
110                }
111               
112                if( name != NULL )
113                {
114                        s = set_add( &acc->set, name, def, eval, acc );
115                        s->flags |= ACC_SET_OFFLINE_ONLY;
116                        s->eval_data = eval_data;
117                        g_free( def );
118                }
119        }
120       
121        if( pi->options & OPT_PROTO_MAIL_CHECK )
122        {
123                s = set_add( &acc->set, "mail_notifications", "false", set_eval_bool, acc );
124                s->flags |= ACC_SET_OFFLINE_ONLY;
125        }
126       
127        /* Go through all away states to figure out if away/status messages
128           are possible. */
129        pa = purple_account_new( acc->user, (char*) acc->prpl->data );
130        for( st = purple_account_get_status_types( pa ); st; st = st->next )
131        {
132                PurpleStatusPrimitive prim = purple_status_type_get_primitive( st->data );
133               
134                if( prim == PURPLE_STATUS_AVAILABLE )
135                {
136                        if( purple_status_type_get_attr( st->data, "message" ) )
137                                acc->flags |= ACC_FLAG_STATUS_MESSAGE;
138                }
139                else if( prim != PURPLE_STATUS_OFFLINE )
140                {
141                        if( purple_status_type_get_attr( st->data, "message" ) )
142                                acc->flags |= ACC_FLAG_AWAY_MESSAGE;
143                }
144        }
145        purple_accounts_remove( pa );
146}
147
148static void purple_sync_settings( account_t *acc, PurpleAccount *pa )
149{
150        PurplePlugin *prpl = purple_plugins_find_with_id( pa->protocol_id );
151        PurplePluginProtocolInfo *pi = prpl->info->extra_info;
152        GList *i;
153       
154        for( i = pi->protocol_options; i; i = i->next )
155        {
156                PurpleAccountOption *o = i->data;
157                const char *name;
158                set_t *s;
159               
160                name = purple_account_option_get_setting( o );
161                s = set_find( &acc->set, name );
162                if( s->value == NULL )
163                        continue;
164               
165                switch( purple_account_option_get_type( o ) )
166                {
167                case PURPLE_PREF_STRING:
168                case PURPLE_PREF_STRING_LIST:
169                        purple_account_set_string( pa, name, set_getstr( &acc->set, name ) );
170                        break;
171               
172                case PURPLE_PREF_INT:
173                        purple_account_set_int( pa, name, set_getint( &acc->set, name ) );
174                        break;
175               
176                case PURPLE_PREF_BOOLEAN:
177                        purple_account_set_bool( pa, name, set_getbool( &acc->set, name ) );
178                        break;
179               
180                default:
181                        break;
182                }
183        }
184       
185        if( pi->options & OPT_PROTO_MAIL_CHECK )
186                purple_account_set_check_mail( pa, set_getbool( &acc->set, "mail_notifications" ) );
187}
188
189static void purple_login( account_t *acc )
190{
191        struct im_connection *ic = imcb_new( acc );
192        PurpleAccount *pa;
193       
194        if( local_irc != NULL && local_irc != acc->irc )
195        {
196                irc_usermsg( acc->irc, "Daemon mode detected. Do *not* try to use libpurple in daemon mode! "
197                                       "Please use inetd or ForkDaemon mode instead." );
198                return;
199        }
200        local_irc = acc->irc;
201       
202        /* For now this is needed in the _connected() handlers if using
203           GLib event handling, to make sure we're not handling events
204           on dead connections. */
205        purple_connections = g_slist_prepend( purple_connections, ic );
206       
207        ic->proto_data = pa = purple_account_new( acc->user, (char*) acc->prpl->data );
208        purple_account_set_password( pa, acc->pass );
209        purple_sync_settings( acc, pa );
210       
211        purple_account_set_enabled( pa, "BitlBee", TRUE );
212}
213
214static void purple_logout( struct im_connection *ic )
215{
216        PurpleAccount *pa = ic->proto_data;
217       
218        purple_account_set_enabled( pa, "BitlBee", FALSE );
219        purple_connections = g_slist_remove( purple_connections, ic );
220        purple_accounts_remove( pa );
221}
222
223static int purple_buddy_msg( struct im_connection *ic, char *who, char *message, int flags )
224{
225        PurpleConversation *conv;
226       
227        if( ( conv = purple_find_conversation_with_account( PURPLE_CONV_TYPE_IM,
228                                                            who, ic->proto_data ) ) == NULL )
229        {
230                conv = purple_conversation_new( PURPLE_CONV_TYPE_IM,
231                                                ic->proto_data, who );
232        }
233       
234        purple_conv_im_send( purple_conversation_get_im_data( conv ), message );
235       
236        return 1;
237}
238
239static GList *purple_away_states( struct im_connection *ic )
240{
241        PurpleAccount *pa = ic->proto_data;
242        GList *st, *ret = NULL;
243       
244        for( st = purple_account_get_status_types( pa ); st; st = st->next )
245        {
246                PurpleStatusPrimitive prim = purple_status_type_get_primitive( st->data );
247                if( prim != PURPLE_STATUS_AVAILABLE && prim != PURPLE_STATUS_OFFLINE )
248                        ret = g_list_append( ret, (void*) purple_status_type_get_name( st->data ) );
249        }
250       
251        return ret;
252}
253
254static void purple_set_away( struct im_connection *ic, char *state_txt, char *message )
255{
256        PurpleAccount *pa = ic->proto_data;
257        GList *status_types = purple_account_get_status_types( pa ), *st;
258        PurpleStatusType *pst = NULL;
259        GList *args = NULL;
260       
261        for( st = status_types; st; st = st->next )
262        {
263                pst = st->data;
264               
265                if( state_txt == NULL &&
266                    purple_status_type_get_primitive( pst ) == PURPLE_STATUS_AVAILABLE )
267                        break;
268
269                if( state_txt != NULL &&
270                    g_strcasecmp( state_txt, purple_status_type_get_name( pst ) ) == 0 )
271                        break;
272        }
273       
274        if( message && purple_status_type_get_attr( pst, "message" ) )
275        {
276                args = g_list_append( args, "message" );
277                args = g_list_append( args, message );
278        }
279       
280        purple_account_set_status_list( pa, st ? purple_status_type_get_id( pst ) : "away",
281                                        TRUE, args );
282
283        g_list_free( args );
284}
285
286static void purple_add_buddy( struct im_connection *ic, char *who, char *group )
287{
288        PurpleBuddy *pb;
289       
290        pb = purple_buddy_new( (PurpleAccount*) ic->proto_data, who, NULL );
291        purple_blist_add_buddy( pb, NULL, NULL, NULL );
292        purple_account_add_buddy( (PurpleAccount*) ic->proto_data, pb );
293}
294
295static void purple_remove_buddy( struct im_connection *ic, char *who, char *group )
296{
297        PurpleBuddy *pb;
298       
299        pb = purple_find_buddy( (PurpleAccount*) ic->proto_data, who );
300        if( pb != NULL )
301        {
302                purple_account_remove_buddy( (PurpleAccount*) ic->proto_data, pb, NULL );
303                purple_blist_remove_buddy( pb );
304        }
305}
306
307static void purple_keepalive( struct im_connection *ic )
308{
309}
310
311static int purple_send_typing( struct im_connection *ic, char *who, int flags )
312{
313        PurpleTypingState state = PURPLE_NOT_TYPING;
314        PurpleConversation *conv;
315       
316        if( flags & OPT_TYPING )
317                state = PURPLE_TYPING;
318        else if( flags & OPT_THINKING )
319                state = PURPLE_TYPED;
320       
321        if( ( conv = purple_find_conversation_with_account( PURPLE_CONV_TYPE_IM,
322                                                            who, ic->proto_data ) ) == NULL )
323        {
324                purple_conv_im_set_typing_state( purple_conversation_get_im_data( conv ), state );
325                return 1;
326        }
327        else
328        {
329                return 0;
330        }
331}
332
333static void purple_ui_init();
334
335static PurpleCoreUiOps bee_core_uiops = 
336{
337        NULL,
338        NULL,
339        purple_ui_init,
340        NULL,
341};
342
343static void prplcb_conn_progress( PurpleConnection *gc, const char *text, size_t step, size_t step_count )
344{
345        struct im_connection *ic = purple_ic_by_gc( gc );
346       
347        imcb_log( ic, "%s", text );
348}
349
350static void prplcb_conn_connected( PurpleConnection *gc )
351{
352        struct im_connection *ic = purple_ic_by_gc( gc );
353       
354        imcb_connected( ic );
355       
356        if( gc->flags & PURPLE_CONNECTION_HTML )
357                ic->flags |= OPT_DOES_HTML;
358}
359
360static void prplcb_conn_disconnected( PurpleConnection *gc )
361{
362        struct im_connection *ic = purple_ic_by_gc( gc );
363       
364        if( ic != NULL )
365        {
366                imc_logout( ic, TRUE );
367        }
368}
369
370static void prplcb_conn_notice( PurpleConnection *gc, const char *text )
371{
372        struct im_connection *ic = purple_ic_by_gc( gc );
373       
374        if( ic != NULL )
375                imcb_log( ic, "%s", text );
376}
377
378static void prplcb_conn_report_disconnect_reason( PurpleConnection *gc, PurpleConnectionError reason, const char *text )
379{
380        struct im_connection *ic = purple_ic_by_gc( gc );
381       
382        /* PURPLE_CONNECTION_ERROR_NAME_IN_USE means concurrent login,
383           should probably handle that. */
384        if( ic != NULL )
385                imcb_error( ic, "%s", text );
386}
387
388static PurpleConnectionUiOps bee_conn_uiops =
389{
390        prplcb_conn_progress,
391        prplcb_conn_connected,
392        prplcb_conn_disconnected,
393        prplcb_conn_notice,
394        NULL,
395        NULL,
396        NULL,
397        prplcb_conn_report_disconnect_reason,
398};
399
400static void prplcb_blist_new( PurpleBlistNode *node )
401{
402        PurpleBuddy *bud = (PurpleBuddy*) node;
403       
404        if( node->type == PURPLE_BLIST_BUDDY_NODE )
405        {
406                struct im_connection *ic = purple_ic_by_pa( bud->account );
407               
408                if( ic == NULL )
409                        return;
410               
411                imcb_add_buddy( ic, bud->name, NULL );
412                if( bud->server_alias )
413                {
414                        imcb_rename_buddy( ic, bud->name, bud->server_alias );
415                        imcb_buddy_nick_hint( ic, bud->name, bud->server_alias );
416                }
417        }
418}
419
420static void prplcb_blist_update( PurpleBuddyList *list, PurpleBlistNode *node )
421{
422        PurpleBuddy *bud = (PurpleBuddy*) node;
423       
424        if( node->type == PURPLE_BLIST_BUDDY_NODE )
425        {
426                struct im_connection *ic = purple_ic_by_pa( bud->account );
427                PurpleStatus *as;
428                int flags = 0;
429               
430                if( ic == NULL )
431                        return;
432               
433                if( bud->server_alias )
434                        imcb_rename_buddy( ic, bud->name, bud->server_alias );
435               
436                flags |= purple_presence_is_online( bud->presence ) ? OPT_LOGGED_IN : 0;
437                flags |= purple_presence_is_available( bud->presence ) ? 0 : OPT_AWAY;
438               
439                as = purple_presence_get_active_status( bud->presence );
440               
441                imcb_buddy_status( ic, bud->name, flags, purple_status_get_name( as ),
442                                   purple_status_get_attr_string( as, "message" ) );
443        }
444}
445
446static void prplcb_blist_remove( PurpleBuddyList *list, PurpleBlistNode *node )
447{
448        /*
449        PurpleBuddy *bud = (PurpleBuddy*) node;
450       
451        if( node->type == PURPLE_BLIST_BUDDY_NODE )
452        {
453                struct im_connection *ic = purple_ic_by_pa( bud->account );
454               
455                if( ic == NULL )
456                        return;
457               
458                imcb_remove_buddy( ic, bud->name, NULL );
459        }
460        */
461}
462
463static PurpleBlistUiOps bee_blist_uiops =
464{
465        NULL,
466        prplcb_blist_new,
467        NULL,
468        prplcb_blist_update,
469        prplcb_blist_remove,
470};
471
472static void prplcb_conv_im( PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime )
473{
474        struct im_connection *ic = purple_ic_by_pa( conv->account );
475        PurpleBuddy *buddy;
476       
477        /* ..._SEND means it's an outgoing message, no need to echo those. */
478        if( flags & PURPLE_MESSAGE_SEND )
479                return;
480       
481        buddy = purple_find_buddy( conv->account, who );
482        if( buddy != NULL )
483                who = purple_buddy_get_name( buddy );
484       
485        imcb_buddy_msg( ic, (char*) who, (char*) message, 0, mtime );
486}
487
488static PurpleConversationUiOps bee_conv_uiops = 
489{
490        NULL,                      /* create_conversation  */
491        NULL,                      /* destroy_conversation */
492        NULL,                      /* write_chat           */
493        prplcb_conv_im,            /* write_im             */
494        NULL,                      /* write_conv           */
495        NULL,                      /* chat_add_users       */
496        NULL,                      /* chat_rename_user     */
497        NULL,                      /* chat_remove_users    */
498        NULL,                      /* chat_update_user     */
499        NULL,                      /* present              */
500        NULL,                      /* has_focus            */
501        NULL,                      /* custom_smiley_add    */
502        NULL,                      /* custom_smiley_write  */
503        NULL,                      /* custom_smiley_close  */
504        NULL,                      /* send_confirm         */
505};
506
507struct prplcb_request_action_data
508{
509        void *user_data, *bee_data;
510        PurpleRequestActionCb yes, no;
511        int yes_i, no_i;
512};
513
514static void prplcb_request_action_yes( void *data )
515{
516        struct prplcb_request_action_data *pqad = data;
517       
518        pqad->yes( pqad->user_data, pqad->yes_i );
519        g_free( pqad );
520}
521
522static void prplcb_request_action_no( void *data )
523{
524        struct prplcb_request_action_data *pqad = data;
525       
526        pqad->no( pqad->user_data, pqad->no_i );
527        g_free( pqad );
528}
529
530static void *prplcb_request_action( const char *title, const char *primary, const char *secondary,
531                                    int default_action, PurpleAccount *account, const char *who,
532                                    PurpleConversation *conv, void *user_data, size_t action_count,
533                                    va_list actions )
534{
535        struct prplcb_request_action_data *pqad; 
536        int i;
537        char *q;
538       
539        pqad = g_new0( struct prplcb_request_action_data, 1 );
540       
541        for( i = 0; i < action_count; i ++ )
542        {
543                char *caption;
544                void *fn;
545               
546                caption = va_arg( actions, char* );
547                fn = va_arg( actions, void* );
548               
549                if( strcmp( caption, "Accept" ) == 0 )
550                {
551                        pqad->yes = fn;
552                        pqad->yes_i = i;
553                }
554                else if( strcmp( caption, "Reject" ) == 0 )
555                {
556                        pqad->no = fn;
557                        pqad->no_i = i;
558                }
559        }
560       
561        pqad->user_data = user_data;
562       
563        q = g_strdup_printf( "Request: %s\n\n%s\n\n%s", title, primary, secondary );
564        pqad->bee_data = query_add( local_irc, purple_ic_by_pa( account ), q,
565                prplcb_request_action_yes, prplcb_request_action_no, pqad );
566       
567        g_free( q );
568       
569        return pqad;
570}
571
572static PurpleRequestUiOps bee_request_uiops =
573{
574        NULL,
575        NULL,
576        prplcb_request_action,
577        NULL,
578        NULL,
579        NULL,
580        NULL,
581};
582
583static void prplcb_debug_print( PurpleDebugLevel level, const char *category, const char *arg_s )
584{
585        fprintf( stderr, "DEBUG %s: %s", category, arg_s );
586}
587
588static PurpleDebugUiOps bee_debug_uiops =
589{
590        prplcb_debug_print,
591};
592
593static guint prplcb_ev_timeout_add( guint interval, GSourceFunc func, gpointer udata )
594{
595        return b_timeout_add( interval, (b_event_handler) func, udata );
596}
597
598static guint prplcb_ev_input_add( int fd, PurpleInputCondition cond, PurpleInputFunction func, gpointer udata )
599{
600        return b_input_add( fd, cond | B_EV_FLAG_FORCE_REPEAT, (b_event_handler) func, udata );
601}
602
603static gboolean prplcb_ev_remove( guint id )
604{
605        b_event_remove( (gint) id );
606        return TRUE;
607}
608
609static PurpleEventLoopUiOps glib_eventloops = 
610{
611        prplcb_ev_timeout_add,
612        prplcb_ev_remove,
613        prplcb_ev_input_add,
614        prplcb_ev_remove,
615};
616
617static void *prplcb_notify_email( PurpleConnection *gc, const char *subject, const char *from,
618                                  const char *to, const char *url )
619{
620        struct im_connection *ic = purple_ic_by_gc( gc );
621       
622        imcb_log( ic, "Received e-mail from %s for %s: %s <%s>", from, to, subject, url );
623       
624        return NULL;
625}
626
627static  PurpleNotifyUiOps bee_notify_uiops =
628{
629        NULL,
630        prplcb_notify_email,
631};
632
633static void purple_ui_init()
634{
635        purple_blist_set_ui_ops( &bee_blist_uiops );
636        purple_connections_set_ui_ops( &bee_conn_uiops );
637        purple_conversations_set_ui_ops( &bee_conv_uiops );
638        purple_request_set_ui_ops( &bee_request_uiops );
639        purple_notify_set_ui_ops(&bee_notify_uiops);
640        //purple_debug_set_ui_ops( &bee_debug_uiops );
641}
642
643void purple_initmodule()
644{
645        struct prpl funcs;
646        GList *prots;
647        GString *help;
648       
649        if( B_EV_IO_READ != PURPLE_INPUT_READ ||
650            B_EV_IO_WRITE != PURPLE_INPUT_WRITE )
651        {
652                /* FIXME FIXME FIXME FIXME FIXME :-) */
653                exit( 1 );
654        }
655       
656        purple_util_set_user_dir("/tmp");
657        purple_debug_set_enabled(FALSE);
658        purple_core_set_ui_ops(&bee_core_uiops);
659        purple_eventloop_set_ui_ops(&glib_eventloops);
660        if( !purple_core_init( "BitlBee") )
661        {
662                /* Initializing the core failed. Terminate. */
663                fprintf( stderr, "libpurple initialization failed.\n" );
664                abort();
665        }
666       
667        /* This seems like stateful shit we don't want... */
668        purple_set_blist(purple_blist_new());
669        purple_blist_load();
670       
671        /* Meh? */
672        purple_prefs_load();
673       
674        memset( &funcs, 0, sizeof( funcs ) );
675        funcs.login = purple_login;
676        funcs.init = purple_init;
677        funcs.logout = purple_logout;
678        funcs.buddy_msg = purple_buddy_msg;
679        funcs.away_states = purple_away_states;
680        funcs.set_away = purple_set_away;
681        funcs.add_buddy = purple_add_buddy;
682        funcs.remove_buddy = purple_remove_buddy;
683        funcs.keepalive = purple_keepalive;
684        funcs.send_typing = purple_send_typing;
685        funcs.handle_cmp = g_strcasecmp;
686       
687        help = g_string_new("BitlBee libpurple module supports the following IM protocols:\n");
688       
689        /* Add a protocol entry to BitlBee's structures for every protocol
690           supported by this libpurple instance. */     
691        for( prots = purple_plugins_get_protocols(); prots; prots = prots->next )
692        {
693                PurplePlugin *prot = prots->data;
694                struct prpl *ret;
695               
696                ret = g_memdup( &funcs, sizeof( funcs ) );
697                ret->name = ret->data = prot->info->id;
698                if( strncmp( ret->name, "prpl-", 5 ) == 0 )
699                        ret->name += 5;
700                register_protocol( ret );
701               
702                g_string_append_printf( help, "\n* %s (%s)", ret->name, prot->info->name );
703               
704                /* libpurple doesn't define a protocol called OSCAR, but we
705                   need it to be compatible with normal BitlBee. */
706                if( g_strcasecmp( prot->info->id, "prpl-aim" ) == 0 )
707                {
708                        ret = g_memdup( &funcs, sizeof( funcs ) );
709                        ret->name = "oscar";
710                        ret->data = prot->info->id;
711                        register_protocol( ret );
712                }
713        }
714       
715        /* Add a simple dynamically-generated help item listing all
716           the supported protocols. */
717        help_add_mem( &global.help, "purple", help->str );
718        g_string_free( help, TRUE );
719}
Note: See TracBrowser for help on using the repository browser.