source: irc_commands.c @ 8240840

Last change on this file since 8240840 was ffa1173, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-04-11T19:53:27Z

Allow the user to change his/her nick. My old concern was that it gets hairy
when dealing with the storage/NickServ part. But meh, just reset all that
state when the nick changes and require the user to re-register if s/he
wants to save stuff.

The only problem's when s/he identifies and may end up getting every account
added twice.

  • Property mode set to 100644
File size: 17.1 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/* IRC 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 "bitlbee.h"
28#include "ipc.h"
29#include "chat.h"
30
31static void irc_cmd_pass( irc_t *irc, char **cmd )
32{
33        if( irc->status & USTATUS_LOGGED_IN )
34        {
35                char *send_cmd[] = { "identify", cmd[1], NULL };
36               
37                /* We're already logged in, this client seems to send the PASS
38                   command last. (Possibly it won't send it at all if it turns
39                   out we don't require it, which will break this feature.)
40                   Try to identify using the given password. */
41                return root_command( irc, send_cmd );
42        }
43        /* Handling in pre-logged-in state, first see if this server is
44           password-protected: */
45        else if( global.conf->auth_pass &&
46            ( strncmp( global.conf->auth_pass, "md5:", 4 ) == 0 ?
47                md5_verify_password( cmd[1], global.conf->auth_pass + 4 ) == 0 :
48                strcmp( cmd[1], global.conf->auth_pass ) == 0 ) )
49        {
50                irc->status |= USTATUS_AUTHORIZED;
51                irc_check_login( irc );
52        }
53        else if( global.conf->auth_pass )
54        {
55                irc_send_num( irc, 464, ":Incorrect password" );
56        }
57        else
58        {
59                /* Remember the password and try to identify after USER/NICK. */
60                /*irc_setpass( irc, cmd[1] ); */
61                irc_check_login( irc );
62        }
63}
64
65static void irc_cmd_user( irc_t *irc, char **cmd )
66{
67        irc->user->user = g_strdup( cmd[1] );
68        irc->user->fullname = g_strdup( cmd[4] );
69       
70        irc_check_login( irc );
71}
72
73static void irc_cmd_nick( irc_t *irc, char **cmd )
74{
75        if( irc_user_by_name( irc, cmd[1] ) )
76        {
77                irc_send_num( irc, 433, ":This nick is already in use" );
78        }
79        else if( !nick_ok( cmd[1] ) )
80        {
81                /* [SH] Invalid characters. */
82                irc_send_num( irc, 432, ":This nick contains invalid characters" );
83        }
84        else if( irc->user->nick )
85        {
86                if( irc->status & USTATUS_IDENTIFIED )
87                {
88                        irc_setpass( irc, NULL );
89                        irc->status &= ~USTATUS_IDENTIFIED;
90                        irc_umode_set( irc, "-R", 1 );
91                }
92               
93                irc_user_set_nick( irc->user, cmd[1] );
94        }
95        else
96        {
97                irc->user->nick = g_strdup( cmd[1] );
98               
99                irc_check_login( irc );
100        }
101}
102
103static void irc_cmd_quit( irc_t *irc, char **cmd )
104{
105        if( cmd[1] && *cmd[1] )
106                irc_abort( irc, 0, "Quit: %s", cmd[1] );
107        else
108                irc_abort( irc, 0, "Leaving..." );
109}
110
111static void irc_cmd_ping( irc_t *irc, char **cmd )
112{
113        irc_write( irc, ":%s PONG %s :%s", irc->root->host,
114                   irc->root->host, cmd[1]?cmd[1]:irc->root->host );
115}
116
117static void irc_cmd_pong( irc_t *irc, char **cmd )
118{
119        /* We could check the value we get back from the user, but in
120           fact we don't care, we're just happy s/he's still alive. */
121        irc->last_pong = gettime();
122        irc->pinging = 0;
123}
124
125static void irc_cmd_join( irc_t *irc, char **cmd )
126{
127        irc_channel_t *ic;
128       
129        if( ( ic = irc_channel_by_name( irc, cmd[1] ) ) == NULL )
130                ic = irc_channel_new( irc, cmd[1] );
131       
132        if( ic == NULL )
133                irc_send_num( irc, 479, "%s :Invalid channel name", cmd[1] );
134       
135        if( ic->flags & IRC_CHANNEL_JOINED )
136                return; /* Dude, you're already there...
137                           RFC doesn't have any reply for that though? */
138       
139        irc_channel_add_user( ic, irc->user );
140}
141
142static void irc_cmd_names( irc_t *irc, char **cmd )
143{
144        irc_channel_t *ic;
145       
146        if( cmd[1] && ( ic = irc_channel_by_name( irc, cmd[1] ) ) )
147                irc_send_names( ic );
148        /* With no args, we should show /names of all chans. Make the code
149           below work well if necessary.
150        else
151        {
152                GSList *l;
153               
154                for( l = irc->channels; l; l = l->next )
155                        irc_send_names( l->data );
156        }
157        */
158}
159
160static void irc_cmd_part( irc_t *irc, char **cmd )
161{
162        irc_channel_t *ic;
163       
164        if( ( ic = irc_channel_by_name( irc, cmd[1] ) ) == NULL )
165        {
166                irc_send_num( irc, 403, "%s :No such channel", cmd[1] );
167        }
168        else if( !irc_channel_del_user( ic, irc->user ) )
169        {
170                irc_send_num( irc, 442, "%s :You're not on that channel", cmd[1] );
171        }
172}
173
174static void irc_cmd_whois( irc_t *irc, char **cmd )
175{
176        char *nick = cmd[1];
177        irc_user_t *iu = irc_user_by_name( irc, nick );
178       
179        if( iu )
180                irc_send_whois( iu );
181        else
182                irc_send_num( irc, 401, "%s :Nick does not exist", nick );
183}
184
185static void irc_cmd_whowas( irc_t *irc, char **cmd )
186{
187        /* For some reason irssi tries a whowas when whois fails. We can
188           ignore this, but then the user never gets a "user not found"
189           message from irssi which is a bit annoying. So just respond
190           with not-found and irssi users will get better error messages */
191       
192        irc_send_num( irc, 406, "%s :Nick does not exist", cmd[1] );
193        irc_send_num( irc, 369, "%s :End of WHOWAS", cmd[1] );
194}
195
196static void irc_cmd_motd( irc_t *irc, char **cmd )
197{
198        irc_send_motd( irc );
199}
200
201static void irc_cmd_mode( irc_t *irc, char **cmd )
202{
203        if( irc_channel_name_ok( cmd[1] ) )
204        {
205                irc_channel_t *ic;
206               
207                if( ( ic = irc_channel_by_name( irc, cmd[1] ) ) == NULL )
208                        irc_send_num( irc, 403, "%s :No such channel", cmd[1] );
209                else if( cmd[2] )
210                {
211                        if( *cmd[2] == '+' || *cmd[2] == '-' )
212                                irc_send_num( irc, 477, "%s :Can't change channel modes", cmd[1] );
213                        else if( *cmd[2] == 'b' )
214                                irc_send_num( irc, 368, "%s :No bans possible", cmd[1] );
215                }
216                else
217                        irc_send_num( irc, 324, "%s +%s", cmd[1], ic->mode );
218        }
219        else
220        {
221                if( nick_cmp( cmd[1], irc->user->nick ) == 0 )
222                {
223                        if( cmd[2] )
224                                irc_umode_set( irc, cmd[2], 0 );
225                        else
226                                irc_send_num( irc, 221, "+%s", irc->umode );
227                }
228                else
229                        irc_send_num( irc, 502, ":Don't touch their modes" );
230        }
231}
232
233static void irc_cmd_who( irc_t *irc, char **cmd )
234{
235        char *channel = cmd[1];
236        irc_channel_t *ic;
237       
238        if( !channel || *channel == '0' || *channel == '*' || !*channel )
239                irc_send_who( irc, irc->users, "**" );
240        else if( ( ic = irc_channel_by_name( irc, channel ) ) )
241                irc_send_who( irc, ic->users, channel );
242        else
243                irc_send_num( irc, 403, "%s :No such channel", channel );
244}
245
246static void irc_cmd_privmsg( irc_t *irc, char **cmd )
247{
248        irc_channel_t *ic;
249        irc_user_t *iu;
250       
251        if( !cmd[2] ) 
252        {
253                irc_send_num( irc, 412, ":No text to send" );
254        }
255        else if( irc_channel_name_ok( cmd[1] ) &&
256                 ( ic = irc_channel_by_name( irc, cmd[1] ) ) )
257        {
258                if( ic->f->privmsg )
259                        ic->f->privmsg( ic, cmd[2] );
260        }
261        else if( ( iu = irc_user_by_name( irc, cmd[1] ) ) )
262        {
263                if( iu->f->privmsg )
264                        iu->f->privmsg( iu, cmd[2] );
265        }
266        else
267        {
268                irc_send_num( irc, 401, "%s :No such nick/channel", cmd[1] );
269        }
270
271
272#if 0
273        else if( irc->nick && g_strcasecmp( cmd[1], irc->nick ) == 0 )
274        {
275        }
276        else
277        {
278                if( g_strcasecmp( cmd[1], irc->channel ) == 0 )
279                {
280                        unsigned int i;
281                        char *t = set_getstr( &irc->set, "default_target" );
282                       
283                        if( g_strcasecmp( t, "last" ) == 0 && irc->last_target )
284                                cmd[1] = irc->last_target;
285                        else if( g_strcasecmp( t, "root" ) == 0 )
286                                cmd[1] = irc->mynick;
287                       
288                        for( i = 0; i < strlen( cmd[2] ); i ++ )
289                        {
290                                if( cmd[2][i] == ' ' ) break;
291                                if( cmd[2][i] == ':' || cmd[2][i] == ',' )
292                                {
293                                        cmd[1] = cmd[2];
294                                        cmd[2] += i;
295                                        *cmd[2] = 0;
296                                        while( *(++cmd[2]) == ' ' );
297                                        break;
298                                }
299                        }
300                       
301                        irc->is_private = 0;
302                       
303                        if( cmd[1] != irc->last_target )
304                        {
305                                g_free( irc->last_target );
306                                irc->last_target = g_strdup( cmd[1] );
307                        }
308                }
309                else
310                {
311                        irc->is_private = 1;
312                }
313                irc_send( irc, cmd[1], cmd[2], ( g_strcasecmp( cmd[0], "NOTICE" ) == 0 ) ? OPT_AWAY : 0 );
314        }
315#endif
316}
317
318static void irc_cmd_nickserv( irc_t *irc, char **cmd )
319{
320        /* [SH] This aliases the NickServ command to PRIVMSG root */
321        /* [TV] This aliases the NS command to PRIVMSG root as well */
322        root_command( irc, cmd + 1 );
323}
324
325
326
327#if 0
328//#if 0
329static void irc_cmd_oper( irc_t *irc, char **cmd )
330{
331        if( global.conf->oper_pass &&
332            ( strncmp( global.conf->oper_pass, "md5:", 4 ) == 0 ?
333                md5_verify_password( cmd[2], global.conf->oper_pass + 4 ) == 0 :
334                strcmp( cmd[2], global.conf->oper_pass ) == 0 ) )
335        {
336                irc_umode_set( irc, "+o", 1 );
337                irc_send_num( irc, 381, ":Password accepted" );
338        }
339        else
340        {
341                irc_send_num( irc, 432, ":Incorrect password" );
342        }
343}
344
345static void irc_cmd_invite( irc_t *irc, char **cmd )
346{
347        char *nick = cmd[1], *channel = cmd[2];
348        struct groupchat *c = irc_chat_by_channel( irc, channel );
349        user_t *u = user_find( irc, nick );
350       
351        if( u && c && ( u->ic == c->ic ) )
352                if( c->ic && c->ic->acc->prpl->chat_invite )
353                {
354                        c->ic->acc->prpl->chat_invite( c, u->handle, NULL );
355                        irc_send_num( irc, 341, "%s %s", nick, channel );
356                        return;
357                }
358       
359        irc_send_num( irc, 482, "%s :Invite impossible; User/Channel non-existent or incompatible", channel );
360}
361
362static void irc_cmd_userhost( irc_t *irc, char **cmd )
363{
364        user_t *u;
365        int i;
366       
367        /* [TV] Usable USERHOST-implementation according to
368                RFC1459. Without this, mIRC shows an error
369                while connecting, and the used way of rejecting
370                breaks standards.
371        */
372       
373        for( i = 1; cmd[i]; i ++ )
374                if( ( u = user_find( irc, cmd[i] ) ) )
375                {
376                        if( u->online && u->away )
377                                irc_send_num( irc, 302, ":%s=-%s@%s", u->nick, u->user, u->host );
378                        else
379                                irc_send_num( irc, 302, ":%s=+%s@%s", u->nick, u->user, u->host );
380                }
381}
382
383static void irc_cmd_ison( irc_t *irc, char **cmd )
384{
385        user_t *u;
386        char buff[IRC_MAX_LINE];
387        int lenleft, i;
388       
389        buff[0] = '\0';
390       
391        /* [SH] Leave room for : and \0 */
392        lenleft = IRC_MAX_LINE - 2;
393       
394        for( i = 1; cmd[i]; i ++ )
395        {
396                char *this, *next;
397               
398                this = cmd[i];
399                while( *this )
400                {
401                        if( ( next = strchr( this, ' ' ) ) )
402                                *next = 0;
403                       
404                        if( ( u = user_find( irc, this ) ) && u->online )
405                        {
406                                lenleft -= strlen( u->nick ) + 1;
407                               
408                                if( lenleft < 0 )
409                                        break;
410                               
411                                strcat( buff, u->nick );
412                                strcat( buff, " " );
413                        }
414                       
415                        if( next )
416                        {
417                                *next = ' ';
418                                this = next + 1;
419                        }
420                        else
421                        {
422                                break;
423                        }   
424                }
425               
426                /* *sigh* */
427                if( lenleft < 0 )
428                        break;
429        }
430       
431        if( strlen( buff ) > 0 )
432                buff[strlen(buff)-1] = '\0';
433       
434        irc_send_num( irc, 303, ":%s", buff );
435}
436
437static void irc_cmd_watch( irc_t *irc, char **cmd )
438{
439        int i;
440       
441        /* Obviously we could also mark a user structure as being
442           watched, but what if the WATCH command is sent right
443           after connecting? The user won't exist yet then... */
444        for( i = 1; cmd[i]; i ++ )
445        {
446                char *nick;
447                user_t *u;
448               
449                if( !cmd[i][0] || !cmd[i][1] )
450                        break;
451               
452                nick = g_strdup( cmd[i] + 1 );
453                nick_lc( nick );
454               
455                u = user_find( irc, nick );
456               
457                if( cmd[i][0] == '+' )
458                {
459                        if( !g_hash_table_lookup( irc->watches, nick ) )
460                                g_hash_table_insert( irc->watches, nick, nick );
461                       
462                        if( u && u->online )
463                                irc_send_num( irc, 604, "%s %s %s %d :%s", u->nick, u->user, u->host, (int) time( NULL ), "is online" );
464                        else
465                                irc_send_num( irc, 605, "%s %s %s %d :%s", nick, "*", "*", (int) time( NULL ), "is offline" );
466                }
467                else if( cmd[i][0] == '-' )
468                {
469                        gpointer okey, ovalue;
470                       
471                        if( g_hash_table_lookup_extended( irc->watches, nick, &okey, &ovalue ) )
472                        {
473                                g_hash_table_remove( irc->watches, okey );
474                                g_free( okey );
475                               
476                                irc_send_num( irc, 602, "%s %s %s %d :%s", nick, "*", "*", 0, "Stopped watching" );
477                        }
478                }
479        }
480}
481
482static void irc_cmd_topic( irc_t *irc, char **cmd )
483{
484        char *channel = cmd[1];
485        char *topic = cmd[2];
486       
487        if( topic )
488        {
489                /* Send the topic */
490                struct groupchat *c = irc_chat_by_channel( irc, channel );
491                if( c && c->ic && c->ic->acc->prpl->chat_topic )
492                        c->ic->acc->prpl->chat_topic( c, topic );
493        }
494        else
495        {
496                /* Get the topic */
497                irc_topic( irc, channel );
498        }
499}
500
501static void irc_cmd_away( irc_t *irc, char **cmd )
502{
503        user_t *u = user_find( irc, irc->nick );
504        char *away = cmd[1];
505       
506        if( !u ) return;
507       
508        if( away && *away )
509        {
510                int i, j;
511               
512                /* Copy away string, but skip control chars. Mainly because
513                   Jabber really doesn't like them. */
514                u->away = g_malloc( strlen( away ) + 1 );
515                for( i = j = 0; away[i]; i ++ )
516                        if( ( u->away[j] = away[i] ) >= ' ' )
517                                j ++;
518                u->away[j] = 0;
519               
520                irc_send_num( irc, 306, ":You're now away: %s", u->away );
521                /* irc_umode_set( irc, irc->myhost, "+a" ); */
522        }
523        else
524        {
525                if( u->away ) g_free( u->away );
526                u->away = NULL;
527                /* irc_umode_set( irc, irc->myhost, "-a" ); */
528                irc_send_num( irc, 305, ":Welcome back" );
529        }
530       
531        set_setstr( &irc->set, "away", u->away );
532}
533
534static void irc_cmd_version( irc_t *irc, char **cmd )
535{
536        irc_send_num( irc, 351, "bitlbee-%s. %s :%s/%s ", BITLBEE_VERSION, irc->myhost, ARCH, CPU );
537}
538
539static void irc_cmd_completions( irc_t *irc, char **cmd )
540{
541        user_t *u = user_find( irc, irc->mynick );
542        help_t *h;
543        set_t *s;
544        int i;
545       
546        irc_privmsg( irc, u, "NOTICE", irc->nick, "COMPLETIONS ", "OK" );
547       
548        for( i = 0; commands[i].command; i ++ )
549                irc_privmsg( irc, u, "NOTICE", irc->nick, "COMPLETIONS ", commands[i].command );
550       
551        for( h = global.help; h; h = h->next )
552                irc_privmsg( irc, u, "NOTICE", irc->nick, "COMPLETIONS help ", h->title );
553       
554        for( s = irc->set; s; s = s->next )
555                irc_privmsg( irc, u, "NOTICE", irc->nick, "COMPLETIONS set ", s->key );
556       
557        irc_privmsg( irc, u, "NOTICE", irc->nick, "COMPLETIONS ", "END" );
558}
559
560static void irc_cmd_rehash( irc_t *irc, char **cmd )
561{
562        if( global.conf->runmode == RUNMODE_INETD )
563                ipc_master_cmd_rehash( NULL, NULL );
564        else
565                ipc_to_master( cmd );
566       
567        irc_send_num( irc, 382, "%s :Rehashing", global.conf_file );
568}
569#endif
570
571static const command_t irc_commands[] = {
572        { "pass",        1, irc_cmd_pass,        0 },
573        { "user",        4, irc_cmd_user,        IRC_CMD_PRE_LOGIN },
574        { "nick",        1, irc_cmd_nick,        0 },
575        { "quit",        0, irc_cmd_quit,        0 },
576        { "ping",        0, irc_cmd_ping,        0 },
577        { "pong",        0, irc_cmd_pong,        IRC_CMD_LOGGED_IN },
578        { "join",        1, irc_cmd_join,        IRC_CMD_LOGGED_IN },
579        { "names",       1, irc_cmd_names,       IRC_CMD_LOGGED_IN },
580        { "part",        1, irc_cmd_part,        IRC_CMD_LOGGED_IN },
581        { "whois",       1, irc_cmd_whois,       IRC_CMD_LOGGED_IN },
582        { "whowas",      1, irc_cmd_whowas,      IRC_CMD_LOGGED_IN },
583        { "motd",        0, irc_cmd_motd,        IRC_CMD_LOGGED_IN },
584        { "mode",        1, irc_cmd_mode,        IRC_CMD_LOGGED_IN },
585        { "who",         0, irc_cmd_who,         IRC_CMD_LOGGED_IN },
586        { "privmsg",     1, irc_cmd_privmsg,     IRC_CMD_LOGGED_IN },
587        { "nickserv",    1, irc_cmd_nickserv,    IRC_CMD_LOGGED_IN },
588        { "ns",          1, irc_cmd_nickserv,    IRC_CMD_LOGGED_IN },
589#if 0
590        { "oper",        2, irc_cmd_oper,        IRC_CMD_LOGGED_IN },
591        { "invite",      2, irc_cmd_invite,      IRC_CMD_LOGGED_IN },
592        { "notice",      1, irc_cmd_privmsg,     IRC_CMD_LOGGED_IN },
593        { "userhost",    1, irc_cmd_userhost,    IRC_CMD_LOGGED_IN },
594        { "ison",        1, irc_cmd_ison,        IRC_CMD_LOGGED_IN },
595        { "watch",       1, irc_cmd_watch,       IRC_CMD_LOGGED_IN },
596        { "topic",       1, irc_cmd_topic,       IRC_CMD_LOGGED_IN },
597        { "away",        0, irc_cmd_away,        IRC_CMD_LOGGED_IN },
598        { "version",     0, irc_cmd_version,     IRC_CMD_LOGGED_IN },
599        { "completions", 0, irc_cmd_completions, IRC_CMD_LOGGED_IN },
600        { "die",         0, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
601        { "deaf",        0, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
602        { "wallops",     1, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
603        { "wall",        1, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
604        { "rehash",      0, irc_cmd_rehash,      IRC_CMD_OPER_ONLY },
605        { "restart",     0, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
606        { "kill",        2, NULL,                IRC_CMD_OPER_ONLY | IRC_CMD_TO_MASTER },
607#endif
608        { NULL }
609};
610
611void irc_exec( irc_t *irc, char *cmd[] )
612{       
613        int i, n_arg;
614       
615        if( !cmd[0] )
616                return;
617       
618        for( i = 0; irc_commands[i].command; i++ )
619                if( g_strcasecmp( irc_commands[i].command, cmd[0] ) == 0 )
620                {
621                        /* There should be no typo in the next line: */
622                        for( n_arg = 0; cmd[n_arg]; n_arg ++ ); n_arg --;
623                       
624                        if( irc_commands[i].flags & IRC_CMD_PRE_LOGIN && irc->status & USTATUS_LOGGED_IN )
625                        {
626                                irc_send_num( irc, 462, ":Only allowed before logging in" );
627                        }
628                        else if( irc_commands[i].flags & IRC_CMD_LOGGED_IN && !( irc->status & USTATUS_LOGGED_IN ) )
629                        {
630                                irc_send_num( irc, 451, ":Register first" );
631                        }
632                        else if( irc_commands[i].flags & IRC_CMD_OPER_ONLY && !strchr( irc->umode, 'o' ) )
633                        {
634                                irc_send_num( irc, 481, ":Permission denied - You're not an IRC operator" );
635                        }
636                        else if( n_arg < irc_commands[i].required_parameters )
637                        {
638                                irc_send_num( irc, 461, "%s :Need more parameters", cmd[0] );
639                        }
640                        else if( irc_commands[i].flags & IRC_CMD_TO_MASTER )
641                        {
642                                /* IPC doesn't make sense in inetd mode,
643                                    but the function will catch that. */
644                                ipc_to_master( cmd );
645                        }
646                        else
647                        {
648                                irc_commands[i].execute( irc, cmd );
649                        }
650                       
651                        return;
652                }
653       
654        if( irc->status >= USTATUS_LOGGED_IN )
655                irc_send_num( irc, 421, "%s :Unknown command", cmd[0] );
656}
Note: See TracBrowser for help on using the repository browser.