source: irc_channel.c @ 289bd2d

Last change on this file since 289bd2d was 65016a6, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-08-04T19:45:18Z

Set channel mode +C for control channels.

  • Property mode set to 100644
File size: 17.9 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/* The IRC-based UI - Representing (virtual) channels.                  */
8
9/*
10  This program is free software; you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation; either version 2 of the License, or
13  (at your option) any later version.
14
15  This program is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  GNU General Public License for more details.
19
20  You should have received a copy of the GNU General Public License with
21  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
22  if not, write to the Free Software Foundation, Inc., 59 Temple Place,
23  Suite 330, Boston, MA  02111-1307  USA
24*/
25
26#include "bitlbee.h"
27
28static char *set_eval_channel_type( set_t *set, char *value );
29static gint irc_channel_user_cmp( gconstpointer a_, gconstpointer b_ );
30static const struct irc_channel_funcs control_channel_funcs;
31
32extern const struct irc_channel_funcs irc_channel_im_chat_funcs;
33
34irc_channel_t *irc_channel_new( irc_t *irc, const char *name )
35{
36        irc_channel_t *ic;
37       
38        if( !irc_channel_name_ok( name ) || irc_channel_by_name( irc, name ) )
39                return NULL;
40       
41        ic = g_new0( irc_channel_t, 1 );
42        ic->irc = irc;
43        ic->name = g_strdup( name );
44        strcpy( ic->mode, CMODE );
45       
46        irc_channel_add_user( ic, irc->root );
47       
48        irc->channels = g_slist_append( irc->channels, ic );
49       
50        set_add( &ic->set, "auto_join", "false", set_eval_bool, ic );
51        set_add( &ic->set, "type", "control", set_eval_channel_type, ic );
52       
53        if( name[0] == '&' )
54                set_setstr( &ic->set, "type", "control" );
55        else /* if( name[0] == '#' ) */
56                set_setstr( &ic->set, "type", "chat" );
57       
58        return ic;
59}
60
61irc_channel_t *irc_channel_by_name( irc_t *irc, const char *name )
62{
63        GSList *l;
64       
65        for( l = irc->channels; l; l = l->next )
66        {
67                irc_channel_t *ic = l->data;
68               
69                if( irc_channel_name_cmp( name, ic->name ) == 0 )
70                        return ic;
71        }
72       
73        return NULL;
74}
75
76irc_channel_t *irc_channel_get( irc_t *irc, char *id )
77{
78        irc_channel_t *ic, *ret = NULL;
79        GSList *l;
80        int nr;
81       
82        if( sscanf( id, "%d", &nr ) == 1 && nr < 1000 )
83        {
84                for( l = irc->channels; l; l = l->next )
85                {
86                        ic = l->data;
87                        if( ( nr-- ) == 0 ) 
88                                return ic;
89                }
90               
91                return NULL;
92        }
93       
94        /* Exact match first: Partial match only sucks if there's a channel
95           #aa and #aabb */
96        if( ( ret = irc_channel_by_name( irc, id ) ) )
97                return ret;
98       
99        for( l = irc->channels; l; l = l->next )
100        {
101                ic = l->data;
102               
103                if( strstr( ic->name, id ) )
104                {
105                        /* Make sure it's a unique match. */
106                        if( !ret )
107                                ret = ic;
108                        else
109                                return NULL;
110                }
111        }
112       
113        return ret;
114}
115
116int irc_channel_free( irc_channel_t *ic )
117{
118        irc_t *irc = ic->irc;
119        GSList *l;
120       
121        if( ic->flags & IRC_CHANNEL_JOINED )
122                irc_channel_del_user( ic, irc->user, IRC_CDU_KICK, "Cleaning up channel" );
123       
124        if( ic->f->_free )
125                ic->f->_free( ic );
126       
127        while( ic->set )
128                set_del( &ic->set, ic->set->key );
129       
130        irc->channels = g_slist_remove( irc->channels, ic );
131        while( ic->users )
132        {
133                g_free( ic->users->data );
134                ic->users = g_slist_remove( ic->users, ic->users->data );
135        }
136       
137        for( l = irc->users; l; l = l->next )
138        {
139                irc_user_t *iu = l->data;
140               
141                if( iu->last_channel == ic )
142                        iu->last_channel = irc->default_channel;
143        }
144       
145        g_free( ic->name );
146        g_free( ic->topic );
147        g_free( ic->topic_who );
148        g_free( ic );
149       
150        return 1;
151}
152
153struct irc_channel_free_data
154{
155        irc_t *irc;
156        irc_channel_t *ic;
157        char *name;
158};
159
160static gboolean irc_channel_free_callback( gpointer data, gint fd, b_input_condition cond )
161{
162        struct irc_channel_free_data *d = data;
163       
164        if( g_slist_find( irc_connection_list, d->irc ) &&
165            irc_channel_by_name( d->irc, d->name ) == d->ic &&
166            !( d->ic->flags & IRC_CHANNEL_JOINED ) )
167                irc_channel_free( d->ic );
168
169        g_free( d->name );
170        g_free( d );
171        return FALSE;
172}
173
174/* Free the channel, but via the event loop, so after finishing whatever event
175   we're currently handling. */
176void irc_channel_free_soon( irc_channel_t *ic )
177{
178        struct irc_channel_free_data *d = g_new0( struct irc_channel_free_data, 1 );
179       
180        d->irc = ic->irc;
181        d->ic = ic;
182        d->name = g_strdup( ic->name );
183       
184        b_timeout_add( 0, irc_channel_free_callback, d );
185}
186
187static char *set_eval_channel_type( set_t *set, char *value )
188{
189        struct irc_channel *ic = set->data;
190        const struct irc_channel_funcs *new;
191       
192        if( strcmp( value, "control" ) == 0 )
193                new = &control_channel_funcs;
194        else if( strcmp( value, "chat" ) == 0 )
195                new = &irc_channel_im_chat_funcs;
196        else
197                return SET_INVALID;
198       
199        /* TODO: Return values. */
200        if( ic->f && ic->f->_free )
201                ic->f->_free( ic );
202       
203        ic->f = new;
204       
205        if( ic->f && ic->f->_init )
206                ic->f->_init( ic );
207       
208        return value;
209}
210
211int irc_channel_add_user( irc_channel_t *ic, irc_user_t *iu )
212{
213        irc_channel_user_t *icu;
214       
215        if( irc_channel_has_user( ic, iu ) )
216                return 0;
217       
218        icu = g_new0( irc_channel_user_t, 1 );
219        icu->iu = iu;
220       
221        ic->users = g_slist_insert_sorted( ic->users, icu, irc_channel_user_cmp );
222       
223        irc_channel_update_ops( ic, set_getstr( &ic->irc->b->set, "ops" ) );
224       
225        if( iu == ic->irc->user || ic->flags & IRC_CHANNEL_JOINED )
226        {
227                ic->flags |= IRC_CHANNEL_JOINED;
228                irc_send_join( ic, iu );
229        }
230       
231        return 1;
232}
233
234int irc_channel_del_user( irc_channel_t *ic, irc_user_t *iu, irc_channel_del_user_type_t type, const char *msg )
235{
236        irc_channel_user_t *icu;
237       
238        if( !( icu = irc_channel_has_user( ic, iu ) ) )
239                return 0;
240       
241        ic->users = g_slist_remove( ic->users, icu );
242        g_free( icu );
243       
244        if( !( ic->flags & IRC_CHANNEL_JOINED ) || type == IRC_CDU_SILENT ) {}
245                /* Do nothing. The caller should promise it won't screw
246                   up state of the IRC client. :-) */
247        else if( type == IRC_CDU_PART )
248                irc_send_part( ic, iu, msg );
249        else if( type == IRC_CDU_KICK )
250                irc_send_kick( ic, iu, ic->irc->root, msg );
251       
252        if( iu == ic->irc->user )
253        {
254                ic->flags &= ~IRC_CHANNEL_JOINED;
255               
256                if( ic->irc->status & USTATUS_SHUTDOWN )
257                {
258                        /* Don't do anything fancy when we're shutting down anyway. */
259                }
260                else if( ic->flags & IRC_CHANNEL_TEMP )
261                {
262                        irc_channel_free_soon( ic );
263                }
264                else
265                {
266                        /* Flush userlist now. The user won't see it anyway. */
267                        while( ic->users )
268                        {
269                                g_free( ic->users->data );
270                                ic->users = g_slist_remove( ic->users, ic->users->data );
271                        }
272                        irc_channel_add_user( ic, ic->irc->root );
273                }
274        }
275       
276        return 1;
277}
278
279irc_channel_user_t *irc_channel_has_user( irc_channel_t *ic, irc_user_t *iu )
280{
281        GSList *l;
282       
283        for( l = ic->users; l; l = l->next )
284        {
285                irc_channel_user_t *icu = l->data;
286               
287                if( icu->iu == iu )
288                        return icu;
289        }
290       
291        return NULL;
292}
293
294int irc_channel_set_topic( irc_channel_t *ic, const char *topic, const irc_user_t *iu )
295{
296        g_free( ic->topic );
297        ic->topic = g_strdup( topic );
298       
299        g_free( ic->topic_who );
300        if( iu )
301                ic->topic_who = g_strdup_printf( "%s!%s@%s", iu->nick, iu->user, iu->host );
302        else
303                ic->topic_who = NULL;
304       
305        ic->topic_time = time( NULL );
306       
307        if( ic->flags & IRC_CHANNEL_JOINED )
308                irc_send_topic( ic, TRUE );
309       
310        return 1;
311}
312
313void irc_channel_user_set_mode( irc_channel_t *ic, irc_user_t *iu, irc_channel_user_flags_t flags )
314{
315        irc_channel_user_t *icu = irc_channel_has_user( ic, iu );
316       
317        if( !icu || icu->flags == flags )
318                return;
319       
320        if( ic->flags & IRC_CHANNEL_JOINED )
321                irc_send_channel_user_mode_diff( ic, iu, icu->flags, flags );
322       
323        icu->flags = flags;
324}
325
326void irc_channel_set_mode( irc_channel_t *ic, const char *s )
327{
328        irc_t *irc = ic->irc;
329        char m[128], st = 1;
330        const char *t;
331        int i;
332        char changes[512], *p, st2 = 2;
333       
334        memset( m, 0, sizeof( m ) );
335       
336        for( t = ic->mode; *t; t ++ )
337                if( *t < sizeof( m ) )
338                        m[(int)*t] = 1;
339       
340        p = changes;
341        for( t = s; *t; t ++ )
342        {
343                if( *t == '+' || *t == '-' )
344                        st = *t == '+';
345                else if( strchr( CMODES, *t ) )
346                {
347                        if( m[(int)*t] != st)
348                        {
349                                if( st != st2 )
350                                        st2 = st, *p++ = st ? '+' : '-';
351                                *p++ = *t;
352                        }
353                        m[(int)*t] = st;
354                }
355        }
356        *p = '\0';
357       
358        memset( ic->mode, 0, sizeof( ic->mode ) );
359       
360        for( i = 'A'; i <= 'z' && strlen( ic->mode ) < ( sizeof( ic->mode ) - 1 ); i ++ )
361                if( m[i] )
362                        ic->mode[strlen(ic->mode)] = i;
363       
364        if( *changes && ( ic->flags & IRC_CHANNEL_JOINED ) )
365                irc_write( irc, ":%s!%s@%s MODE %s :%s", irc->root->nick,
366                           irc->root->user, irc->root->host, ic->name,
367                           changes );
368}
369
370void irc_channel_auto_joins( irc_t *irc, account_t *acc )
371{
372        GSList *l;
373       
374        for( l = irc->channels; l; l = l->next )
375        {
376                irc_channel_t *ic = l->data;
377                gboolean aj = set_getbool( &ic->set, "auto_join" );
378                char *type;
379               
380                if( acc &&
381                    ( type = set_getstr( &ic->set, "chat_type" ) ) &&
382                    strcmp( type, "room" ) == 0 )
383                {
384                        /* Bit of an ugly special case: Handle chatrooms here, we
385                           can only auto-join them if their account is online. */
386                        char *acc_s;
387                       
388                        if( !aj && !( ic->flags & IRC_CHANNEL_JOINED ) )
389                                /* Only continue if this one's marked as auto_join
390                                   or if we're in it already. (Possible if the
391                                   client auto-rejoined it before identyfing.) */
392                                continue;
393                        else if( !( acc_s = set_getstr( &ic->set, "account" ) ) )
394                                continue;
395                        else if( account_get( irc->b, acc_s ) != acc )
396                                continue;
397                        else if( acc->ic == NULL || !( acc->ic->flags & OPT_LOGGED_IN ) )
398                                continue;
399                        else
400                                ic->f->join( ic );
401                }
402                else if( aj )
403                {
404                        irc_channel_add_user( ic, irc->user );
405                }
406        }
407}
408
409void irc_channel_printf( irc_channel_t *ic, char *format, ... )
410{
411        va_list params;
412        char *text;
413       
414        va_start( params, format );
415        text = g_strdup_vprintf( format, params );
416        va_end( params );
417       
418        irc_send_msg( ic->irc->root, "PRIVMSG", ic->name, text, NULL );
419        g_free( text );
420}
421
422gboolean irc_channel_name_ok( const char *name_ )
423{
424        const unsigned char *name = (unsigned char*) name_;
425        int i;
426       
427        if( name_[0] == '\0' )
428                return FALSE;
429       
430        /* Check if the first character is in CTYPES (#&) */
431        if( strchr( CTYPES, name_[0] ) == NULL )
432                return FALSE;
433       
434        /* RFC 1459 keeps amazing me: While only a "few" chars are allowed
435           in nicknames, channel names can be pretty much anything as long
436           as they start with # or &. I'll be a little bit more strict and
437           disallow all non-printable characters. */
438        for( i = 1; name[i]; i ++ )
439                if( name[i] <= ' ' || name[i] == ',' )
440                        return FALSE;
441       
442        return TRUE;
443}
444
445void irc_channel_name_strip( char *name )
446{
447        int i, j;
448       
449        for( i = j = 0; name[i]; i ++ )
450                if( name[i] > ' ' && name[i] != ',' )
451                        name[j++] = name[i];
452       
453        name[j] = '\0';
454}
455
456int irc_channel_name_cmp( const char *a_, const char *b_ )
457{
458        static unsigned char case_map[256];
459        const unsigned char *a = (unsigned char*) a_, *b = (unsigned char*) b_;
460        int i;
461       
462        if( case_map['A'] == '\0' )
463        {
464                for( i = 33; i < 256; i ++ )
465                        if( i != ',' )
466                                case_map[i] = i;
467               
468                for( i = 0; i < 26; i ++ )
469                        case_map['A'+i] = 'a' + i;
470               
471                case_map['['] = '{';
472                case_map[']'] = '}';
473                case_map['~'] = '`';
474                case_map['\\'] = '|';
475        }
476       
477        if( !irc_channel_name_ok( a_ ) || !irc_channel_name_ok( b_ ) )
478                return -1;
479       
480        for( i = 0; a[i] && b[i] && case_map[a[i]] && case_map[b[i]]; i ++ )
481        {
482                if( case_map[a[i]] == case_map[b[i]] )
483                        continue;
484                else
485                        return case_map[a[i]] - case_map[b[i]];
486        }
487       
488        return case_map[a[i]] - case_map[b[i]];
489}
490
491static gint irc_channel_user_cmp( gconstpointer a_, gconstpointer b_ )
492{
493        const irc_channel_user_t *a = a_, *b = b_;
494       
495        return irc_user_cmp( a->iu, b->iu );
496}
497
498void irc_channel_update_ops( irc_channel_t *ic, char *value )
499{
500        irc_channel_user_set_mode( ic, ic->irc->root,
501                ( strcmp( value, "both" ) == 0 ||
502                  strcmp( value, "root" ) == 0 ) ? IRC_CHANNEL_USER_OP : 0 );
503        irc_channel_user_set_mode( ic, ic->irc->user,
504                ( strcmp( value, "both" ) == 0 ||
505                  strcmp( value, "user" ) == 0 ) ? IRC_CHANNEL_USER_OP : 0 );
506}
507
508char *set_eval_irc_channel_ops( set_t *set, char *value )
509{
510        irc_t *irc = set->data;
511        GSList *l;
512       
513        if( strcmp( value, "both" ) != 0 && strcmp( value, "none" ) != 0 && 
514            strcmp( value, "user" ) != 0 && strcmp( value, "root" ) != 0 )
515                return SET_INVALID;
516       
517        for( l = irc->channels; l; l = l->next )
518                irc_channel_update_ops( l->data, value );
519       
520        return value;
521}
522
523/* Channel-type dependent functions, for control channels: */
524static gboolean control_channel_privmsg( irc_channel_t *ic, const char *msg )
525{
526        irc_t *irc = ic->irc;
527        irc_user_t *iu;
528        const char *s;
529       
530        /* Scan for non-whitespace chars followed by a colon: */
531        for( s = msg; *s && !isspace( *s ) && *s != ':' && *s != ','; s ++ ) {}
532       
533        if( *s == ':' || *s == ',' )
534        {
535                char to[s-msg+1];
536               
537                memset( to, 0, sizeof( to ) );
538                strncpy( to, msg, s - msg );
539                while( *(++s) && isspace( *s ) ) {}
540                msg = s;
541               
542                if( !( iu = irc_user_by_name( irc, to ) ) )
543                        irc_channel_printf( ic, "User does not exist: %s", to );
544                else
545                        ic->last_target = iu;
546        }
547        else if( g_strcasecmp( set_getstr( &irc->b->set, "default_target" ), "last" ) == 0 &&
548                 ic->last_target && g_slist_find( irc->users, ic->last_target ) )
549                iu = ic->last_target;
550        else
551                iu = irc->root;
552       
553        if( iu && iu->f->privmsg )
554        {
555                iu->last_channel = ic;
556                iu->f->privmsg( iu, msg );
557        }
558       
559        return TRUE;
560}
561
562static gboolean control_channel_invite( irc_channel_t *ic, irc_user_t *iu )
563{
564        struct irc_control_channel *icc = ic->data;
565        bee_user_t *bu = iu->bu;
566       
567        if( bu == NULL )
568                return FALSE;
569       
570        if( icc->type != IRC_CC_TYPE_GROUP )
571        {
572                irc_send_num( ic->irc, 482, "%s :Invitations are only possible to fill_by=group channels", ic->name );
573                return FALSE;
574        }
575       
576        bu->ic->acc->prpl->add_buddy( bu->ic, bu->handle,
577                                      icc->group ? icc->group->name : NULL );
578       
579        return TRUE;
580}
581
582static char *set_eval_by_account( set_t *set, char *value );
583static char *set_eval_fill_by( set_t *set, char *value );
584static char *set_eval_by_group( set_t *set, char *value );
585static char *set_eval_by_protocol( set_t *set, char *value );
586static char *set_eval_show_users( set_t *set, char *value );
587
588static gboolean control_channel_init( irc_channel_t *ic )
589{
590        struct irc_control_channel *icc;
591       
592        set_add( &ic->set, "account", NULL, set_eval_by_account, ic );
593        set_add( &ic->set, "fill_by", "all", set_eval_fill_by, ic );
594        set_add( &ic->set, "group", NULL, set_eval_by_group, ic );
595        set_add( &ic->set, "protocol", NULL, set_eval_by_protocol, ic );
596       
597        /* When changing the default, also change it below. */
598        set_add( &ic->set, "show_users", "online+,away", set_eval_show_users, ic );
599       
600        ic->data = icc = g_new0( struct irc_control_channel, 1 );
601        icc->type = IRC_CC_TYPE_DEFAULT;
602       
603        /* Have to run the evaluator to initialize icc->modes. */
604        set_setstr( &ic->set, "show_users", "online+,away" );
605       
606        /* For scripts that care. */
607        irc_channel_set_mode( ic, "+C" );
608       
609        return TRUE;
610}
611
612static gboolean control_channel_join( irc_channel_t *ic )
613{
614        bee_irc_channel_update( ic->irc, ic, NULL );
615       
616        return TRUE;
617}
618
619static char *set_eval_by_account( set_t *set, char *value )
620{
621        struct irc_channel *ic = set->data;
622        struct irc_control_channel *icc = ic->data;
623        account_t *acc;
624       
625        if( !( acc = account_get( ic->irc->b, value ) ) )
626                return SET_INVALID;
627       
628        icc->account = acc;
629        if( icc->type == IRC_CC_TYPE_ACCOUNT )
630                bee_irc_channel_update( ic->irc, ic, NULL );
631       
632        return g_strdup( acc->tag );
633}
634
635static char *set_eval_fill_by( set_t *set, char *value )
636{
637        struct irc_channel *ic = set->data;
638        struct irc_control_channel *icc = ic->data;
639       
640        if( strcmp( value, "all" ) == 0 )
641                icc->type = IRC_CC_TYPE_DEFAULT;
642        else if( strcmp( value, "rest" ) == 0 )
643                icc->type = IRC_CC_TYPE_REST;
644        else if( strcmp( value, "group" ) == 0 )
645                icc->type = IRC_CC_TYPE_GROUP;
646        else if( strcmp( value, "account" ) == 0 )
647                icc->type = IRC_CC_TYPE_ACCOUNT;
648        else if( strcmp( value, "protocol" ) == 0 )
649                icc->type = IRC_CC_TYPE_PROTOCOL;
650        else
651                return SET_INVALID;
652       
653        bee_irc_channel_update( ic->irc, ic, NULL );
654        return value;
655}
656
657static char *set_eval_by_group( set_t *set, char *value )
658{
659        struct irc_channel *ic = set->data;
660        struct irc_control_channel *icc = ic->data;
661       
662        icc->group = bee_group_by_name( ic->irc->b, value, TRUE );
663        if( icc->type == IRC_CC_TYPE_GROUP )
664                bee_irc_channel_update( ic->irc, ic, NULL );
665       
666        return g_strdup( icc->group->name );
667}
668
669static char *set_eval_by_protocol( set_t *set, char *value )
670{
671        struct irc_channel *ic = set->data;
672        struct irc_control_channel *icc = ic->data;
673        struct prpl *prpl;
674       
675        if( !( prpl = find_protocol( value ) ) )
676                return SET_INVALID;
677       
678        icc->protocol = prpl;
679        if( icc->type == IRC_CC_TYPE_PROTOCOL )
680                bee_irc_channel_update( ic->irc, ic, NULL );
681       
682        return value;
683}
684
685static char *set_eval_show_users( set_t *set, char *value )
686{
687        struct irc_channel *ic = set->data;
688        struct irc_control_channel *icc = ic->data;
689        char **parts = g_strsplit( value, ",", 0 ), **part;
690        char modes[4];
691       
692        memset( modes, 0, 4 );
693        for( part = parts; *part; part ++ )
694        {
695                char last, modechar = IRC_CHANNEL_USER_NONE;
696               
697                if( **part == '\0' )
698                        goto fail;
699               
700                last = (*part)[strlen(*part+1)];
701                if( last == '+' )
702                        modechar = IRC_CHANNEL_USER_VOICE;
703                else if( last == '%' )
704                        modechar = IRC_CHANNEL_USER_HALFOP;
705                else if( last == '@' )
706                        modechar = IRC_CHANNEL_USER_OP;
707               
708                if( strncmp( *part, "offline", 7 ) == 0 )
709                        modes[0] = modechar;
710                else if( strncmp( *part, "away", 4 ) == 0 )
711                        modes[1] = modechar;
712                else if( strncmp( *part, "online", 6 ) == 0 )
713                        modes[2] = modechar;
714                else
715                        goto fail;
716        }
717        memcpy( icc->modes, modes, 4 );
718        bee_irc_channel_update( ic->irc, ic, NULL );
719       
720        g_strfreev( parts );
721        return value;
722       
723fail:
724        g_strfreev( parts );
725        return SET_INVALID;     
726}
727
728static gboolean control_channel_free( irc_channel_t *ic )
729{
730        struct irc_control_channel *icc = ic->data;
731       
732        set_del( &ic->set, "account" );
733        set_del( &ic->set, "fill_by" );
734        set_del( &ic->set, "group" );
735        set_del( &ic->set, "protocol" );
736       
737        g_free( icc );
738        ic->data = NULL;
739       
740        /* For scripts that care. */
741        irc_channel_set_mode( ic, "-C" );
742       
743        return TRUE;
744}
745
746static const struct irc_channel_funcs control_channel_funcs = {
747        control_channel_privmsg,
748        control_channel_join,
749        NULL,
750        NULL,
751        control_channel_invite,
752       
753        control_channel_init,
754        control_channel_free,
755};
Note: See TracBrowser for help on using the repository browser.