source: protocols/msn/sb.c @ ffb6dea

Last change on this file since ffb6dea was e5a8118, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-03-20T17:27:23Z

Added soap.c with a fairly reusable SOAP framework and simple code for
sending offline messages. It works somewhat, just that Pidgin shows the
messages as empty. :-(

  • Property mode set to 100644
File size: 17.6 KB
Line 
1  /********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2005 Wilmer van der Gaast and others                *
5  \********************************************************************/
6
7/* MSN module - Switchboard server callbacks and utilities              */
8
9/*
10  This program is free software; you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation; either version 2 of the License, or
13  (at your option) any later version.
14
15  This program is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  GNU General Public License for more details.
19
20  You should have received a copy of the GNU General Public License with
21  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
22  if not, write to the Free Software Foundation, Inc., 59 Temple Place,
23  Suite 330, Boston, MA  02111-1307  USA
24*/
25
26#include <ctype.h>
27#include "nogaim.h"
28#include "msn.h"
29#include "passport.h"
30#include "md5.h"
31#include "soap.h"
32
33static gboolean msn_sb_callback( gpointer data, gint source, b_input_condition cond );
34static int msn_sb_command( gpointer data, char **cmd, int num_parts );
35static int msn_sb_message( gpointer data, char *msg, int msglen, char **cmd, int num_parts );
36
37int msn_sb_write( struct msn_switchboard *sb, char *s, int len )
38{
39        int st;
40       
41        st = write( sb->fd, s, len );
42        if( st != len )
43        {
44                msn_sb_destroy( sb );
45                return( 0 );
46        }
47       
48        return( 1 );
49}
50
51int msn_sb_write_msg( struct im_connection *ic, struct msn_message *m )
52{
53        struct msn_data *md = ic->proto_data;
54        struct msn_switchboard *sb;
55        char buf[1024];
56
57        /* FIXME: *CHECK* the reliability of using spare sb's! */
58        if( ( sb = msn_sb_spare( ic ) ) )
59        {
60                debug( "Trying to use a spare switchboard to message %s", m->who );
61               
62                sb->who = g_strdup( m->who );
63                g_snprintf( buf, sizeof( buf ), "CAL %d %s\r\n", ++sb->trId, m->who );
64                if( msn_sb_write( sb, buf, strlen( buf ) ) )
65                {
66                        /* He/She should join the switchboard soon, let's queue the message. */
67                        sb->msgq = g_slist_append( sb->msgq, m );
68                        return( 1 );
69                }
70        }
71       
72        debug( "Creating a new switchboard to message %s", m->who );
73       
74        /* If we reach this line, there was no spare switchboard, so let's make one. */
75        g_snprintf( buf, sizeof( buf ), "XFR %d SB\r\n", ++md->trId );
76        if( !msn_write( ic, buf, strlen( buf ) ) )
77        {
78                g_free( m->who );
79                g_free( m->text );
80                g_free( m );
81               
82                return( 0 );
83        }
84       
85        /* And queue the message to md. We'll pick it up when the switchboard comes up. */
86        md->msgq = g_slist_append( md->msgq, m );
87       
88        /* FIXME: If the switchboard creation fails, the message will not be sent. */
89       
90        return( 1 );
91}
92
93struct msn_switchboard *msn_sb_create( struct im_connection *ic, char *host, int port, char *key, int session )
94{
95        struct msn_data *md = ic->proto_data;
96        struct msn_switchboard *sb = g_new0( struct msn_switchboard, 1 );
97       
98        sb->fd = proxy_connect( host, port, msn_sb_connected, sb );
99        if( sb->fd < 0 )
100        {
101                g_free( sb );
102                return( NULL );
103        }
104       
105        sb->ic = ic;
106        sb->key = g_strdup( key );
107        sb->session = session;
108       
109        msn_switchboards = g_slist_append( msn_switchboards, sb );
110        md->switchboards = g_slist_append( md->switchboards, sb );
111       
112        return( sb );
113}
114
115struct msn_switchboard *msn_sb_by_handle( struct im_connection *ic, char *handle )
116{
117        struct msn_data *md = ic->proto_data;
118        struct msn_switchboard *sb;
119        GSList *l;
120       
121        for( l = md->switchboards; l; l = l->next )
122        {
123                sb = l->data;
124                if( sb->who && strcmp( sb->who, handle ) == 0 )
125                        return( sb );
126        }
127       
128        return( NULL );
129}
130
131struct msn_switchboard *msn_sb_by_chat( struct groupchat *c )
132{
133        struct msn_data *md = c->ic->proto_data;
134        struct msn_switchboard *sb;
135        GSList *l;
136       
137        for( l = md->switchboards; l; l = l->next )
138        {
139                sb = l->data;
140                if( sb->chat == c )
141                        return( sb );
142        }
143       
144        return( NULL );
145}
146
147struct msn_switchboard *msn_sb_spare( struct im_connection *ic )
148{
149        struct msn_data *md = ic->proto_data;
150        struct msn_switchboard *sb;
151        GSList *l;
152       
153        for( l = md->switchboards; l; l = l->next )
154        {
155                sb = l->data;
156                if( !sb->who && !sb->chat )
157                        return( sb );
158        }
159       
160        return( NULL );
161}
162
163int msn_sb_sendmessage( struct msn_switchboard *sb, char *text )
164{
165        if( sb->ready )
166        {
167                char *packet, *buf;
168                int i, j;
169               
170                /* Build the message. Convert LF to CR-LF for normal messages. */
171                if( strcmp( text, TYPING_NOTIFICATION_MESSAGE ) != 0 )
172                {
173                        buf = g_new0( char, sizeof( MSN_MESSAGE_HEADERS ) + strlen( text ) * 2 + 1 );
174                        i = strlen( MSN_MESSAGE_HEADERS );
175                       
176                        strcpy( buf, MSN_MESSAGE_HEADERS );
177                        for( j = 0; text[j]; j ++ )
178                        {
179                                if( text[j] == '\n' )
180                                        buf[i++] = '\r';
181                               
182                                buf[i++] = text[j];
183                        }
184                }
185                else
186                {
187                        i = strlen( MSN_TYPING_HEADERS ) + strlen( sb->ic->acc->user );
188                        buf = g_new0( char, i );
189                        i = g_snprintf( buf, i, MSN_TYPING_HEADERS, sb->ic->acc->user );
190                }
191               
192                /* Build the final packet (MSG command + the message). */
193                packet = g_strdup_printf( "MSG %d N %d\r\n%s", ++sb->trId, i, buf );
194                g_free( buf );
195                if( msn_sb_write( sb, packet, strlen( packet ) ) )
196                {
197                        g_free( packet );
198                        return( 1 );
199                }
200                else
201                {
202                        g_free( packet );
203                        return( 0 );
204                }
205        }
206        else if( sb->who )
207        {
208                struct msn_message *m = g_new0( struct msn_message, 1 );
209               
210                m->who = g_strdup( "" );
211                m->text = g_strdup( text );
212                sb->msgq = g_slist_append( sb->msgq, m );
213               
214                return( 1 );
215        }
216        else
217        {
218                return( 0 );
219        }
220}
221
222struct groupchat *msn_sb_to_chat( struct msn_switchboard *sb )
223{
224        struct im_connection *ic = sb->ic;
225        char buf[1024];
226       
227        /* Create the groupchat structure. */
228        g_snprintf( buf, sizeof( buf ), "MSN groupchat session %d", sb->session );
229        sb->chat = imcb_chat_new( ic, buf );
230       
231        /* Populate the channel. */
232        if( sb->who ) imcb_chat_add_buddy( sb->chat, sb->who );
233        imcb_chat_add_buddy( sb->chat, ic->acc->user );
234       
235        /* And make sure the switchboard doesn't look like a regular chat anymore. */
236        if( sb->who )
237        {
238                g_free( sb->who );
239                sb->who = NULL;
240        }
241       
242        return sb->chat;
243}
244
245void msn_sb_destroy( struct msn_switchboard *sb )
246{
247        struct im_connection *ic = sb->ic;
248        struct msn_data *md = ic->proto_data;
249       
250        debug( "Destroying switchboard: %s", sb->who ? sb->who : sb->key ? sb->key : "" );
251       
252        msn_msgq_purge( ic, &sb->msgq );
253       
254        if( sb->key ) g_free( sb->key );
255        if( sb->who ) g_free( sb->who );
256       
257        if( sb->chat )
258        {
259                imcb_chat_free( sb->chat );
260        }
261       
262        if( sb->handler )
263        {
264                if( sb->handler->rxq ) g_free( sb->handler->rxq );
265                if( sb->handler->cmd_text ) g_free( sb->handler->cmd_text );
266                g_free( sb->handler );
267        }
268       
269        if( sb->inp ) b_event_remove( sb->inp );
270        closesocket( sb->fd );
271       
272        msn_switchboards = g_slist_remove( msn_switchboards, sb );
273        md->switchboards = g_slist_remove( md->switchboards, sb );
274        g_free( sb );
275}
276
277gboolean msn_sb_connected( gpointer data, gint source, b_input_condition cond )
278{
279        struct msn_switchboard *sb = data;
280        struct im_connection *ic;
281        struct msn_data *md;
282        char buf[1024];
283       
284        /* Are we still alive? */
285        if( !g_slist_find( msn_switchboards, sb ) )
286                return FALSE;
287       
288        ic = sb->ic;
289        md = ic->proto_data;
290       
291        if( source != sb->fd )
292        {
293                debug( "Error %d while connecting to switchboard server", 1 );
294                msn_sb_destroy( sb );
295                return FALSE;
296        }
297       
298        /* Prepare the callback */
299        sb->handler = g_new0( struct msn_handler_data, 1 );
300        sb->handler->fd = sb->fd;
301        sb->handler->rxq = g_new0( char, 1 );
302        sb->handler->data = sb;
303        sb->handler->exec_command = msn_sb_command;
304        sb->handler->exec_message = msn_sb_message;
305       
306        if( sb->session == MSN_SB_NEW )
307                g_snprintf( buf, sizeof( buf ), "USR %d %s %s\r\n", ++sb->trId, ic->acc->user, sb->key );
308        else
309                g_snprintf( buf, sizeof( buf ), "ANS %d %s %s %d\r\n", ++sb->trId, ic->acc->user, sb->key, sb->session );
310       
311        if( msn_sb_write( sb, buf, strlen( buf ) ) )
312                sb->inp = b_input_add( sb->fd, GAIM_INPUT_READ, msn_sb_callback, sb );
313        else
314                debug( "Error %d while connecting to switchboard server", 2 );
315       
316        return FALSE;
317}
318
319static gboolean msn_sb_callback( gpointer data, gint source, b_input_condition cond )
320{
321        struct msn_switchboard *sb = data;
322        struct im_connection *ic = sb->ic;
323        struct msn_data *md = ic->proto_data;
324       
325        if( msn_handler( sb->handler ) == -1 )
326        {
327                time_t now = time( NULL );
328               
329                if( now - md->first_sb_failure > 600 )
330                {
331                        /* It's not really the first one, but the start of this "series".
332                           With this, the warning below will be shown only if this happens
333                           at least three times in ten minutes. This algorithm isn't
334                           perfect, but for this purpose it will do. */
335                        md->first_sb_failure = now;
336                        md->sb_failures = 0;
337                }
338               
339                debug( "Error: Switchboard died" );
340                if( ++ md->sb_failures >= 3 )
341                        imcb_log( ic, "Warning: Many switchboard failures on MSN connection. "
342                                      "There might be problems delivering your messages." );
343               
344                if( sb->msgq != NULL )
345                {
346                        char buf[1024];
347                       
348                        if( md->msgq == NULL )
349                        {
350                                md->msgq = sb->msgq;
351                        }
352                        else
353                        {
354                                GSList *l;
355                               
356                                for( l = md->msgq; l->next; l = l->next );
357                                l->next = sb->msgq;
358                        }
359                        sb->msgq = NULL;
360                       
361                        debug( "Moved queued messages back to the main queue, creating a new switchboard to retry." );
362                        g_snprintf( buf, sizeof( buf ), "XFR %d SB\r\n", ++md->trId );
363                        if( !msn_write( ic, buf, strlen( buf ) ) )
364                                return FALSE;
365                }
366               
367                msn_sb_destroy( sb );
368               
369                return FALSE;
370        }
371        else
372        {
373                return TRUE;
374        }
375}
376
377static int msn_sb_command( gpointer data, char **cmd, int num_parts )
378{
379        struct msn_switchboard *sb = data;
380        struct im_connection *ic = sb->ic;
381        char buf[1024];
382       
383        if( !num_parts )
384        {
385                /* Hrrm... Empty command...? Ignore? */
386                return( 1 );
387        }
388       
389        if( strcmp( cmd[0], "XFR" ) == 0 )
390        {
391                imcb_error( ic, "Received an XFR from a switchboard server, unable to comply! This is likely to be a bug, please report it!" );
392                imc_logout( ic, TRUE );
393                return( 0 );
394        }
395        else if( strcmp( cmd[0], "USR" ) == 0 )
396        {
397                if( num_parts != 5 )
398                {
399                        msn_sb_destroy( sb );
400                        return( 0 );
401                }
402               
403                if( strcmp( cmd[2], "OK" ) != 0 )
404                {
405                        msn_sb_destroy( sb );
406                        return( 0 );
407                }
408               
409                if( sb->who )
410                {
411                        g_snprintf( buf, sizeof( buf ), "CAL %d %s\r\n", ++sb->trId, sb->who );
412                        return( msn_sb_write( sb, buf, strlen( buf ) ) );
413                }
414                else
415                {
416                        debug( "Just created a switchboard, but I don't know what to do with it." );
417                }
418        }
419        else if( strcmp( cmd[0], "IRO" ) == 0 )
420        {
421                int num, tot;
422               
423                if( num_parts != 6 )
424                {
425                        msn_sb_destroy( sb );
426                        return( 0 );
427                }
428               
429                num = atoi( cmd[2] );
430                tot = atoi( cmd[3] );
431               
432                if( tot <= 0 )
433                {
434                        msn_sb_destroy( sb );
435                        return( 0 );
436                }
437                else if( tot > 1 )
438                {
439                        char buf[1024];
440                       
441                        if( num == 1 )
442                        {
443                                g_snprintf( buf, sizeof( buf ), "MSN groupchat session %d", sb->session );
444                                sb->chat = imcb_chat_new( ic, buf );
445                               
446                                g_free( sb->who );
447                                sb->who = NULL;
448                        }
449                       
450                        imcb_chat_add_buddy( sb->chat, cmd[4] );
451                       
452                        if( num == tot )
453                        {
454                                imcb_chat_add_buddy( sb->chat, ic->acc->user );
455                        }
456                }
457        }
458        else if( strcmp( cmd[0], "ANS" ) == 0 )
459        {
460                if( num_parts != 3 )
461                {
462                        msn_sb_destroy( sb );
463                        return( 0 );
464                }
465               
466                if( strcmp( cmd[2], "OK" ) != 0 )
467                {
468                        debug( "Switchboard server sent a negative ANS reply" );
469                        msn_sb_destroy( sb );
470                        return( 0 );
471                }
472               
473                sb->ready = 1;
474        }
475        else if( strcmp( cmd[0], "CAL" ) == 0 )
476        {
477                if( num_parts != 4 || !isdigit( cmd[3][0] ) )
478                {
479                        msn_sb_destroy( sb );
480                        return( 0 );
481                }
482               
483                sb->session = atoi( cmd[3] );
484        }
485        else if( strcmp( cmd[0], "JOI" ) == 0 )
486        {
487                if( num_parts != 3 )
488                {
489                        msn_sb_destroy( sb );
490                        return( 0 );
491                }
492               
493                if( sb->who && g_strcasecmp( cmd[1], sb->who ) == 0 )
494                {
495                        /* The user we wanted to talk to is finally there, let's send the queued messages then. */
496                        struct msn_message *m;
497                        GSList *l;
498                        int st = 1;
499                       
500                        debug( "%s arrived in the switchboard session, now sending queued message(s)", cmd[1] );
501                       
502                        /* Without this, sendmessage() will put everything back on the queue... */
503                        sb->ready = 1;
504                       
505                        while( ( l = sb->msgq ) )
506                        {
507                                m = l->data;
508                                if( st )
509                                {
510                                        /* This hack is meant to convert a regular new chat into a groupchat */
511                                        if( strcmp( m->text, GROUPCHAT_SWITCHBOARD_MESSAGE ) == 0 )
512                                                msn_sb_to_chat( sb );
513                                        else
514                                                st = msn_sb_sendmessage( sb, m->text );
515                                }
516                                g_free( m->text );
517                                g_free( m->who );
518                                g_free( m );
519                               
520                                sb->msgq = g_slist_remove( sb->msgq, m );
521                        }
522                       
523                        return( st );
524                }
525                else if( sb->who )
526                {
527                        debug( "Converting chat with %s to a groupchat because %s joined the session.", sb->who, cmd[1] );
528                       
529                        /* This SB is a one-to-one chat right now, but someone else is joining. */
530                        msn_sb_to_chat( sb );
531                       
532                        imcb_chat_add_buddy( sb->chat, cmd[1] );
533                }
534                else if( sb->chat )
535                {
536                        imcb_chat_add_buddy( sb->chat, cmd[1] );
537                        sb->ready = 1;
538                }
539                else
540                {
541                        /* PANIC! */
542                }
543        }
544        else if( strcmp( cmd[0], "MSG" ) == 0 )
545        {
546                if( num_parts != 4 )
547                {
548                        msn_sb_destroy( sb );
549                        return( 0 );
550                }
551               
552                sb->handler->msglen = atoi( cmd[3] );
553               
554                if( sb->handler->msglen <= 0 )
555                {
556                        debug( "Received a corrupted message on the switchboard, the switchboard will be closed" );
557                        msn_sb_destroy( sb );
558                        return( 0 );
559                }
560        }
561        else if( strcmp( cmd[0], "NAK" ) == 0 )
562        {
563                if( sb->who )
564                {
565                        imcb_log( ic, "The MSN servers could not deliver one of your messages to %s.", sb->who );
566                }
567                else
568                {
569                        imcb_log( ic, "The MSN servers could not deliver one of your groupchat messages to all participants." );
570                }
571        }
572        else if( strcmp( cmd[0], "BYE" ) == 0 )
573        {
574                if( num_parts < 2 )
575                {
576                        msn_sb_destroy( sb );
577                        return( 0 );
578                }
579               
580                /* if( cmd[2] && *cmd[2] == '1' ) -=> Chat is being cleaned up because of idleness */
581               
582                if( sb->who )
583                {
584                        /* This is a single-person chat, and the other person is leaving. */
585                        g_free( sb->who );
586                        sb->who = NULL;
587                        sb->ready = 0;
588                       
589                        debug( "Person %s left the one-to-one switchboard connection. Keeping it around as a spare...", cmd[1] );
590                       
591                        /* We could clean up the switchboard now, but keeping it around
592                           as a spare for a next conversation sounds more sane to me.
593                           The server will clean it up when it's idle for too long. */
594                }
595                else if( sb->chat )
596                {
597                        imcb_chat_remove_buddy( sb->chat, cmd[1], "" );
598                }
599                else
600                {
601                        /* PANIC! */
602                }
603        }
604        else if( isdigit( cmd[0][0] ) )
605        {
606                int num = atoi( cmd[0] );
607                const struct msn_status_code *err = msn_status_by_number( num );
608               
609                if( num == 217 )
610                {
611                        GSList *l;
612                       
613                        for( l = sb->msgq; l; l = l->next )
614                        {
615                                struct msn_message *m = l->data;
616                                msn_soap_oim_send( ic, m->who, m->text );
617                        }
618                }
619               
620                imcb_error( ic, "Error reported by switchboard server: %s", err->text );
621               
622                if( err->flags & STATUS_SB_FATAL )
623                {
624                        msn_sb_destroy( sb );
625                        return 0;
626                }
627                else if( err->flags & STATUS_FATAL )
628                {
629                        imc_logout( ic, TRUE );
630                        return 0;
631                }
632                else if( err->flags & STATUS_SB_IM_SPARE )
633                {
634                        if( sb->who )
635                        {
636                                /* Apparently some invitation failed. We might want to use this
637                                   board later, so keep it as a spare. */
638                                g_free( sb->who );
639                                sb->who = NULL;
640                               
641                                /* Also clear the msgq, otherwise someone else might get them. */
642                                msn_msgq_purge( ic, &sb->msgq );
643                        }
644                       
645                        /* Do NOT return 0 here, we want to keep this sb. */
646                }
647        }
648        else
649        {
650                /* debug( "Received unknown command from switchboard server: %s", cmd[0] ); */
651        }
652       
653        return( 1 );
654}
655
656static int msn_sb_message( gpointer data, char *msg, int msglen, char **cmd, int num_parts )
657{
658        struct msn_switchboard *sb = data;
659        struct im_connection *ic = sb->ic;
660        char *body;
661        int blen = 0;
662       
663        if( !num_parts )
664                return( 1 );
665       
666        if( ( body = strstr( msg, "\r\n\r\n" ) ) )
667        {
668                body += 4;
669                blen = msglen - ( body - msg );
670        }
671       
672        if( strcmp( cmd[0], "MSG" ) == 0 )
673        {
674                char *ct = msn_findheader( msg, "Content-Type:", msglen );
675               
676                if( !ct )
677                        return( 1 );
678               
679                if( g_strncasecmp( ct, "text/plain", 10 ) == 0 )
680                {
681                        g_free( ct );
682                       
683                        if( !body )
684                                return( 1 );
685                       
686                        if( sb->who )
687                        {
688                                imcb_buddy_msg( ic, cmd[1], body, 0, 0 );
689                        }
690                        else if( sb->chat )
691                        {
692                                imcb_chat_msg( sb->chat, cmd[1], body, 0, 0 );
693                        }
694                        else
695                        {
696                                /* PANIC! */
697                        }
698                }
699                else if( g_strncasecmp( ct, "text/x-msmsgsinvite", 19 ) == 0 )
700                {
701                        char *itype = msn_findheader( body, "Application-GUID:", blen );
702                        char buf[1024];
703                       
704                        g_free( ct );
705                       
706                        *buf = 0;
707                       
708                        if( !itype )
709                                return( 1 );
710                       
711                        /* File transfer. */
712                        if( strcmp( itype, "{5D3E02AB-6190-11d3-BBBB-00C04F795683}" ) == 0 )
713                        {
714                                char *name = msn_findheader( body, "Application-File:", blen );
715                                char *size = msn_findheader( body, "Application-FileSize:", blen );
716                               
717                                if( name && size )
718                                {
719                                        g_snprintf( buf, sizeof( buf ), "<< \x02""BitlBee\x02"" - Filetransfer: `%s', %s bytes >>\n"
720                                                    "Filetransfers are not supported by BitlBee for now...", name, size );
721                                }
722                                else
723                                {
724                                        strcpy( buf, "<< \x02""BitlBee\x02"" - Corrupted MSN filetransfer invitation message >>" );
725                                }
726                               
727                                if( name ) g_free( name );
728                                if( size ) g_free( size );
729                        }
730                        else
731                        {
732                                char *iname = msn_findheader( body, "Application-Name:", blen );
733                               
734                                g_snprintf( buf, sizeof( buf ), "<< \x02""BitlBee\x02"" - Unknown MSN invitation - %s (%s) >>",
735                                                                itype, iname ? iname : "no name" );
736                               
737                                if( iname ) g_free( iname );
738                        }
739                       
740                        g_free( itype );
741                       
742                        if( !*buf )
743                                return( 1 );
744                       
745                        if( sb->who )
746                        {
747                                imcb_buddy_msg( ic, cmd[1], buf, 0, 0 );
748                        }
749                        else if( sb->chat )
750                        {
751                                imcb_chat_msg( sb->chat, cmd[1], buf, 0, 0 );
752                        }
753                        else
754                        {
755                                /* PANIC! */
756                        }
757                }
758                else if( g_strncasecmp( ct, "text/x-msmsgscontrol", 20 ) == 0 )
759                {
760                        char *who = msn_findheader( msg, "TypingUser:", msglen );
761                       
762                        if( who )
763                        {
764                                imcb_buddy_typing( ic, who, OPT_TYPING );
765                                g_free( who );
766                        }
767                       
768                        g_free( ct );
769                }
770                else
771                {
772                        g_free( ct );
773                }
774        }
775       
776        return( 1 );
777}
Note: See TracBrowser for help on using the repository browser.