source: protocols/twitter/twitter_lib.c @ bb5ce4d1

Last change on this file since bb5ce4d1 was bb5ce4d1, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-05-23T12:50:51Z

Added base_url settting to Twitter module so other services using the
Twitter API can be used. Only with Basic authentication though.

  • Property mode set to 100644
File size: 17.9 KB
RevLine 
[1b221e0]1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
6*  Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com>                 *
7*                                                                           *
8*  This library is free software; you can redistribute it and/or            *
9*  modify it under the terms of the GNU Lesser General Public               *
10*  License as published by the Free Software Foundation, version            *
11*  2.1.                                                                     *
12*                                                                           *
13*  This library 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 GNU        *
16*  Lesser General Public License for more details.                          *
17*                                                                           *
18*  You should have received a copy of the GNU Lesser General Public License *
19*  along with this library; if not, write to the Free Software Foundation,  *
20*  Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA           *
21*                                                                           *
22****************************************************************************/
23
[08579a1]24/* For strptime(): */
25#define _XOPEN_SOURCE
26
[1b221e0]27#include "twitter_http.h"
28#include "twitter.h"
29#include "bitlbee.h"
30#include "url.h"
31#include "misc.h"
32#include "base64.h"
33#include "xmltree.h"
34#include "twitter_lib.h"
35#include <ctype.h>
36#include <errno.h>
37
38#define TXL_STATUS 1
[62d2cfb]39#define TXL_USER 2
40#define TXL_ID 3
41
[1b221e0]42struct twitter_xml_list {
[62d2cfb]43        int type;
[1b221e0]44        int next_cursor;
45        GSList *list;
46        gpointer data;
47};
48
49struct twitter_xml_user {
50        char *name;
51        char *screen_name;
52};
53
54struct twitter_xml_status {
[08579a1]55        time_t created_at;
[1b221e0]56        char *text;
57        struct twitter_xml_user *user;
58        guint64 id;
59};
60
[62d2cfb]61/**
62 * Frees a twitter_xml_user struct.
63 */
64static void txu_free(struct twitter_xml_user *txu)
65{
66        g_free(txu->name);
67        g_free(txu->screen_name);
[2abceca]68        g_free(txu);
[62d2cfb]69}
70
71
72/**
73 * Frees a twitter_xml_status struct.
74 */
75static void txs_free(struct twitter_xml_status *txs)
76{
77        g_free(txs->text);
78        txu_free(txs->user);
[2abceca]79        g_free(txs);
[62d2cfb]80}
81
82/**
83 * Free a twitter_xml_list struct.
84 * type is the type of list the struct holds.
85 */
86static void txl_free(struct twitter_xml_list *txl)
87{
88        GSList *l;
89        for ( l = txl->list; l ; l = g_slist_next(l) )
90                if (txl->type == TXL_STATUS)
91                        txs_free((struct twitter_xml_status *)l->data);
92                else if (txl->type == TXL_ID)
93                        g_free(l->data);
94        g_slist_free(txl->list);
95}
96
97/**
98 * Add a buddy if it is not allready added, set the status to logged in.
99 */
[3e69802]100static void twitter_add_buddy(struct im_connection *ic, char *name, const char *fullname)
[62d2cfb]101{
[1014cab]102        struct twitter_data *td = ic->proto_data;
103
[62d2cfb]104        // Check if the buddy is allready in the buddy list.
[d569019]105        if (!imcb_find_buddy( ic, name ))
[62d2cfb]106        {
[e88fbe27]107                char *mode = set_getstr(&ic->acc->set, "mode");
108               
[62d2cfb]109                // The buddy is not in the list, add the buddy and set the status to logged in.
110                imcb_add_buddy( ic, name, NULL );
[3e69802]111                imcb_rename_buddy( ic, name, fullname );
[e88fbe27]112                if (g_strcasecmp(mode, "chat") == 0)
[1014cab]113                        imcb_chat_add_buddy( td->home_timeline_gc, name );
[e88fbe27]114                else if (g_strcasecmp(mode, "many") == 0)
[1014cab]115                        imcb_buddy_status( ic, name, OPT_LOGGED_IN, NULL, NULL );
[62d2cfb]116        }
117}
[1b221e0]118
119static void twitter_http_get_friends_ids(struct http_request *req);
120
121/**
122 * Get the friends ids.
123 */
124void twitter_get_friends_ids(struct im_connection *ic, int next_cursor)
125{
126        struct twitter_data *td = ic->proto_data;
127
128        // Primitive, but hey! It works...     
129        char* args[2];
130        args[0] = "cursor";
131        args[1] = g_strdup_printf ("%d", next_cursor);
[bb5ce4d1]132        twitter_http(ic, TWITTER_FRIENDS_IDS_URL, twitter_http_get_friends_ids, ic, 0, args, 2);
[1b221e0]133
134        g_free(args[1]);
135}
136
137/**
138 * Function to help fill a list.
139 */
140static xt_status twitter_xt_next_cursor( struct xt_node *node, struct twitter_xml_list *txl )
141{
142        // Do something with the cursor.
[2abceca]143        txl->next_cursor = node->text != NULL ? atoi(node->text) : -1;
[1b221e0]144
145        return XT_HANDLED;
146}
147
148/**
149 * Fill a list of ids.
150 */
151static xt_status twitter_xt_get_friends_id_list( struct xt_node *node, struct twitter_xml_list *txl )
152{
153        struct xt_node *child;
[62d2cfb]154       
155        // Set the list type.
156        txl->type = TXL_ID;
[1b221e0]157
158        // The root <statuses> node should hold the list of statuses <status>
159        // Walk over the nodes children.
160        for( child = node->children ; child ; child = child->next )
161        {
162                if ( g_strcasecmp( "id", child->name ) == 0)
163                {
164                        // Add the item to the list.
[2abceca]165                        txl->list = g_slist_append (txl->list, g_memdup( child->text, child->text_len + 1 ));
[1b221e0]166                }
167                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
168                {
169                        twitter_xt_next_cursor(child, txl);
170                }
171        }
172
173        return XT_HANDLED;
174}
175
176/**
177 * Callback for getting the friends ids.
178 */
179static void twitter_http_get_friends_ids(struct http_request *req)
180{
181        struct im_connection *ic;
182        struct xt_parser *parser;
183        struct twitter_xml_list *txl;
[3bd4a93]184        struct twitter_data *td;
[1b221e0]185
186        ic = req->data;
187
[62d2cfb]188        // Check if the connection is still active.
189        if( !g_slist_find( twitter_connections, ic ) )
190                return;
[37aa317]191       
192        td = ic->proto_data;
[62d2cfb]193
[1b221e0]194        // Check if the HTTP request went well.
195        if (req->status_code != 200) {
196                // It didn't go well, output the error and return.
[3bd4a93]197                if (++td->http_fails >= 5)
198                        imcb_error(ic, "Could not retrieve friends. HTTP STATUS: %d", req->status_code);
199               
[1b221e0]200                return;
[3bd4a93]201        } else {
202                td->http_fails = 0;
[1b221e0]203        }
204
205        txl = g_new0(struct twitter_xml_list, 1);
206
207        // Parse the data.
208        parser = xt_new( NULL, txl );
209        xt_feed( parser, req->reply_body, req->body_size );
210        twitter_xt_get_friends_id_list(parser->root, txl);
211        xt_free( parser );
212
213        if (txl->next_cursor)
214                twitter_get_friends_ids(ic, txl->next_cursor);
215
[62d2cfb]216        txl_free(txl);
[1b221e0]217        g_free(txl);
218}
219
220/**
221 * Function to fill a twitter_xml_user struct.
222 * It sets:
223 *  - the name and
224 *  - the screen_name.
225 */
226static xt_status twitter_xt_get_user( struct xt_node *node, struct twitter_xml_user *txu )
227{
228        struct xt_node *child;
229
230        // Walk over the nodes children.
231        for( child = node->children ; child ; child = child->next )
232        {
233                if ( g_strcasecmp( "name", child->name ) == 0)
234                {
235                        txu->name = g_memdup( child->text, child->text_len + 1 );
236                }
237                else if (g_strcasecmp( "screen_name", child->name ) == 0)
238                {
239                        txu->screen_name = g_memdup( child->text, child->text_len + 1 );
240                }
241        }
242        return XT_HANDLED;
243}
244
[62d2cfb]245/**
246 * Function to fill a twitter_xml_list struct.
247 * It sets:
248 *  - all <user>s from the <users> element.
249 */
250static xt_status twitter_xt_get_users( struct xt_node *node, struct twitter_xml_list *txl )
251{
252        struct twitter_xml_user *txu;
253        struct xt_node *child;
254
255        // Set the type of the list.
256        txl->type = TXL_USER;
257
258        // The root <users> node should hold the list of users <user>
259        // Walk over the nodes children.
260        for( child = node->children ; child ; child = child->next )
261        {
262                if ( g_strcasecmp( "user", child->name ) == 0)
263                {
264                        txu = g_new0(struct twitter_xml_user, 1);
265                        twitter_xt_get_user(child, txu);
266                        // Put the item in the front of the list.
267                        txl->list = g_slist_prepend (txl->list, txu);
268                }
269        }
270
271        return XT_HANDLED;
272}
273
274/**
275 * Function to fill a twitter_xml_list struct.
276 * It calls twitter_xt_get_users to get the <user>s from a <users> element.
277 * It sets:
278 *  - the next_cursor.
279 */
280static xt_status twitter_xt_get_user_list( struct xt_node *node, struct twitter_xml_list *txl )
281{
282        struct xt_node *child;
283
284        // Set the type of the list.
285        txl->type = TXL_USER;
286
287        // The root <user_list> node should hold a users <users> element
288        // Walk over the nodes children.
289        for( child = node->children ; child ; child = child->next )
290        {
291                if ( g_strcasecmp( "users", child->name ) == 0)
292                {
293                        twitter_xt_get_users(child, txl);
294                }
295                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
296                {
297                        twitter_xt_next_cursor(child, txl);
298                }
299        }
300
301        return XT_HANDLED;
302}
303
304
[1b221e0]305/**
306 * Function to fill a twitter_xml_status struct.
307 * It sets:
308 *  - the status text and
309 *  - the created_at timestamp and
310 *  - the status id and
311 *  - the user in a twitter_xml_user struct.
312 */
313static xt_status twitter_xt_get_status( struct xt_node *node, struct twitter_xml_status *txs )
314{
315        struct xt_node *child;
316
317        // Walk over the nodes children.
318        for( child = node->children ; child ; child = child->next )
319        {
320                if ( g_strcasecmp( "text", child->name ) == 0)
321                {
322                        txs->text = g_memdup( child->text, child->text_len + 1 );
323                }
324                else if (g_strcasecmp( "created_at", child->name ) == 0)
325                {
[08579a1]326                        struct tm parsed;
327                       
328                        /* Very sensitive to changes to the formatting of
329                           this field. :-( Also assumes the timezone used
330                           is UTC since C time handling functions suck. */
331                        if( strptime( child->text, "%a %b %d %H:%M:%S %z %Y", &parsed ) != NULL )
332                                txs->created_at = mktime_utc( &parsed );
[1b221e0]333                }
334                else if (g_strcasecmp( "user", child->name ) == 0)
335                {
336                        txs->user = g_new0(struct twitter_xml_user, 1);
337                        twitter_xt_get_user( child, txs->user );
338                }
339                else if (g_strcasecmp( "id", child->name ) == 0)
340                {
341                        txs->id = g_ascii_strtoull (child->text, NULL, 10);
342                }
343        }
344        return XT_HANDLED;
345}
346
347/**
348 * Function to fill a twitter_xml_list struct.
349 * It sets:
350 *  - all <status>es within the <status> element and
351 *  - the next_cursor.
352 */
353static xt_status twitter_xt_get_status_list( struct xt_node *node, struct twitter_xml_list *txl )
354{
355        struct twitter_xml_status *txs;
356        struct xt_node *child;
357
[62d2cfb]358        // Set the type of the list.
359        txl->type = TXL_STATUS;
360
[1b221e0]361        // The root <statuses> node should hold the list of statuses <status>
362        // Walk over the nodes children.
363        for( child = node->children ; child ; child = child->next )
364        {
365                if ( g_strcasecmp( "status", child->name ) == 0)
366                {
367                        txs = g_new0(struct twitter_xml_status, 1);
368                        twitter_xt_get_status(child, txs);
369                        // Put the item in the front of the list.
370                        txl->list = g_slist_prepend (txl->list, txs);
371                }
372                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
373                {
374                        twitter_xt_next_cursor(child, txl);
375                }
376        }
377
378        return XT_HANDLED;
379}
380
381static void twitter_http_get_home_timeline(struct http_request *req);
382
383/**
384 * Get the timeline.
385 */
386void twitter_get_home_timeline(struct im_connection *ic, int next_cursor)
387{
388        struct twitter_data *td = ic->proto_data;
389
390        char* args[4];
391        args[0] = "cursor";
392        args[1] = g_strdup_printf ("%d", next_cursor);
393        if (td->home_timeline_id) {
394                args[2] = "since_id";
[0519b0a]395                args[3] = g_strdup_printf ("%llu", (long long unsigned int) td->home_timeline_id);
[1b221e0]396        }
397
[bb5ce4d1]398        twitter_http(ic, TWITTER_HOME_TIMELINE_URL, twitter_http_get_home_timeline, ic, 0, args, td->home_timeline_id ? 4 : 2);
[1b221e0]399
400        g_free(args[1]);
401        if (td->home_timeline_id) {
402                g_free(args[3]);
403        }
404}
405
[62d2cfb]406/**
407 * Function that is called to see the statuses in a groupchat window.
408 */
409static void twitter_groupchat(struct im_connection *ic, GSList *list)
410{
411        struct twitter_data *td = ic->proto_data;
412        GSList *l = NULL;
413        struct twitter_xml_status *status;
414        struct groupchat *gc;
415
416        // Create a new groupchat if it does not exsist.
417        if (!td->home_timeline_gc)
418        {   
[cca0692]419                char *name_hint = g_strdup_printf( "Twitter_%s", ic->acc->user );
[62d2cfb]420                td->home_timeline_gc = gc = imcb_chat_new( ic, "home/timeline" );
[cca0692]421                imcb_chat_name_hint( gc, name_hint );
422                g_free( name_hint );
[62d2cfb]423                // Add the current user to the chat...
424                imcb_chat_add_buddy( gc, ic->acc->user );
425        }
426        else
427        {   
428                gc = td->home_timeline_gc;
429        }
430
431        for ( l = list; l ; l = g_slist_next(l) )
432        {
433                status = l->data;
[3e69802]434                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
[d569019]435               
[0b3ffb1]436                strip_html(status->text);
437               
[62d2cfb]438                // Say it!
[d569019]439                if (g_strcasecmp(td->user, status->user->screen_name) == 0)
440                        imcb_chat_log (gc, "Your Tweet: %s", status->text);
441                else
[08579a1]442                        imcb_chat_msg (gc, status->user->screen_name, status->text, 0, status->created_at );
[d569019]443               
[62d2cfb]444                // Update the home_timeline_id to hold the highest id, so that by the next request
445                // we won't pick up the updates allready in the list.
446                td->home_timeline_id = td->home_timeline_id < status->id ? status->id : td->home_timeline_id;
447        }
448}
449
450/**
451 * Function that is called to see statuses as private messages.
452 */
453static void twitter_private_message_chat(struct im_connection *ic, GSList *list)
454{
455        struct twitter_data *td = ic->proto_data;
456        GSList *l = NULL;
457        struct twitter_xml_status *status;
[e88fbe27]458        char from[MAX_STRING];
459        gboolean mode_one;
460       
461        mode_one = g_strcasecmp( set_getstr( &ic->acc->set, "mode" ), "one" ) == 0;
[62d2cfb]462
[e88fbe27]463        if( mode_one )
464        {
465                g_snprintf( from, sizeof( from ) - 1, "twitter_%s", ic->acc->user );
466                from[MAX_STRING-1] = '\0';
467        }
468       
[62d2cfb]469        for ( l = list; l ; l = g_slist_next(l) )
470        {
[e88fbe27]471                char *text = NULL;
472               
[62d2cfb]473                status = l->data;
[e88fbe27]474               
[0b3ffb1]475                strip_html( status->text );
[e88fbe27]476                if( mode_one )
477                        text = g_strdup_printf( "\002<\002%s\002>\002 %s",
478                                                status->user->screen_name, status->text );
[55b1e69]479                else
480                        twitter_add_buddy(ic, status->user->screen_name, status->user->name);
[e88fbe27]481               
482                imcb_buddy_msg( ic,
483                                mode_one ? from : status->user->screen_name,
484                                mode_one ? text : status->text,
485                                0, status->created_at );
486               
[62d2cfb]487                // Update the home_timeline_id to hold the highest id, so that by the next request
488                // we won't pick up the updates allready in the list.
489                td->home_timeline_id = td->home_timeline_id < status->id ? status->id : td->home_timeline_id;
[e88fbe27]490               
491                g_free( text );
[62d2cfb]492        }
493}
494
[1b221e0]495/**
496 * Callback for getting the home timeline.
497 */
498static void twitter_http_get_home_timeline(struct http_request *req)
499{
[62d2cfb]500        struct im_connection *ic = req->data;
[37aa317]501        struct twitter_data *td;
[1b221e0]502        struct xt_parser *parser;
503        struct twitter_xml_list *txl;
[62d2cfb]504
505        // Check if the connection is still active.
506        if( !g_slist_find( twitter_connections, ic ) )
507                return;
[37aa317]508       
509        td = ic->proto_data;
[1b221e0]510
511        // Check if the HTTP request went well.
[3bd4a93]512        if (req->status_code == 200)
513        {
514                td->http_fails = 0;
[c2ecadc]515                if (!(ic->flags & OPT_LOGGED_IN))
[3bd4a93]516                        imcb_connected(ic);
517        }
518        else if (req->status_code == 401)
519        {
520                imcb_error( ic, "Authentication failure" );
521                imc_logout( ic, FALSE );
522                return;
523        }
524        else
525        {
[1b221e0]526                // It didn't go well, output the error and return.
[3bd4a93]527                if (++td->http_fails >= 5)
528                        imcb_error(ic, "Could not retrieve " TWITTER_HOME_TIMELINE_URL ". HTTP STATUS: %d", req->status_code);
529               
[1b221e0]530                return;
531        }
532
533        txl = g_new0(struct twitter_xml_list, 1);
534        txl->list = NULL;
[62d2cfb]535
[1b221e0]536        // Parse the data.
537        parser = xt_new( NULL, txl );
538        xt_feed( parser, req->reply_body, req->body_size );
539        // The root <statuses> node should hold the list of statuses <status>
540        twitter_xt_get_status_list(parser->root, txl);
541        xt_free( parser );
542
[62d2cfb]543        // See if the user wants to see the messages in a groupchat window or as private messages.
[e88fbe27]544        if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0)
[62d2cfb]545                twitter_groupchat(ic, txl->list);
[b4dd253]546        else
[62d2cfb]547                twitter_private_message_chat(ic, txl->list);
[1b221e0]548
549        // Free the structure. 
[62d2cfb]550        txl_free(txl);
[1b221e0]551        g_free(txl);
552}
553
554/**
[62d2cfb]555 * Callback for getting (twitter)friends...
556 *
557 * Be afraid, be very afraid! This function will potentially add hundreds of "friends". "Who has
558 * hundreds of friends?" you wonder? You probably not, since you are reading the source of
559 * BitlBee... Get a life and meet new people!
[1b221e0]560 */
[62d2cfb]561static void twitter_http_get_statuses_friends(struct http_request *req)
[1b221e0]562{
[62d2cfb]563        struct im_connection *ic = req->data;
[37aa317]564        struct twitter_data *td;
[62d2cfb]565        struct xt_parser *parser;
566        struct twitter_xml_list *txl;
[2abceca]567        GSList *l = NULL;
568        struct twitter_xml_user *user;
[62d2cfb]569
570        // Check if the connection is still active.
571        if( !g_slist_find( twitter_connections, ic ) )
572                return;
[37aa317]573       
574        td = ic->proto_data;
575       
[62d2cfb]576        // Check if the HTTP request went well.
577        if (req->status_code != 200) {
578                // It didn't go well, output the error and return.
[3bd4a93]579                if (++td->http_fails >= 5)
580                        imcb_error(ic, "Could not retrieve " TWITTER_SHOW_FRIENDS_URL " HTTP STATUS: %d", req->status_code);
581               
[62d2cfb]582                return;
[3bd4a93]583        } else {
584                td->http_fails = 0;
[62d2cfb]585        }
586
587        txl = g_new0(struct twitter_xml_list, 1);
588        txl->list = NULL;
589
590        // Parse the data.
591        parser = xt_new( NULL, txl );
592        xt_feed( parser, req->reply_body, req->body_size );
593
594        // Get the user list from the parsed xml feed.
595        twitter_xt_get_user_list(parser->root, txl);
596        xt_free( parser );
597
598        // Add the users as buddies.
[1b221e0]599        for ( l = txl->list; l ; l = g_slist_next(l) )
[62d2cfb]600        {
601                user = l->data;
[3e69802]602                twitter_add_buddy(ic, user->screen_name, user->name);
[62d2cfb]603        }
[1b221e0]604
[62d2cfb]605        // if the next_cursor is set to something bigger then 0 there are more friends to gather.
606        if (txl->next_cursor > 0)
607                twitter_get_statuses_friends(ic, txl->next_cursor);
608
609        // Free the structure.
610        txl_free(txl);
611        g_free(txl);
[1b221e0]612}
613
614/**
[62d2cfb]615 * Get the friends.
[1b221e0]616 */
[62d2cfb]617void twitter_get_statuses_friends(struct im_connection *ic, int next_cursor)
[1b221e0]618{
[62d2cfb]619        struct twitter_data *td = ic->proto_data;
620
621        char* args[2];
622        args[0] = "cursor";
623        args[1] = g_strdup_printf ("%d", next_cursor);
624
[bb5ce4d1]625        twitter_http(ic, TWITTER_SHOW_FRIENDS_URL, twitter_http_get_statuses_friends, ic, 0, args, 2);
[62d2cfb]626
627        g_free(args[1]);
[1b221e0]628}
629
630/**
631 * Callback after sending a new update to twitter.
632 */
633static void twitter_http_post_status(struct http_request *req)
634{
635        struct im_connection *ic = req->data;
636
[62d2cfb]637        // Check if the connection is still active.
638        if( !g_slist_find( twitter_connections, ic ) )
639                return;
640
[1b221e0]641        // Check if the HTTP request went well.
642        if (req->status_code != 200) {
643                // It didn't go well, output the error and return.
[e88fbe27]644                imcb_error(ic, "Could not post message... HTTP STATUS: %d", req->status_code);
[1b221e0]645                return;
646        }
647}
648
649/**
650 * Function to POST a new status to twitter.
651 */ 
652void twitter_post_status(struct im_connection *ic, char* msg)
653{
654        struct twitter_data *td = ic->proto_data;
655
656        char* args[2];
657        args[0] = "status";
658        args[1] = msg;
[bb5ce4d1]659        twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post_status, ic, 1, args, 2);
[62d2cfb]660//      g_free(args[1]);
[1b221e0]661}
662
663
[62d2cfb]664/**
665 * Function to POST a new message to twitter.
666 */
667void twitter_direct_messages_new(struct im_connection *ic, char *who, char *msg)
668{
669        struct twitter_data *td = ic->proto_data;
670
671        char* args[4];
672        args[0] = "screen_name";
673        args[1] = who;
674        args[2] = "text";
675        args[3] = msg;
676        // Use the same callback as for twitter_post_status, since it does basically the same.
[bb5ce4d1]677        twitter_http(ic, TWITTER_DIRECT_MESSAGES_NEW_URL, twitter_http_post_status, ic, 1, args, 4);
[62d2cfb]678//      g_free(args[1]);
679//      g_free(args[3]);
680}
Note: See TracBrowser for help on using the repository browser.