source: root_commands.c @ b11b781

3.0.1
Last change on this file since b11b781 was bedad20, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-11-24T22:32:12Z

Some polishing/documentation for the "account add" without password hack.

  • Property mode set to 100644
File size: 35.3 KB
Line 
1  /********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2010 Wilmer van der Gaast and others                *
5  \********************************************************************/
6
7/* User manager (root) commands                                         */
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#define BITLBEE_CORE
27#include "commands.h"
28#include "bitlbee.h"
29#include "help.h"
30#include "ipc.h"
31
32void root_command_string( irc_t *irc, char *command )
33{
34        root_command( irc, split_command_parts( command ) );
35}
36
37#define MIN_ARGS( x, y... )                                                    \
38        do                                                                     \
39        {                                                                      \
40                int blaat;                                                     \
41                for( blaat = 0; blaat <= x; blaat ++ )                         \
42                        if( cmd[blaat] == NULL )                               \
43                        {                                                      \
44                                irc_usermsg( irc, "Not enough parameters given (need %d).", x ); \
45                                return y;                                      \
46                        }                                                      \
47        } while( 0 )
48
49void root_command( irc_t *irc, char *cmd[] )
50{       
51        int i, len;
52       
53        if( !cmd[0] )
54                return;
55       
56        len = strlen( cmd[0] );
57        for( i = 0; root_commands[i].command; i++ )
58                if( g_strncasecmp( root_commands[i].command, cmd[0], len ) == 0 )
59                {
60                        if( root_commands[i+1].command &&
61                            g_strncasecmp( root_commands[i+1].command, cmd[0], len ) == 0 )
62                                /* Only match on the first letters if the match is unique. */
63                                break;
64                       
65                        MIN_ARGS( root_commands[i].required_parameters );
66                       
67                        root_commands[i].execute( irc, cmd );
68                        return;
69                }
70       
71        irc_usermsg( irc, "Unknown command: %s. Please use \x02help commands\x02 to get a list of available commands.", cmd[0] );
72}
73
74static void cmd_help( irc_t *irc, char **cmd )
75{
76        char param[80];
77        int i;
78        char *s;
79       
80        memset( param, 0, sizeof(param) );
81        for ( i = 1; (cmd[i] != NULL && ( strlen(param) < (sizeof(param)-1) ) ); i++ ) {
82                if ( i != 1 )   // prepend space except for the first parameter
83                        strcat(param, " ");
84                strncat( param, cmd[i], sizeof(param) - strlen(param) - 1 );
85        }
86
87        s = help_get( &(global.help), param );
88        if( !s ) s = help_get( &(global.help), "" );
89       
90        if( s )
91        {
92                irc_usermsg( irc, "%s", s );
93                g_free( s );
94        }
95        else
96        {
97                irc_usermsg( irc, "Error opening helpfile." );
98        }
99}
100
101static void cmd_account( irc_t *irc, char **cmd );
102static void bitlbee_whatsnew( irc_t *irc );
103
104static void cmd_identify( irc_t *irc, char **cmd )
105{
106        storage_status_t status;
107        gboolean load = TRUE;
108        char *password = cmd[1];
109       
110        if( irc->status & USTATUS_IDENTIFIED )
111        {
112                irc_usermsg( irc, "You're already logged in." );
113                return;
114        }
115       
116        if( strncmp( cmd[1], "-no", 3 ) == 0 )
117        {
118                load = FALSE;
119                password = cmd[2];
120        }
121        else if( strncmp( cmd[1], "-force", 6 ) == 0 )
122        {
123                password = cmd[2];
124        }
125        else if( irc->b->accounts != NULL )
126        {
127                irc_usermsg( irc,
128                             "You're trying to identify yourself, but already have "
129                             "at least one IM account set up. "
130                             "Use \x02identify -noload\x02 or \x02identify -force\x02 "
131                             "instead (see \x02help identify\x02)." );
132                return;
133        }
134       
135        if( password == NULL )
136        {
137                MIN_ARGS( 2 );
138        }
139       
140        if( load )
141                status = storage_load( irc, password );
142        else
143                status = storage_check_pass( irc->user->nick, password );
144       
145        switch (status) {
146        case STORAGE_INVALID_PASSWORD:
147                irc_usermsg( irc, "Incorrect password" );
148                break;
149        case STORAGE_NO_SUCH_USER:
150                irc_usermsg( irc, "The nick is (probably) not registered" );
151                break;
152        case STORAGE_OK:
153                irc_usermsg( irc, "Password accepted%s",
154                             load ? ", settings and accounts loaded" : "" );
155                irc_setpass( irc, password );
156                irc->status |= USTATUS_IDENTIFIED;
157                irc_umode_set( irc, "+R", 1 );
158               
159                bitlbee_whatsnew( irc );
160               
161                /* The following code is a bit hairy now. With takeover
162                   support, we shouldn't immediately auto_connect in case
163                   we're going to offer taking over an existing session.
164                   Do it in 200ms since that should give the parent process
165                   enough time to come back to us. */
166                if( load )
167                {
168                        irc_channel_auto_joins( irc, NULL );
169                        if( !set_getbool( &irc->default_channel->set, "auto_join" ) )
170                                irc_channel_del_user( irc->default_channel, irc->user,
171                                                      IRC_CDU_PART, "auto_join disabled "
172                                                      "for this channel." );
173                        if( set_getbool( &irc->b->set, "auto_connect" ) )
174                                irc->login_source_id = b_timeout_add( 200,
175                                        cmd_identify_finish, irc );
176                }
177               
178                /* If ipc_child_identify() returns FALSE, it means we're
179                   already sure that there's no takeover target (only
180                   possible in 1-process daemon mode). Start auto_connect
181                   immediately. */
182                if( !ipc_child_identify( irc ) && load &&
183                    set_getbool( &irc->b->set, "auto_connect" ) )
184                        cmd_identify_finish( irc, 0, 0 );
185               
186                break;
187        case STORAGE_OTHER_ERROR:
188        default:
189                irc_usermsg( irc, "Unknown error while loading configuration" );
190                break;
191        }
192}
193
194gboolean cmd_identify_finish( gpointer data, gint fd, b_input_condition cond )
195{
196        char *account_on[] = { "account", "on", NULL };
197        irc_t *irc = data;
198       
199        cmd_account( irc, account_on );
200       
201        b_event_remove( irc->login_source_id );
202        irc->login_source_id = -1;
203        return FALSE;
204}
205
206static void cmd_register( irc_t *irc, char **cmd )
207{
208        char s[16];
209       
210        if( global.conf->authmode == AUTHMODE_REGISTERED )
211        {
212                irc_usermsg( irc, "This server does not allow registering new accounts" );
213                return;
214        }
215
216        switch( storage_save( irc, cmd[1], FALSE ) ) {
217                case STORAGE_ALREADY_EXISTS:
218                        irc_usermsg( irc, "Nick is already registered" );
219                        break;
220                       
221                case STORAGE_OK:
222                        irc_usermsg( irc, "Account successfully created" );
223                        irc_setpass( irc, cmd[1] );
224                        irc->status |= USTATUS_IDENTIFIED;
225                        irc_umode_set( irc, "+R", 1 );
226                       
227                        /* Set this var now, or anyone who logs in to his/her
228                           newly created account for the first time gets the
229                           whatsnew story. */
230                        g_snprintf( s, sizeof( s ), "%d", BITLBEE_VERSION_CODE );
231                        set_setstr( &irc->b->set, "last_version", s );
232                        break;
233
234                default:
235                        irc_usermsg( irc, "Error registering" );
236                        break;
237        }
238}
239
240static void cmd_drop( irc_t *irc, char **cmd )
241{
242        storage_status_t status;
243       
244        status = storage_remove (irc->user->nick, cmd[1]);
245        switch (status) {
246        case STORAGE_NO_SUCH_USER:
247                irc_usermsg( irc, "That account does not exist" );
248                break;
249        case STORAGE_INVALID_PASSWORD:
250                irc_usermsg( irc, "Password invalid" );
251                break;
252        case STORAGE_OK:
253                irc_setpass( irc, NULL );
254                irc->status &= ~USTATUS_IDENTIFIED;
255                irc_umode_set( irc, "-R", 1 );
256                irc_usermsg( irc, "Account `%s' removed", irc->user->nick );
257                break;
258        default:
259                irc_usermsg( irc, "Error: `%d'", status );
260                break;
261        }
262}
263
264static void cmd_save( irc_t *irc, char **cmd )
265{
266        if( ( irc->status & USTATUS_IDENTIFIED ) == 0 )
267                irc_usermsg( irc, "Please create an account first" );
268        else if( storage_save( irc, NULL, TRUE ) == STORAGE_OK )
269                irc_usermsg( irc, "Configuration saved" );
270        else
271                irc_usermsg( irc, "Configuration could not be saved!" );
272}
273
274static void cmd_showset( irc_t *irc, set_t **head, char *key )
275{
276        set_t *set;
277        char *val;
278       
279        if( ( val = set_getstr( head, key ) ) )
280                irc_usermsg( irc, "%s = `%s'", key, val );
281        else if( !( set = set_find( head, key ) ) )
282        {
283                irc_usermsg( irc, "Setting `%s' does not exist.", key );
284                if( *head == irc->b->set )
285                        irc_usermsg( irc, "It might be an account or channel setting. "
286                                     "See \x02help account set\x02 and \x02help channel set\x02." );
287        }
288        else if( set->flags & SET_PASSWORD )
289                irc_usermsg( irc, "%s = `********' (hidden)", key );
290        else
291                irc_usermsg( irc, "%s is empty", key );
292}
293
294typedef set_t** (*cmd_set_findhead)( irc_t*, char* );
295typedef int (*cmd_set_checkflags)( irc_t*, set_t *set );
296
297static int cmd_set_real( irc_t *irc, char **cmd, set_t **head, cmd_set_checkflags checkflags )
298{
299        char *set_name = NULL, *value = NULL;
300        gboolean del = FALSE;
301       
302        if( cmd[1] && g_strncasecmp( cmd[1], "-del", 4 ) == 0 )
303        {
304                MIN_ARGS( 2, 0 );
305                set_name = cmd[2];
306                del = TRUE;
307        }
308        else
309        {
310                set_name = cmd[1];
311                value = cmd[2];
312        }
313       
314        if( set_name && ( value || del ) )
315        {
316                set_t *s = set_find( head, set_name );
317                int st;
318               
319                if( s && checkflags && checkflags( irc, s ) == 0 )
320                        return 0;
321               
322                if( del )
323                        st = set_reset( head, set_name );
324                else
325                        st = set_setstr( head, set_name, value );
326               
327                if( set_getstr( head, set_name ) == NULL &&
328                    set_find( head, set_name ) )
329                {
330                        /* This happens when changing the passwd, for example.
331                           Showing these msgs instead gives slightly clearer
332                           feedback. */
333                        if( st )
334                                irc_usermsg( irc, "Setting changed successfully" );
335                        else
336                                irc_usermsg( irc, "Failed to change setting" );
337                }
338                else
339                {
340                        cmd_showset( irc, head, set_name );
341                }
342        }
343        else if( set_name )
344        {
345                cmd_showset( irc, head, set_name );
346        }
347        else
348        {
349                set_t *s = *head;
350                while( s )
351                {
352                        if( !( s->flags & SET_HIDDEN ) )
353                                cmd_showset( irc, &s, s->key );
354                        s = s->next;
355                }
356        }
357       
358        return 1;
359}
360
361static int cmd_account_set_checkflags( irc_t *irc, set_t *s )
362{
363        account_t *a = s->data;
364       
365        if( a->ic && s && s->flags & ACC_SET_OFFLINE_ONLY )
366        {
367                irc_usermsg( irc, "This setting can only be changed when the account is %s-line", "off" );
368                return 0;
369        }
370        else if( !a->ic && s && s->flags & ACC_SET_ONLINE_ONLY )
371        {
372                irc_usermsg( irc, "This setting can only be changed when the account is %s-line", "on" );
373                return 0;
374        }
375       
376        return 1;
377}
378
379static void cmd_account( irc_t *irc, char **cmd )
380{
381        account_t *a;
382        int len;
383       
384        if( global.conf->authmode == AUTHMODE_REGISTERED && !( irc->status & USTATUS_IDENTIFIED ) )
385        {
386                irc_usermsg( irc, "This server only accepts registered users" );
387                return;
388        }
389       
390        len = strlen( cmd[1] );
391       
392        if( len >= 1 && g_strncasecmp( cmd[1], "add", len ) == 0 )
393        {
394                struct prpl *prpl;
395               
396                MIN_ARGS( 3 );
397               
398                if( cmd[4] == NULL )
399                        for( a = irc->b->accounts; a; a = a->next )
400                                if( strcmp( a->pass, PASSWORD_PENDING ) == 0 )
401                                {
402                                        irc_usermsg( irc, "Enter password for account %s(%s) "
403                                                     "first (use /OPER)", a->prpl->name, a->user );
404                                        return;
405                                }
406               
407                prpl = find_protocol( cmd[2] );
408               
409                if( prpl == NULL )
410                {
411                        irc_usermsg( irc, "Unknown protocol" );
412                        return;
413                }
414               
415                for( a = irc->b->accounts; a; a = a->next )
416                        if( a->prpl == prpl && prpl->handle_cmp( a->user, cmd[3] ) == 0 )
417                                irc_usermsg( irc, "Warning: You already have an account with "
418                                             "protocol `%s' and username `%s'. Are you accidentally "
419                                             "trying to add it twice?", prpl->name, cmd[3] );
420               
421                a = account_add( irc->b, prpl, cmd[3], cmd[4] ? cmd[4] : PASSWORD_PENDING );
422                if( cmd[5] )
423                {
424                        irc_usermsg( irc, "Warning: Passing a servername/other flags to `account add' "
425                                          "is now deprecated. Use `account set' instead." );
426                        set_setstr( &a->set, "server", cmd[5] );
427                }
428               
429                irc_usermsg( irc, "Account successfully added%s", cmd[4] ? "" :
430                             ", now use /OPER to enter the password" );
431               
432                return;
433        }
434        else if( len >= 1 && g_strncasecmp( cmd[1], "list", len ) == 0 )
435        {
436                int i = 0;
437               
438                if( strchr( irc->umode, 'b' ) )
439                        irc_usermsg( irc, "Account list:" );
440               
441                for( a = irc->b->accounts; a; a = a->next )
442                {
443                        char *con;
444                       
445                        if( a->ic && ( a->ic->flags & OPT_LOGGED_IN ) )
446                                con = " (connected)";
447                        else if( a->ic )
448                                con = " (connecting)";
449                        else if( a->reconnect )
450                                con = " (awaiting reconnect)";
451                        else
452                                con = "";
453                       
454                        irc_usermsg( irc, "%2d (%s): %s, %s%s", i, a->tag, a->prpl->name, a->user, con );
455                       
456                        i ++;
457                }
458                irc_usermsg( irc, "End of account list" );
459               
460                return;
461        }
462        else if( cmd[2] )
463        {
464                /* Try the following two only if cmd[2] == NULL */
465        }
466        else if( len >= 2 && g_strncasecmp( cmd[1], "on", len ) == 0 )
467        {
468                if ( irc->b->accounts )
469                {
470                        irc_usermsg( irc, "Trying to get all accounts connected..." );
471               
472                        for( a = irc->b->accounts; a; a = a->next )
473                                if( !a->ic && a->auto_connect )
474                                {
475                                        if( strcmp( a->pass, PASSWORD_PENDING ) == 0 )
476                                                irc_usermsg( irc, "Enter password for account %s(%s) "
477                                                             "first (use /OPER)", a->prpl->name, a->user );
478                                        else
479                                                account_on( irc->b, a );
480                                }
481                } 
482                else
483                {
484                        irc_usermsg( irc, "No accounts known. Use `account add' to add one." );
485                }
486               
487                return;
488        }
489        else if( len >= 2 && g_strncasecmp( cmd[1], "off", len ) == 0 )
490        {
491                irc_usermsg( irc, "Deactivating all active (re)connections..." );
492               
493                for( a = irc->b->accounts; a; a = a->next )
494                {
495                        if( a->ic )
496                                account_off( irc->b, a );
497                        else if( a->reconnect )
498                                cancel_auto_reconnect( a );
499                }
500               
501                return;
502        }
503       
504        MIN_ARGS( 2 );
505        len = strlen( cmd[2] );
506       
507        /* At least right now, don't accept on/off/set/del as account IDs even
508           if they're a proper match, since people not familiar with the new
509           syntax yet may get a confusing/nasty surprise. */
510        if( g_strcasecmp( cmd[1], "on" ) == 0 ||
511            g_strcasecmp( cmd[1], "off" ) == 0 ||
512            g_strcasecmp( cmd[1], "set" ) == 0 ||
513            g_strcasecmp( cmd[1], "del" ) == 0 ||
514            ( a = account_get( irc->b, cmd[1] ) ) == NULL )
515        {
516                irc_usermsg( irc, "Could not find account `%s'. Note that the syntax "
517                             "of the account command changed, see \x02help account\x02.", cmd[1] );
518               
519                return;
520        }
521       
522        if( len >= 1 && g_strncasecmp( cmd[2], "del", len ) == 0 )
523        {
524                if( a->ic )
525                {
526                        irc_usermsg( irc, "Account is still logged in, can't delete" );
527                }
528                else
529                {
530                        account_del( irc->b, a );
531                        irc_usermsg( irc, "Account deleted" );
532                }
533        }
534        else if( len >= 2 && g_strncasecmp( cmd[2], "on", len ) == 0 )
535        {
536                if( a->ic )
537                        irc_usermsg( irc, "Account already online" );
538                else if( strcmp( a->pass, PASSWORD_PENDING ) == 0 )
539                        irc_usermsg( irc, "Enter password for account %s(%s) "
540                                     "first (use /OPER)", a->prpl->name, a->user );
541                else
542                        account_on( irc->b, a );
543        }
544        else if( len >= 2 && g_strncasecmp( cmd[2], "off", len ) == 0 )
545        {
546                if( a->ic )
547                {
548                        account_off( irc->b, a );
549                }
550                else if( a->reconnect )
551                {
552                        cancel_auto_reconnect( a );
553                        irc_usermsg( irc, "Reconnect cancelled" );
554                }
555                else
556                {
557                        irc_usermsg( irc, "Account already offline" );
558                }
559        }
560        else if( len >= 1 && g_strncasecmp( cmd[2], "set", len ) == 0 )
561        {
562                cmd_set_real( irc, cmd + 2, &a->set, cmd_account_set_checkflags );
563        }
564        else
565        {
566                irc_usermsg( irc, "Unknown command: %s [...] %s. Please use \x02help commands\x02 to get a list of available commands.", "account", cmd[2] );
567        }
568}
569
570static void cmd_channel( irc_t *irc, char **cmd )
571{
572        irc_channel_t *ic;
573        int len;
574       
575        len = strlen( cmd[1] );
576       
577        if( len >= 1 && g_strncasecmp( cmd[1], "list", len ) == 0 )
578        {
579                GSList *l;
580                int i = 0;
581               
582                if( strchr( irc->umode, 'b' ) )
583                        irc_usermsg( irc, "Channel list:" );
584               
585                for( l = irc->channels; l; l = l->next )
586                {
587                        irc_channel_t *ic = l->data;
588                       
589                        irc_usermsg( irc, "%2d. %s, %s channel%s", i, ic->name,
590                                     set_getstr( &ic->set, "type" ),
591                                     ic->flags & IRC_CHANNEL_JOINED ? " (joined)" : "" );
592                       
593                        i ++;
594                }
595                irc_usermsg( irc, "End of channel list" );
596               
597                return;
598        }
599       
600        if( ( ic = irc_channel_get( irc, cmd[1] ) ) == NULL )
601        {
602                /* If this doesn't match any channel, maybe this is the short
603                   syntax (only works when used inside a channel). */
604                if( ( ic = irc->root->last_channel ) &&
605                    ( len = strlen( cmd[1] ) ) &&
606                    g_strncasecmp( cmd[1], "set", len ) == 0 )
607                        cmd_set_real( irc, cmd + 1, &ic->set, NULL );
608                else
609                        irc_usermsg( irc, "Could not find channel `%s'", cmd[1] );
610               
611                return;
612        }
613       
614        MIN_ARGS( 2 );
615        len = strlen( cmd[2] );
616       
617        if( len >= 1 && g_strncasecmp( cmd[2], "set", len ) == 0 )
618        {
619                cmd_set_real( irc, cmd + 2, &ic->set, NULL );
620        }
621        else if( len >= 1 && g_strncasecmp( cmd[2], "del", len ) == 0 )
622        {
623                if( !( ic->flags & IRC_CHANNEL_JOINED ) &&
624                    ic != ic->irc->default_channel )
625                {
626                        irc_usermsg( irc, "Channel %s deleted.", ic->name );
627                        irc_channel_free( ic );
628                }
629                else
630                        irc_usermsg( irc, "Couldn't remove channel (main channel %s or "
631                                          "channels you're still in cannot be deleted).",
632                                          irc->default_channel->name );
633        }
634        else
635        {
636                irc_usermsg( irc, "Unknown command: %s [...] %s. Please use \x02help commands\x02 to get a list of available commands.", "channel", cmd[1] );
637        }
638}
639
640static void cmd_add( irc_t *irc, char **cmd )
641{
642        account_t *a;
643        int add_on_server = 1;
644       
645        if( g_strcasecmp( cmd[1], "-tmp" ) == 0 )
646        {
647                MIN_ARGS( 3 );
648                add_on_server = 0;
649                cmd ++;
650        }
651       
652        if( !( a = account_get( irc->b, cmd[1] ) ) )
653        {
654                irc_usermsg( irc, "Invalid account" );
655                return;
656        }
657        else if( !( a->ic && ( a->ic->flags & OPT_LOGGED_IN ) ) )
658        {
659                irc_usermsg( irc, "That account is not on-line" );
660                return;
661        }
662       
663        if( cmd[3] )
664        {
665                if( !nick_ok( cmd[3] ) )
666                {
667                        irc_usermsg( irc, "The requested nick `%s' is invalid", cmd[3] );
668                        return;
669                }
670                else if( irc_user_by_name( irc, cmd[3] ) )
671                {
672                        irc_usermsg( irc, "The requested nick `%s' already exists", cmd[3] );
673                        return;
674                }
675                else
676                {
677                        nick_set_raw( a, cmd[2], cmd[3] );
678                }
679        }
680       
681        if( add_on_server )
682        {
683                irc_channel_t *ic;
684                char *s, *group = NULL;;
685               
686                if( ( ic = irc->root->last_channel ) &&
687                    ( s = set_getstr( &ic->set, "fill_by" ) ) &&
688                    strcmp( s, "group" ) == 0 &&
689                    ( group = set_getstr( &ic->set, "group" ) ) )
690                        irc_usermsg( irc, "Adding `%s' to contact list (group %s)",
691                                     cmd[2], group );
692                else
693                        irc_usermsg( irc, "Adding `%s' to contact list", cmd[2] );
694               
695                a->prpl->add_buddy( a->ic, cmd[2], group );
696        }
697        else
698        {
699                bee_user_t *bu;
700                irc_user_t *iu;
701               
702                /* Only for add -tmp. For regular adds, this callback will
703                   be called once the IM server confirms. */
704                if( ( bu = bee_user_new( irc->b, a->ic, cmd[2], BEE_USER_LOCAL ) ) &&
705                    ( iu = bu->ui_data ) )
706                        irc_usermsg( irc, "Temporarily assigned nickname `%s' "
707                                     "to contact `%s'", iu->nick, cmd[2] );
708        }
709       
710}
711
712static void cmd_remove( irc_t *irc, char **cmd )
713{
714        irc_user_t *iu;
715        bee_user_t *bu;
716        char *s;
717       
718        if( !( iu = irc_user_by_name( irc, cmd[1] ) ) || !( bu = iu->bu ) )
719        {
720                irc_usermsg( irc, "Buddy `%s' not found", cmd[1] );
721                return;
722        }
723        s = g_strdup( bu->handle );
724       
725        bu->ic->acc->prpl->remove_buddy( bu->ic, bu->handle, NULL );
726        nick_del( bu );
727        if( g_slist_find( irc->users, iu ) )
728                bee_user_free( irc->b, bu );
729       
730        irc_usermsg( irc, "Buddy `%s' (nick %s) removed from contact list", s, cmd[1] );
731        g_free( s );
732       
733        return;
734}
735
736static void cmd_info( irc_t *irc, char **cmd )
737{
738        struct im_connection *ic;
739        account_t *a;
740       
741        if( !cmd[2] )
742        {
743                irc_user_t *iu = irc_user_by_name( irc, cmd[1] );
744                if( !iu || !iu->bu )
745                {
746                        irc_usermsg( irc, "Nick `%s' does not exist", cmd[1] );
747                        return;
748                }
749                ic = iu->bu->ic;
750                cmd[2] = iu->bu->handle;
751        }
752        else if( !( a = account_get( irc->b, cmd[1] ) ) )
753        {
754                irc_usermsg( irc, "Invalid account" );
755                return;
756        }
757        else if( !( ( ic = a->ic ) && ( a->ic->flags & OPT_LOGGED_IN ) ) )
758        {
759                irc_usermsg( irc, "That account is not on-line" );
760                return;
761        }
762       
763        if( !ic->acc->prpl->get_info )
764        {
765                irc_usermsg( irc, "Command `%s' not supported by this protocol", cmd[0] );
766        }
767        else
768        {
769                ic->acc->prpl->get_info( ic, cmd[2] );
770        }
771}
772
773static void cmd_rename( irc_t *irc, char **cmd )
774{
775        irc_user_t *iu, *old;
776       
777        iu = irc_user_by_name( irc, cmd[1] );
778       
779        if( iu == NULL )
780        {
781                irc_usermsg( irc, "Nick `%s' does not exist", cmd[1] );
782        }
783        else if( iu == irc->user )
784        {
785                irc_usermsg( irc, "Use /nick to change your own nickname" );
786        }
787        else if( !nick_ok( cmd[2] ) )
788        {
789                irc_usermsg( irc, "Nick `%s' is invalid", cmd[2] );
790        }
791        else if( ( old = irc_user_by_name( irc, cmd[2] ) ) && old != iu )
792        {
793                irc_usermsg( irc, "Nick `%s' already exists", cmd[2] );
794        }
795        else
796        {
797                if( !irc_user_set_nick( iu, cmd[2] ) )
798                {
799                        irc_usermsg( irc, "Error while changing nick" );
800                        return;
801                }
802               
803                if( iu == irc->root )
804                {
805                        /* If we're called internally (user did "set root_nick"),
806                           let's not go O(INF). :-) */
807                        if( strcmp( cmd[0], "set_rename" ) != 0 )
808                                set_setstr( &irc->b->set, "root_nick", cmd[2] );
809                }
810                else if( iu->bu )
811                {
812                        nick_set( iu->bu, cmd[2] );
813                }
814               
815                irc_usermsg( irc, "Nick successfully changed" );
816        }
817}
818
819char *set_eval_root_nick( set_t *set, char *new_nick )
820{
821        irc_t *irc = set->data;
822       
823        if( strcmp( irc->root->nick, new_nick ) != 0 )
824        {
825                char *cmd[] = { "set_rename", irc->root->nick, new_nick, NULL };
826               
827                cmd_rename( irc, cmd );
828        }
829       
830        return strcmp( irc->root->nick, new_nick ) == 0 ? new_nick : SET_INVALID;
831}
832
833static void cmd_block( irc_t *irc, char **cmd )
834{
835        struct im_connection *ic;
836        account_t *a;
837       
838        if( !cmd[2] && ( a = account_get( irc->b, cmd[1] ) ) && a->ic )
839        {
840                char *format;
841                GSList *l;
842               
843                if( strchr( irc->umode, 'b' ) != NULL )
844                        format = "%s\t%s";
845                else
846                        format = "%-32.32s  %-16.16s";
847               
848                irc_usermsg( irc, format, "Handle", "Nickname" );
849                for( l = a->ic->deny; l; l = l->next )
850                {
851                        bee_user_t *bu = bee_user_by_handle( irc->b, a->ic, l->data );
852                        irc_user_t *iu = bu ? bu->ui_data : NULL;
853                        irc_usermsg( irc, format, l->data, iu ? iu->nick : "(none)" );
854                }
855                irc_usermsg( irc, "End of list." );
856               
857                return;
858        }
859        else if( !cmd[2] )
860        {
861                irc_user_t *iu = irc_user_by_name( irc, cmd[1] );
862                if( !iu || !iu->bu )
863                {
864                        irc_usermsg( irc, "Nick `%s' does not exist", cmd[1] );
865                        return;
866                }
867                ic = iu->bu->ic;
868                cmd[2] = iu->bu->handle;
869        }
870        else if( !( a = account_get( irc->b, cmd[1] ) ) )
871        {
872                irc_usermsg( irc, "Invalid account" );
873                return;
874        }
875        else if( !( ( ic = a->ic ) && ( a->ic->flags & OPT_LOGGED_IN ) ) )
876        {
877                irc_usermsg( irc, "That account is not on-line" );
878                return;
879        }
880       
881        if( !ic->acc->prpl->add_deny || !ic->acc->prpl->rem_permit )
882        {
883                irc_usermsg( irc, "Command `%s' not supported by this protocol", cmd[0] );
884        }
885        else
886        {
887                imc_rem_allow( ic, cmd[2] );
888                imc_add_block( ic, cmd[2] );
889                irc_usermsg( irc, "Buddy `%s' moved from allow- to block-list", cmd[2] );
890        }
891}
892
893static void cmd_allow( irc_t *irc, char **cmd )
894{
895        struct im_connection *ic;
896        account_t *a;
897       
898        if( !cmd[2] && ( a = account_get( irc->b, cmd[1] ) ) && a->ic )
899        {
900                char *format;
901                GSList *l;
902               
903                if( strchr( irc->umode, 'b' ) != NULL )
904                        format = "%s\t%s";
905                else
906                        format = "%-32.32s  %-16.16s";
907               
908                irc_usermsg( irc, format, "Handle", "Nickname" );
909                for( l = a->ic->permit; l; l = l->next )
910                {
911                        bee_user_t *bu = bee_user_by_handle( irc->b, a->ic, l->data );
912                        irc_user_t *iu = bu ? bu->ui_data : NULL;
913                        irc_usermsg( irc, format, l->data, iu ? iu->nick : "(none)" );
914                }
915                irc_usermsg( irc, "End of list." );
916               
917                return;
918        }
919        else if( !cmd[2] )
920        {
921                irc_user_t *iu = irc_user_by_name( irc, cmd[1] );
922                if( !iu || !iu->bu )
923                {
924                        irc_usermsg( irc, "Nick `%s' does not exist", cmd[1] );
925                        return;
926                }
927                ic = iu->bu->ic;
928                cmd[2] = iu->bu->handle;
929        }
930        else if( !( a = account_get( irc->b, cmd[1] ) ) )
931        {
932                irc_usermsg( irc, "Invalid account" );
933                return;
934        }
935        else if( !( ( ic = a->ic ) && ( a->ic->flags & OPT_LOGGED_IN ) ) )
936        {
937                irc_usermsg( irc, "That account is not on-line" );
938                return;
939        }
940       
941        if( !ic->acc->prpl->rem_deny || !ic->acc->prpl->add_permit )
942        {
943                irc_usermsg( irc, "Command `%s' not supported by this protocol", cmd[0] );
944        }
945        else
946        {
947                imc_rem_block( ic, cmd[2] );
948                imc_add_allow( ic, cmd[2] );
949               
950                irc_usermsg( irc, "Buddy `%s' moved from block- to allow-list", cmd[2] );
951        }
952}
953
954static void cmd_yesno( irc_t *irc, char **cmd )
955{
956        query_t *q = NULL;
957        int numq = 0;
958       
959        if( irc->queries == NULL )
960        {
961                /* Alright, alright, let's add a tiny easter egg here. */
962                static irc_t *last_irc = NULL;
963                static time_t last_time = 0;
964                static int times = 0;
965                static const char *msg[] = {
966                        "Oh yeah, that's right.",
967                        "Alright, alright. Now go back to work.",
968                        "Buuuuuuuuuuuuuuuurp... Excuse me!",
969                        "Yes?",
970                        "No?",
971                };
972               
973                if( last_irc == irc && time( NULL ) - last_time < 15 )
974                {
975                        if( ( ++times >= 3 ) )
976                        {
977                                irc_usermsg( irc, "%s", msg[rand()%(sizeof(msg)/sizeof(char*))] );
978                                last_irc = NULL;
979                                times = 0;
980                                return;
981                        }
982                }
983                else
984                {
985                        last_time = time( NULL );
986                        last_irc = irc;
987                        times = 0;
988                }
989               
990                irc_usermsg( irc, "Did I ask you something?" );
991                return;
992        }
993       
994        /* If there's an argument, the user seems to want to answer another question than the
995           first/last (depending on the query_order setting) one. */
996        if( cmd[1] )
997        {
998                if( sscanf( cmd[1], "%d", &numq ) != 1 )
999                {
1000                        irc_usermsg( irc, "Invalid query number" );
1001                        return;
1002                }
1003               
1004                for( q = irc->queries; q; q = q->next, numq -- )
1005                        if( numq == 0 )
1006                                break;
1007               
1008                if( !q )
1009                {
1010                        irc_usermsg( irc, "Uhm, I never asked you something like that..." );
1011                        return;
1012                }
1013        }
1014       
1015        if( g_strcasecmp( cmd[0], "yes" ) == 0 )
1016                query_answer( irc, q, 1 );
1017        else if( g_strcasecmp( cmd[0], "no" ) == 0 )
1018                query_answer( irc, q, 0 );
1019}
1020
1021static void cmd_set( irc_t *irc, char **cmd )
1022{
1023        cmd_set_real( irc, cmd, &irc->b->set, NULL );
1024}
1025
1026static void cmd_blist( irc_t *irc, char **cmd )
1027{
1028        int online = 0, away = 0, offline = 0;
1029        GSList *l;
1030        char s[256];
1031        char *format;
1032        int n_online = 0, n_away = 0, n_offline = 0;
1033       
1034        if( cmd[1] && g_strcasecmp( cmd[1], "all" ) == 0 )
1035                online = offline = away = 1;
1036        else if( cmd[1] && g_strcasecmp( cmd[1], "offline" ) == 0 )
1037                offline = 1;
1038        else if( cmd[1] && g_strcasecmp( cmd[1], "away" ) == 0 )
1039                away = 1;
1040        else if( cmd[1] && g_strcasecmp( cmd[1], "online" ) == 0 )
1041                online = 1;
1042        else
1043                online = away = 1;
1044       
1045        if( strchr( irc->umode, 'b' ) != NULL )
1046                format = "%s\t%s\t%s";
1047        else
1048                format = "%-16.16s  %-40.40s  %s";
1049       
1050        irc_usermsg( irc, format, "Nick", "Handle/Account", "Status" );
1051       
1052        if( irc->root->last_channel &&
1053            strcmp( set_getstr( &irc->root->last_channel->set, "type" ), "control" ) != 0 )
1054                irc->root->last_channel = NULL;
1055       
1056        for( l = irc->users; l; l = l->next )
1057        {
1058                irc_user_t *iu = l->data;
1059                bee_user_t *bu = iu->bu;
1060               
1061                if( !bu || ( irc->root->last_channel && !irc_channel_wants_user( irc->root->last_channel, iu ) ) ||
1062                    ( bu->flags & ( BEE_USER_ONLINE | BEE_USER_AWAY ) ) != BEE_USER_ONLINE )
1063                        continue;
1064               
1065                if( online == 1 )
1066                {
1067                        char st[256] = "Online";
1068                       
1069                        if( bu->status_msg )
1070                                g_snprintf( st, sizeof( st ) - 1, "Online (%s)", bu->status_msg );
1071                       
1072                        g_snprintf( s, sizeof( s ) - 1, "%s %s(%s)", bu->handle, bu->ic->acc->prpl->name, bu->ic->acc->user );
1073                        irc_usermsg( irc, format, iu->nick, s, st );
1074                }
1075               
1076                n_online ++;
1077        }
1078
1079        for( l = irc->users; l; l = l->next )
1080        {
1081                irc_user_t *iu = l->data;
1082                bee_user_t *bu = iu->bu;
1083               
1084                if( !bu || ( irc->root->last_channel && !irc_channel_wants_user( irc->root->last_channel, iu ) ) ||
1085                    !( bu->flags & BEE_USER_ONLINE ) || !( bu->flags & BEE_USER_AWAY ) )
1086                        continue;
1087               
1088                if( away == 1 )
1089                {
1090                        g_snprintf( s, sizeof( s ) - 1, "%s %s(%s)", bu->handle, bu->ic->acc->prpl->name, bu->ic->acc->user );
1091                        irc_usermsg( irc, format, iu->nick, s, irc_user_get_away( iu ) );
1092                }
1093                n_away ++;
1094        }
1095       
1096        for( l = irc->users; l; l = l->next )
1097        {
1098                irc_user_t *iu = l->data;
1099                bee_user_t *bu = iu->bu;
1100               
1101                if( !bu || ( irc->root->last_channel && !irc_channel_wants_user( irc->root->last_channel, iu ) ) ||
1102                    bu->flags & BEE_USER_ONLINE )
1103                        continue;
1104               
1105                if( offline == 1 )
1106                {
1107                        g_snprintf( s, sizeof( s ) - 1, "%s %s(%s)", bu->handle, bu->ic->acc->prpl->name, bu->ic->acc->user );
1108                        irc_usermsg( irc, format, iu->nick, s, "Offline" );
1109                }
1110                n_offline ++;
1111        }
1112       
1113        irc_usermsg( irc, "%d buddies (%d available, %d away, %d offline)", n_online + n_away + n_offline, n_online, n_away, n_offline );
1114}
1115
1116static void cmd_qlist( irc_t *irc, char **cmd )
1117{
1118        query_t *q = irc->queries;
1119        int num;
1120       
1121        if( !q )
1122        {
1123                irc_usermsg( irc, "There are no pending questions." );
1124                return;
1125        }
1126       
1127        irc_usermsg( irc, "Pending queries:" );
1128       
1129        for( num = 0; q; q = q->next, num ++ )
1130                if( q->ic ) /* Not necessary yet, but it might come later */
1131                        irc_usermsg( irc, "%d, %s(%s): %s", num, q->ic->acc->prpl->name, q->ic->acc->user, q->question );
1132                else
1133                        irc_usermsg( irc, "%d, BitlBee: %s", num, q->question );
1134}
1135
1136static void cmd_chat( irc_t *irc, char **cmd )
1137{
1138        account_t *acc;
1139       
1140        if( g_strcasecmp( cmd[1], "add" ) == 0 )
1141        {
1142                char *channel, *s;
1143                struct irc_channel *ic;
1144               
1145                MIN_ARGS( 3 );
1146               
1147                if( !( acc = account_get( irc->b, cmd[2] ) ) )
1148                {
1149                        irc_usermsg( irc, "Invalid account" );
1150                        return;
1151                }
1152                else if( !acc->prpl->chat_join )
1153                {
1154                        irc_usermsg( irc, "Named chatrooms not supported on that account." );
1155                        return;
1156                }
1157               
1158                if( cmd[4] == NULL )
1159                {
1160                        channel = g_strdup( cmd[3] );
1161                        if( ( s = strchr( channel, '@' ) ) )
1162                                *s = 0;
1163                }
1164                else
1165                {
1166                        channel = g_strdup( cmd[4] );
1167                }
1168               
1169                if( strchr( CTYPES, channel[0] ) == NULL )
1170                {
1171                        s = g_strdup_printf( "#%s", channel );
1172                        g_free( channel );
1173                        channel = s;
1174                       
1175                        irc_channel_name_strip( channel );
1176                }
1177               
1178                if( ( ic = irc_channel_new( irc, channel ) ) &&
1179                    set_setstr( &ic->set, "type", "chat" ) &&
1180                    set_setstr( &ic->set, "chat_type", "room" ) &&
1181                    set_setstr( &ic->set, "account", cmd[2] ) &&
1182                    set_setstr( &ic->set, "room", cmd[3] ) )
1183                {
1184                        irc_usermsg( irc, "Chatroom successfully added." );
1185                }
1186                else
1187                {
1188                        if( ic )
1189                                irc_channel_free( ic );
1190                       
1191                        irc_usermsg( irc, "Could not add chatroom." );
1192                }
1193                g_free( channel );
1194        }
1195        else if( g_strcasecmp( cmd[1], "with" ) == 0 )
1196        {
1197                irc_user_t *iu;
1198               
1199                MIN_ARGS( 2 );
1200               
1201                if( ( iu = irc_user_by_name( irc, cmd[2] ) ) &&
1202                    iu->bu && iu->bu->ic->acc->prpl->chat_with )
1203                {
1204                        if( !iu->bu->ic->acc->prpl->chat_with( iu->bu->ic, iu->bu->handle ) )
1205                        {
1206                                irc_usermsg( irc, "(Possible) failure while trying to open "
1207                                                  "a groupchat with %s.", iu->nick );
1208                        }
1209                }
1210                else
1211                {
1212                        irc_usermsg( irc, "Can't open a groupchat with %s.", cmd[2] );
1213                }
1214        }
1215        else if( g_strcasecmp( cmd[1], "list" ) == 0 ||
1216                 g_strcasecmp( cmd[1], "set" ) == 0 ||
1217                 g_strcasecmp( cmd[1], "del" ) == 0 )
1218        {
1219                irc_usermsg( irc, "Warning: The \002chat\002 command was mostly replaced with the \002channel\002 command." );
1220                cmd_channel( irc, cmd );
1221        }
1222        else
1223        {
1224                irc_usermsg( irc, "Unknown command: %s %s. Please use \x02help commands\x02 to get a list of available commands.", "chat", cmd[1] );
1225        }
1226}
1227
1228static void cmd_group( irc_t *irc, char **cmd )
1229{
1230        GSList *l;
1231        int len;
1232       
1233        len = strlen( cmd[1] );
1234        if( g_strncasecmp( cmd[1], "list", len ) == 0 )
1235        {
1236                int n = 0;
1237               
1238                if( strchr( irc->umode, 'b' ) )
1239                        irc_usermsg( irc, "Group list:" );
1240               
1241                for( l = irc->b->groups; l; l = l->next )
1242                {
1243                        bee_group_t *bg = l->data;
1244                        irc_usermsg( irc, "%d. %s", n ++, bg->name );
1245                }
1246                irc_usermsg( irc, "End of group list" );
1247        }
1248        else
1249        {
1250                irc_usermsg( irc, "Unknown command: %s %s. Please use \x02help commands\x02 to get a list of available commands.", "group", cmd[1] );
1251        }
1252}
1253
1254static void cmd_transfer( irc_t *irc, char **cmd )
1255{
1256        GSList *files = irc->file_transfers;
1257        enum { LIST, REJECT, CANCEL };
1258        int subcmd = LIST;
1259        int fid;
1260
1261        if( !files )
1262        {
1263                irc_usermsg( irc, "No pending transfers" );
1264                return;
1265        }
1266
1267        if( cmd[1] && ( strcmp( cmd[1], "reject" ) == 0 ) )
1268        {
1269                subcmd = REJECT;
1270        }
1271        else if( cmd[1] && ( strcmp( cmd[1], "cancel" ) == 0 ) && 
1272                 cmd[2] && ( sscanf( cmd[2], "%d", &fid ) == 1 ) )
1273        {
1274                subcmd = CANCEL;
1275        }
1276
1277        for( ; files; files = g_slist_next( files ) )
1278        {
1279                file_transfer_t *file = files->data;
1280               
1281                switch( subcmd ) {
1282                case LIST:
1283                        if ( file->status == FT_STATUS_LISTENING )
1284                                irc_usermsg( irc, 
1285                                        "Pending file(id %d): %s (Listening...)", file->local_id, file->file_name);
1286                        else 
1287                        {
1288                                int kb_per_s = 0;
1289                                time_t diff = time( NULL ) - file->started ? : 1;
1290                                if ( ( file->started > 0 ) && ( file->bytes_transferred > 0 ) )
1291                                        kb_per_s = file->bytes_transferred / 1024 / diff;
1292                                       
1293                                irc_usermsg( irc, 
1294                                        "Pending file(id %d): %s (%10zd/%zd kb, %d kb/s)", file->local_id, file->file_name, 
1295                                        file->bytes_transferred/1024, file->file_size/1024, kb_per_s);
1296                        }
1297                        break;
1298                case REJECT:
1299                        if( file->status == FT_STATUS_LISTENING )
1300                        {
1301                                irc_usermsg( irc, "Rejecting file transfer for %s", file->file_name );
1302                                imcb_file_canceled( file->ic, file, "Denied by user" );
1303                        }
1304                        break;
1305                case CANCEL:
1306                        if( file->local_id == fid )
1307                        {
1308                                irc_usermsg( irc, "Canceling file transfer for %s", file->file_name );
1309                                imcb_file_canceled( file->ic, file, "Canceled by user" );
1310                        }
1311                        break;
1312                }
1313        }
1314}
1315
1316static void cmd_nick( irc_t *irc, char **cmd )
1317{
1318        irc_usermsg( irc, "This command is deprecated. Try: account %s set display_name", cmd[1] );
1319}
1320
1321/* Maybe this should be a stand-alone command as well? */
1322static void bitlbee_whatsnew( irc_t *irc )
1323{
1324        int last = set_getint( &irc->b->set, "last_version" );
1325        char s[16], *msg;
1326       
1327        if( last >= BITLBEE_VERSION_CODE )
1328                return;
1329       
1330        msg = help_get_whatsnew( &(global.help), last );
1331       
1332        if( msg )
1333                irc_usermsg( irc, "%s: This seems to be your first time using this "
1334                                  "this version of BitlBee. Here's a list of new "
1335                                  "features you may like to know about:\n\n%s\n",
1336                                  irc->user->nick, msg );
1337       
1338        g_free( msg );
1339       
1340        g_snprintf( s, sizeof( s ), "%d", BITLBEE_VERSION_CODE );
1341        set_setstr( &irc->b->set, "last_version", s );
1342}
1343
1344/* IMPORTANT: Keep this list sorted! The short command logic needs that. */
1345command_t root_commands[] = {
1346        { "account",        1, cmd_account,        0 },
1347        { "add",            2, cmd_add,            0 },
1348        { "allow",          1, cmd_allow,          0 },
1349        { "blist",          0, cmd_blist,          0 },
1350        { "block",          1, cmd_block,          0 },
1351        { "channel",        1, cmd_channel,        0 },
1352        { "chat",           1, cmd_chat,           0 },
1353        { "drop",           1, cmd_drop,           0 },
1354        { "ft",             0, cmd_transfer,       0 },
1355        { "group",          1, cmd_group,          0 },
1356        { "help",           0, cmd_help,           0 }, 
1357        { "identify",       1, cmd_identify,       0 },
1358        { "info",           1, cmd_info,           0 },
1359        { "nick",           1, cmd_nick,           0 },
1360        { "no",             0, cmd_yesno,          0 },
1361        { "qlist",          0, cmd_qlist,          0 },
1362        { "register",       1, cmd_register,       0 },
1363        { "remove",         1, cmd_remove,         0 },
1364        { "rename",         2, cmd_rename,         0 },
1365        { "save",           0, cmd_save,           0 },
1366        { "set",            0, cmd_set,            0 },
1367        { "transfer",       0, cmd_transfer,       0 },
1368        { "yes",            0, cmd_yesno,          0 },
1369        /* Not expecting too many plugins adding root commands so just make a
1370           dumb array with some empty entried at the end. */
1371        { NULL },
1372        { NULL },
1373        { NULL },
1374        { NULL },
1375        { NULL },
1376        { NULL },
1377        { NULL },
1378        { NULL },
1379        { NULL },
1380};
1381static const int num_root_commands = sizeof( root_commands ) / sizeof( command_t );
1382
1383gboolean root_command_add( const char *command, int params, void (*func)(irc_t *, char **args), int flags )
1384{
1385        int i;
1386       
1387        if( root_commands[num_root_commands-2].command )
1388                /* Planning fail! List is full. */
1389                return FALSE;
1390       
1391        for( i = 0; root_commands[i].command; i++ )
1392        {
1393                if( g_strcasecmp( root_commands[i].command, command ) == 0 )
1394                        return FALSE;
1395                else if( g_strcasecmp( root_commands[i].command, command ) > 0 )
1396                        break;
1397        }
1398        memmove( root_commands + i + 1, root_commands + i,
1399                 sizeof( command_t ) * ( num_root_commands - i - 1 ) );
1400       
1401        root_commands[i].command = g_strdup( command );
1402        root_commands[i].required_parameters = params;
1403        root_commands[i].execute = func;
1404        root_commands[i].flags = flags;
1405       
1406        return TRUE;
1407}
Note: See TracBrowser for help on using the repository browser.