source: libfaim/chat.c @ 6bf73ce

barnowl_perlaimdebianowlrelease-1.10release-1.4release-1.5release-1.6release-1.7release-1.8release-1.9
Last change on this file since 6bf73ce was 5e53c4a, checked in by James M. Kretchmar <kretch@mit.edu>, 21 years ago
*** empty log message ***
  • Property mode set to 100644
File size: 15.9 KB
Line 
1/*
2 * aim_chat.c
3 *
4 * Routines for the Chat service.
5 *
6 */
7
8#define FAIM_INTERNAL
9#include <aim.h>
10
11/* Stored in the ->priv of chat connections */
12struct chatconnpriv {
13        fu16_t exchange;
14        char *name;
15        fu16_t instance;
16};
17
18static void dumpbox(aim_session_t *sess, unsigned char *buf, int len)
19{
20        int i;
21
22        if (!sess || !buf || !len)
23                return;
24
25        faimdprintf(sess, 1, "\nDump of %d bytes at %p:", len, buf);
26
27        for (i = 0; i < len; i++) {
28                if ((i % 8) == 0)
29                        faimdprintf(sess, 1, "\n\t");
30
31                faimdprintf(sess, 1, "0x%2x ", buf[i]);
32        }
33
34        faimdprintf(sess, 1, "\n\n");
35
36        return;
37}
38
39faim_internal void aim_conn_kill_chat(aim_session_t *sess, aim_conn_t *conn)
40{
41        struct chatconnpriv *ccp = (struct chatconnpriv *)conn->priv;
42
43        if (ccp)
44                free(ccp->name);
45        free(ccp);
46
47        return;
48}
49
50faim_export char *aim_chat_getname(aim_conn_t *conn)
51{
52        struct chatconnpriv *ccp;
53
54        if (!conn)
55                return NULL;
56
57        if (conn->type != AIM_CONN_TYPE_CHAT)
58                return NULL;
59
60        ccp = (struct chatconnpriv *)conn->priv;
61
62        return ccp->name;
63}
64
65/* XXX get this into conn.c -- evil!! */
66faim_export aim_conn_t *aim_chat_getconn(aim_session_t *sess, const char *name)
67{
68        aim_conn_t *cur;
69
70        for (cur = sess->connlist; cur; cur = cur->next) {
71                struct chatconnpriv *ccp = (struct chatconnpriv *)cur->priv;
72
73                if (cur->type != AIM_CONN_TYPE_CHAT)
74                        continue;
75                if (!cur->priv) {
76                        faimdprintf(sess, 0, "faim: chat: chat connection with no name! (fd = %d)\n", cur->fd);
77                        continue;
78                }
79
80                if (strcmp(ccp->name, name) == 0)
81                        break;
82        }
83
84        return cur;
85}
86
87faim_export int aim_chat_attachname(aim_conn_t *conn, fu16_t exchange, const char *roomname, fu16_t instance)
88{
89        struct chatconnpriv *ccp;
90
91        if (!conn || !roomname)
92                return -EINVAL;
93
94        if (conn->priv)
95                free(conn->priv);
96
97        if (!(ccp = malloc(sizeof(struct chatconnpriv))))
98                return -ENOMEM;
99
100        ccp->exchange = exchange;
101        ccp->name = strdup(roomname);
102        ccp->instance = instance;
103
104        conn->priv = (void *)ccp;
105
106        return 0;
107}
108
109/*
110 * Send a Chat Message.
111 *
112 * Possible flags:
113 *   AIM_CHATFLAGS_NOREFLECT   --  Unset the flag that requests messages
114 *                                 should be sent to their sender.
115 *   AIM_CHATFLAGS_AWAY        --  Mark the message as an autoresponse
116 *                                 (Note that WinAIM does not honor this,
117 *                                 and displays the message as normal.)
118 *
119 * XXX convert this to use tlvchains
120 */
121faim_export int aim_chat_send_im(aim_session_t *sess, aim_conn_t *conn, fu16_t flags, const char *msg, int msglen)
122{   
123        int i;
124        aim_frame_t *fr;
125        aim_msgcookie_t *cookie;
126        aim_snacid_t snacid;
127        fu8_t ckstr[8];
128        aim_tlvlist_t *otl = NULL, *itl = NULL;
129
130        if (!sess || !conn || !msg || (msglen <= 0))
131                return 0;
132
133        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x02, 1152)))
134                return -ENOMEM;
135
136        snacid = aim_cachesnac(sess, 0x000e, 0x0005, 0x0000, NULL, 0);
137        aim_putsnac(&fr->data, 0x000e, 0x0005, 0x0000, snacid);
138
139
140        /*
141         * Generate a random message cookie.
142         *
143         * XXX mkcookie should generate the cookie and cache it in one
144         * operation to preserve uniqueness.
145         *
146         */
147        for (i = 0; i < sizeof(ckstr); i++)
148                aimutil_put8(ckstr+i, (fu8_t) rand());
149
150        cookie = aim_mkcookie(ckstr, AIM_COOKIETYPE_CHAT, NULL);
151        cookie->data = NULL; /* XXX store something useful here */
152
153        aim_cachecookie(sess, cookie);
154
155        for (i = 0; i < sizeof(ckstr); i++)
156                aimbs_put8(&fr->data, ckstr[i]);
157
158
159        /*
160         * Channel ID.
161         */
162        aimbs_put16(&fr->data, 0x0003);
163
164
165        /*
166         * Type 1: Flag meaning this message is destined to the room.
167         */
168        aim_addtlvtochain_noval(&otl, 0x0001);
169
170        /*
171         * Type 6: Reflect
172         */
173        if (!(flags & AIM_CHATFLAGS_NOREFLECT))
174                aim_addtlvtochain_noval(&otl, 0x0006);
175
176        /*
177         * Type 7: Autoresponse
178         */
179        if (flags & AIM_CHATFLAGS_AWAY)
180                aim_addtlvtochain_noval(&otl, 0x0007);
181
182        /*
183         * SubTLV: Type 1: Message
184         */
185        aim_addtlvtochain_raw(&itl, 0x0001, strlen(msg), msg);
186
187        /*
188         * Type 5: Message block.  Contains more TLVs.
189         *
190         * This could include other information... We just
191         * put in a message TLV however. 
192         *
193         */
194        aim_addtlvtochain_frozentlvlist(&otl, 0x0005, &itl);
195
196        aim_writetlvchain(&fr->data, &otl);
197       
198        aim_freetlvchain(&itl);
199        aim_freetlvchain(&otl);
200       
201        aim_tx_enqueue(sess, fr);
202
203        return 0;
204}
205
206static int aim_addtlvtochain_chatroom(aim_tlvlist_t **list, fu16_t type, fu16_t exchange, const char *roomname, fu16_t instance)
207{
208        fu8_t *buf;
209        int buflen;
210        aim_bstream_t bs;
211
212        buflen = 2 + 1 + strlen(roomname) + 2;
213       
214        if (!(buf = malloc(buflen)))
215                return 0;
216
217        aim_bstream_init(&bs, buf, buflen);
218
219        aimbs_put16(&bs, exchange);
220        aimbs_put8(&bs, strlen(roomname));
221        aimbs_putraw(&bs, roomname, strlen(roomname));
222        aimbs_put16(&bs, instance);
223
224        aim_addtlvtochain_raw(list, type, aim_bstream_curpos(&bs), buf);
225
226        free(buf);
227
228        return 0;
229}
230
231/*
232 * Join a room of name roomname.  This is the first step to joining an
233 * already created room.  It's basically a Service Request for
234 * family 0x000e, with a little added on to specify the exchange and room
235 * name.
236 */
237faim_export int aim_chat_join(aim_session_t *sess, aim_conn_t *conn, fu16_t exchange, const char *roomname, fu16_t instance)
238{
239        aim_frame_t *fr;
240        aim_snacid_t snacid;
241        aim_tlvlist_t *tl = NULL;
242        struct chatsnacinfo csi;
243       
244        if (!sess || !conn || !roomname || !strlen(roomname))
245                return -EINVAL;
246
247        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x02, 512)))
248                return -ENOMEM;
249
250        memset(&csi, 0, sizeof(csi));
251        csi.exchange = exchange;
252        strncpy(csi.name, roomname, sizeof(csi.name));
253        csi.instance = instance;
254
255        snacid = aim_cachesnac(sess, 0x0001, 0x0004, 0x0000, &csi, sizeof(csi));
256        aim_putsnac(&fr->data, 0x0001, 0x0004, 0x0000, snacid);
257
258        /*
259         * Requesting service chat (0x000e)
260         */
261        aimbs_put16(&fr->data, 0x000e);
262
263        aim_addtlvtochain_chatroom(&tl, 0x0001, exchange, roomname, instance);
264        aim_writetlvchain(&fr->data, &tl);
265        aim_freetlvchain(&tl);
266
267        aim_tx_enqueue(sess, fr);
268
269        return 0; 
270}
271
272faim_internal int aim_chat_readroominfo(aim_bstream_t *bs, struct aim_chat_roominfo *outinfo)
273{
274        int namelen;
275
276        if (!bs || !outinfo)
277                return 0;
278
279        outinfo->exchange = aimbs_get16(bs);
280        namelen = aimbs_get8(bs);
281        outinfo->name = aimbs_getstr(bs, namelen);
282        outinfo->instance = aimbs_get16(bs);
283
284        return 0;
285}
286
287faim_export int aim_chat_leaveroom(aim_session_t *sess, const char *name)
288{
289        aim_conn_t *conn;
290
291        if (!(conn = aim_chat_getconn(sess, name)))
292                return -ENOENT;
293
294        aim_conn_close(conn);
295
296        return 0;
297}
298
299/*
300 * conn must be a BOS connection!
301 */
302faim_export int aim_chat_invite(aim_session_t *sess, aim_conn_t *conn, const char *sn, const char *msg, fu16_t exchange, const char *roomname, fu16_t instance)
303{
304        int i;
305        aim_frame_t *fr;
306        aim_msgcookie_t *cookie;
307        struct aim_invite_priv *priv;
308        fu8_t ckstr[8];
309        aim_snacid_t snacid;
310        aim_tlvlist_t *otl = NULL, *itl = NULL;
311        fu8_t *hdr;
312        int hdrlen;
313        aim_bstream_t hdrbs;
314       
315        if (!sess || !conn || !sn || !msg || !roomname)
316                return -EINVAL;
317
318        if (conn->type != AIM_CONN_TYPE_BOS)
319                return -EINVAL;
320
321        if (!(fr = aim_tx_new(sess, conn, AIM_FRAMETYPE_FLAP, 0x02, 1152+strlen(sn)+strlen(roomname)+strlen(msg))))
322                return -ENOMEM;
323
324        snacid = aim_cachesnac(sess, 0x0004, 0x0006, 0x0000, sn, strlen(sn)+1);
325        aim_putsnac(&fr->data, 0x0004, 0x0006, 0x0000, snacid);
326
327
328        /*
329         * Cookie
330         */
331        for (i = 0; i < sizeof(ckstr); i++)
332                aimutil_put8(ckstr, (fu8_t) rand());
333
334        /* XXX should be uncached by an unwritten 'invite accept' handler */
335        if ((priv = malloc(sizeof(struct aim_invite_priv)))) {
336                priv->sn = strdup(sn);
337                priv->roomname = strdup(roomname);
338                priv->exchange = exchange;
339                priv->instance = instance;
340        }
341
342        if ((cookie = aim_mkcookie(ckstr, AIM_COOKIETYPE_INVITE, priv)))
343                aim_cachecookie(sess, cookie);
344        else
345                free(priv);
346
347        for (i = 0; i < sizeof(ckstr); i++)
348                aimbs_put8(&fr->data, ckstr[i]);
349
350
351        /*
352         * Channel (2)
353         */
354        aimbs_put16(&fr->data, 0x0002);
355
356        /*
357         * Dest sn
358         */
359        aimbs_put8(&fr->data, strlen(sn));
360        aimbs_putraw(&fr->data, sn, strlen(sn));
361
362        /*
363         * TLV t(0005)
364         *
365         * Everything else is inside this TLV.
366         *
367         * Sigh.  AOL was rather inconsistent right here.  So we have
368         * to play some minor tricks.  Right inside the type 5 is some
369         * raw data, followed by a series of TLVs. 
370         *
371         */
372        hdrlen = 2+8+16+6+4+4+strlen(msg)+4+2+1+strlen(roomname)+2;
373        hdr = malloc(hdrlen);
374        aim_bstream_init(&hdrbs, hdr, hdrlen);
375       
376        aimbs_put16(&hdrbs, 0x0000); /* Unknown! */
377        aimbs_putraw(&hdrbs, ckstr, sizeof(ckstr)); /* I think... */
378        aim_putcap(&hdrbs, AIM_CAPS_CHAT);
379
380        aim_addtlvtochain16(&itl, 0x000a, 0x0001);
381        aim_addtlvtochain_noval(&itl, 0x000f);
382        aim_addtlvtochain_raw(&itl, 0x000c, strlen(msg), msg);
383        aim_addtlvtochain_chatroom(&itl, 0x2711, exchange, roomname, instance);
384        aim_writetlvchain(&hdrbs, &itl);
385       
386        aim_addtlvtochain_raw(&otl, 0x0005, aim_bstream_curpos(&hdrbs), hdr);
387
388        aim_writetlvchain(&fr->data, &otl);
389
390        free(hdr);
391        aim_freetlvchain(&itl);
392        aim_freetlvchain(&otl);
393       
394        aim_tx_enqueue(sess, fr);
395
396        return 0;
397}
398
399/*
400 * General room information.  Lots of stuff.
401 *
402 * Values I know are in here but I havent attached
403 * them to any of the 'Unknown's:
404 *      - Language (English)
405 *
406 * SNAC 000e/0002
407 */
408static int infoupdate(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
409{
410        aim_userinfo_t *userinfo = NULL;
411        aim_rxcallback_t userfunc;
412        int ret = 0;
413        int usercount = 0;
414        fu8_t detaillevel = 0;
415        char *roomname = NULL;
416        struct aim_chat_roominfo roominfo;
417        fu16_t tlvcount = 0;
418        aim_tlvlist_t *tlvlist;
419        char *roomdesc = NULL;
420        fu16_t flags = 0;
421        fu32_t creationtime = 0;
422        fu16_t maxmsglen = 0, maxvisiblemsglen = 0;
423        fu16_t unknown_d2 = 0, unknown_d5 = 0;
424
425        dumpbox(sess, bs->data + bs->offset, aim_bstream_empty(bs));
426
427        aim_chat_readroominfo(bs, &roominfo);
428
429        detaillevel = aimbs_get8(bs);
430
431        if (detaillevel != 0x02) {
432                faimdprintf(sess, 0, "faim: chat_roomupdateinfo: detail level %d not supported\n", detaillevel);
433                return 1;
434        }
435
436        tlvcount = aimbs_get16(bs);
437
438        /*
439         * Everything else are TLVs.
440         */ 
441        tlvlist = aim_readtlvchain(bs);
442
443        /*
444         * TLV type 0x006a is the room name in Human Readable Form.
445         */
446        if (aim_gettlv(tlvlist, 0x006a, 1))
447                roomname = aim_gettlv_str(tlvlist, 0x006a, 1);
448
449        /*
450         * Type 0x006f: Number of occupants.
451         */
452        if (aim_gettlv(tlvlist, 0x006f, 1))
453                usercount = aim_gettlv16(tlvlist, 0x006f, 1);
454
455        /*
456         * Type 0x0073:  Occupant list.
457         */
458        if (aim_gettlv(tlvlist, 0x0073, 1)) {   
459                int curoccupant = 0;
460                aim_tlv_t *tmptlv;
461                aim_bstream_t occbs;
462
463                tmptlv = aim_gettlv(tlvlist, 0x0073, 1);
464
465                /* Allocate enough userinfo structs for all occupants */
466                userinfo = calloc(usercount, sizeof(aim_userinfo_t));
467
468                aim_bstream_init(&occbs, tmptlv->value, tmptlv->length);
469
470                while (curoccupant < usercount)
471                        aim_extractuserinfo(sess, &occbs, &userinfo[curoccupant++]);
472        }
473
474        /*
475         * Type 0x00c9: Flags. (AIM_CHATROOM_FLAG)
476         */
477        if (aim_gettlv(tlvlist, 0x00c9, 1))
478                flags = aim_gettlv16(tlvlist, 0x00c9, 1);
479
480        /*
481         * Type 0x00ca: Creation time (4 bytes)
482         */
483        if (aim_gettlv(tlvlist, 0x00ca, 1))
484                creationtime = aim_gettlv32(tlvlist, 0x00ca, 1);
485
486        /*
487         * Type 0x00d1: Maximum Message Length
488         */
489        if (aim_gettlv(tlvlist, 0x00d1, 1))
490                maxmsglen = aim_gettlv16(tlvlist, 0x00d1, 1);
491
492        /*
493         * Type 0x00d2: Unknown. (2 bytes)
494         */
495        if (aim_gettlv(tlvlist, 0x00d2, 1))
496                unknown_d2 = aim_gettlv16(tlvlist, 0x00d2, 1);
497
498        /*
499         * Type 0x00d3: Room Description
500         */
501        if (aim_gettlv(tlvlist, 0x00d3, 1))
502                roomdesc = aim_gettlv_str(tlvlist, 0x00d3, 1);
503
504        /*
505         * Type 0x000d4: Unknown (flag only)
506         */
507        if (aim_gettlv(tlvlist, 0x000d4, 1))
508                ;
509
510        /*
511         * Type 0x00d5: Unknown. (1 byte)
512         */
513        if (aim_gettlv(tlvlist, 0x00d5, 1))
514                unknown_d5 = aim_gettlv8(tlvlist, 0x00d5, 1);
515
516
517        /*
518         * Type 0x00d6: Encoding 1 ("us-ascii")
519         */
520        if (aim_gettlv(tlvlist, 0x000d6, 1))
521                ;
522       
523        /*
524         * Type 0x00d7: Language 1 ("en")
525         */
526        if (aim_gettlv(tlvlist, 0x000d7, 1))
527                ;
528
529        /*
530         * Type 0x00d8: Encoding 2 ("us-ascii")
531         */
532        if (aim_gettlv(tlvlist, 0x000d8, 1))
533                ;
534       
535        /*
536         * Type 0x00d9: Language 2 ("en")
537         */
538        if (aim_gettlv(tlvlist, 0x000d9, 1))
539                ;
540
541        /*
542         * Type 0x00da: Maximum visible message length
543         */
544        if (aim_gettlv(tlvlist, 0x000da, 1))
545                maxvisiblemsglen = aim_gettlv16(tlvlist, 0x00da, 1);
546
547        if ((userfunc = aim_callhandler(sess, rx->conn, snac->family, snac->subtype))) {
548                ret = userfunc(sess,
549                                rx, 
550                                &roominfo,
551                                roomname,
552                                usercount,
553                                userinfo,       
554                                roomdesc,
555                                flags,
556                                creationtime,
557                                maxmsglen,
558                                unknown_d2,
559                                unknown_d5,
560                                maxvisiblemsglen);
561        }
562
563        free(roominfo.name);
564        free(userinfo);
565        free(roomname);
566        free(roomdesc);
567        aim_freetlvchain(&tlvlist);
568
569        return ret;
570}
571
572static int userlistchange(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
573{
574        aim_userinfo_t *userinfo = NULL;
575        aim_rxcallback_t userfunc;
576        int curcount = 0, ret = 0;
577
578        while (aim_bstream_empty(bs)) {
579                curcount++;
580                userinfo = realloc(userinfo, curcount * sizeof(aim_userinfo_t));
581                aim_extractuserinfo(sess, bs, &userinfo[curcount-1]);
582        }
583
584        if ((userfunc = aim_callhandler(sess, rx->conn, snac->family, snac->subtype)))
585                ret = userfunc(sess, rx, curcount, userinfo);
586
587        free(userinfo);
588
589        return ret;
590}
591
592/*
593 * We could probably include this in the normal ICBM parsing
594 * code as channel 0x0003, however, since only the start
595 * would be the same, we might as well do it here.
596 *
597 * General outline of this SNAC:
598 *   snac
599 *   cookie
600 *   channel id
601 *   tlvlist
602 *     unknown
603 *     source user info
604 *       name
605 *       evility
606 *       userinfo tlvs
607 *         online time
608 *         etc
609 *     message metatlv
610 *       message tlv
611 *         message string
612 *       possibly others
613 * 
614 */
615static int incomingmsg(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
616{
617        aim_userinfo_t userinfo;
618        aim_rxcallback_t userfunc;     
619        int ret = 0;
620        fu8_t *cookie;
621        fu16_t channel;
622        aim_tlvlist_t *otl;
623        char *msg = NULL;
624        aim_msgcookie_t *ck;
625
626        memset(&userinfo, 0, sizeof(aim_userinfo_t));
627
628        /*
629         * ICBM Cookie.  Uncache it.
630         */
631        cookie = aimbs_getraw(bs, 8);
632
633        if ((ck = aim_uncachecookie(sess, cookie, AIM_COOKIETYPE_CHAT))) {
634                free(ck->data);
635                free(ck);
636        }
637
638        /*
639         * Channel ID
640         *
641         * Channels 1 and 2 are implemented in the normal ICBM
642         * parser.
643         *
644         * We only do channel 3 here.
645         *
646         */
647        channel = aimbs_get16(bs);
648
649        if (channel != 0x0003) {
650                faimdprintf(sess, 0, "faim: chat_incoming: unknown channel! (0x%04x)\n", channel);
651                return 0;
652        }
653
654        /*
655         * Start parsing TLVs right away.
656         */
657        otl = aim_readtlvchain(bs);
658
659        /*
660         * Type 0x0003: Source User Information
661         */
662        if (aim_gettlv(otl, 0x0003, 1)) {
663                aim_tlv_t *userinfotlv;
664                aim_bstream_t tbs;
665
666                userinfotlv = aim_gettlv(otl, 0x0003, 1);
667
668                aim_bstream_init(&tbs, userinfotlv->value, userinfotlv->length);
669                aim_extractuserinfo(sess, &tbs, &userinfo);
670        }
671
672        /*
673         * Type 0x0001: If present, it means it was a message to the
674         * room (as opposed to a whisper).
675         */
676        if (aim_gettlv(otl, 0x0001, 1))
677                ;
678
679        /*
680         * Type 0x0005: Message Block.  Conains more TLVs.
681         */
682        if (aim_gettlv(otl, 0x0005, 1)) {
683                aim_tlvlist_t *itl;
684                aim_tlv_t *msgblock;
685                aim_bstream_t tbs;
686
687                msgblock = aim_gettlv(otl, 0x0005, 1);
688                aim_bstream_init(&tbs, msgblock->value, msgblock->length);
689                itl = aim_readtlvchain(&tbs);
690
691                /*
692                 * Type 0x0001: Message.
693                 */     
694                if (aim_gettlv(itl, 0x0001, 1))
695                        msg = aim_gettlv_str(itl, 0x0001, 1);
696
697                aim_freetlvchain(&itl); 
698        }
699
700        if ((userfunc = aim_callhandler(sess, rx->conn, snac->family, snac->subtype)))
701                ret = userfunc(sess, rx, &userinfo, msg);
702
703        free(cookie);
704        free(msg);
705        aim_freetlvchain(&otl);
706
707        return ret;
708}
709
710static int snachandler(aim_session_t *sess, aim_module_t *mod, aim_frame_t *rx, aim_modsnac_t *snac, aim_bstream_t *bs)
711{
712
713        if (snac->subtype == 0x0002)
714                return infoupdate(sess, mod, rx, snac, bs);
715        else if ((snac->subtype == 0x0003) || (snac->subtype == 0x0004))
716                return userlistchange(sess, mod, rx, snac, bs);
717        else if (snac->subtype == 0x0006)
718                return incomingmsg(sess, mod, rx, snac, bs);
719
720        return 0;
721}
722
723faim_internal int chat_modfirst(aim_session_t *sess, aim_module_t *mod)
724{
725
726        mod->family = 0x000e;
727        mod->version = 0x0001;
728        mod->toolid = 0x0004; /* XXX this doesn't look right */
729        mod->toolversion = 0x0001; /* nor does this */
730        mod->flags = 0;
731        strncpy(mod->name, "chat", sizeof(mod->name));
732        mod->snachandler = snachandler;
733
734        return 0;
735}
736
737
Note: See TracBrowser for help on using the repository browser.