source: irc.c @ b919363

Last change on this file since b919363 was b919363, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-03-27T14:31:03Z

Mode stuff. Also disallow unsetting +R umode which was possible so far
(and shouldn't be).

  • Property mode set to 100644
File size: 18.2 KB
Line 
1  /********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2004 Wilmer van der Gaast and others                *
5  \********************************************************************/
6
7/* The IRC-based UI (for now the only one)                              */
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
28GSList *irc_connection_list;
29
30static char *set_eval_charset( set_t *set, char *value );
31
32irc_t *irc_new( int fd )
33{
34        irc_t *irc;
35        struct sockaddr_storage sock;
36        socklen_t socklen = sizeof( sock );
37        char *host = NULL, *myhost = NULL;
38        irc_user_t *iu;
39        set_t *s;
40        bee_t *b;
41       
42        irc = g_new0( irc_t, 1 );
43       
44        irc->fd = fd;
45        sock_make_nonblocking( irc->fd );
46       
47        irc->r_watch_source_id = b_input_add( irc->fd, GAIM_INPUT_READ, bitlbee_io_current_client_read, irc );
48       
49        irc->status = USTATUS_OFFLINE;
50        irc->last_pong = gettime();
51       
52        irc->nick_user_hash = g_hash_table_new( g_str_hash, g_str_equal );
53        irc->watches = g_hash_table_new( g_str_hash, g_str_equal );
54       
55        irc->iconv = (GIConv) -1;
56        irc->oconv = (GIConv) -1;
57       
58        if( global.conf->hostname )
59        {
60                myhost = g_strdup( global.conf->hostname );
61        }
62        else if( getsockname( irc->fd, (struct sockaddr*) &sock, &socklen ) == 0 ) 
63        {
64                char buf[NI_MAXHOST+1];
65
66                if( getnameinfo( (struct sockaddr *) &sock, socklen, buf,
67                                 NI_MAXHOST, NULL, 0, 0 ) == 0 )
68                {
69                        myhost = g_strdup( ipv6_unwrap( buf ) );
70                }
71        }
72       
73        if( getpeername( irc->fd, (struct sockaddr*) &sock, &socklen ) == 0 )
74        {
75                char buf[NI_MAXHOST+1];
76
77                if( getnameinfo( (struct sockaddr *)&sock, socklen, buf,
78                                 NI_MAXHOST, NULL, 0, 0 ) == 0 )
79                {
80                        host = g_strdup( ipv6_unwrap( buf ) );
81                }
82        }
83       
84        if( host == NULL )
85                host = g_strdup( "localhost.localdomain" );
86        if( myhost == NULL )
87                myhost = g_strdup( "localhost.localdomain" );
88       
89        //if( global.conf->ping_interval > 0 && global.conf->ping_timeout > 0 )
90        //      irc->ping_source_id = b_timeout_add( global.conf->ping_interval * 1000, irc_userping, irc );
91
92        irc_connection_list = g_slist_append( irc_connection_list, irc );
93       
94        b = irc->b = bee_new();
95       
96        s = set_add( &b->set, "away_devoice", "true", NULL/*set_eval_away_devoice*/, irc );
97        s = set_add( &b->set, "buddy_sendbuffer", "false", set_eval_bool, irc );
98        s = set_add( &b->set, "buddy_sendbuffer_delay", "200", set_eval_int, irc );
99        s = set_add( &b->set, "charset", "utf-8", set_eval_charset, irc );
100        //s = set_add( &b->set, "control_channel", irc->channel, NULL/*set_eval_control_channel*/, irc );
101        s = set_add( &b->set, "default_target", "root", NULL, irc );
102        s = set_add( &b->set, "display_namechanges", "false", set_eval_bool, irc );
103        s = set_add( &b->set, "handle_unknown", "root", NULL, irc );
104        s = set_add( &b->set, "lcnicks", "true", set_eval_bool, irc );
105        s = set_add( &b->set, "ops", "both", NULL/*set_eval_ops*/, irc );
106        s = set_add( &b->set, "private", "true", set_eval_bool, irc );
107        s = set_add( &b->set, "query_order", "lifo", NULL, irc );
108        s = set_add( &b->set, "root_nick", ROOT_NICK, NULL/*set_eval_root_nick*/, irc );
109        s = set_add( &b->set, "simulate_netsplit", "true", set_eval_bool, irc );
110        s = set_add( &b->set, "to_char", ": ", set_eval_to_char, irc );
111        s = set_add( &b->set, "typing_notice", "false", set_eval_bool, irc );
112
113        irc->root = iu = irc_user_new( irc, ROOT_NICK );
114        iu->host = g_strdup( myhost );
115        iu->fullname = g_strdup( ROOT_FN );
116       
117        iu = irc_user_new( irc, NS_NICK );
118        iu->host = g_strdup( myhost );
119        iu->fullname = g_strdup( ROOT_FN );
120       
121        irc->user = g_new0( irc_user_t, 1 );
122        irc->user->host = g_strdup( host );
123       
124        conf_loaddefaults( irc );
125       
126        /* Evaluator sets the iconv/oconv structures. */
127        set_eval_charset( set_find( &b->set, "charset" ), set_getstr( &b->set, "charset" ) );
128       
129        irc_write( irc, ":%s NOTICE AUTH :%s", irc->root->host, "BitlBee-IRCd initialized, please go on" );
130       
131        g_free( myhost );
132        g_free( host );
133       
134        return irc;
135}
136
137/* immed=1 makes this function pretty much equal to irc_free(), except that
138   this one will "log". In case the connection is already broken and we
139   shouldn't try to write to it. */
140void irc_abort( irc_t *irc, int immed, char *format, ... )
141{
142        if( format != NULL )
143        {
144                va_list params;
145                char *reason;
146               
147                va_start( params, format );
148                reason = g_strdup_vprintf( format, params );
149                va_end( params );
150               
151                if( !immed )
152                        irc_write( irc, "ERROR :Closing link: %s", reason );
153               
154                ipc_to_master_str( "OPERMSG :Client exiting: %s@%s [%s]\r\n",
155                                   irc->user->nick ? irc->user->nick : "(NONE)", irc->root->host, reason );
156               
157                g_free( reason );
158        }
159        else
160        {
161                if( !immed )
162                        irc_write( irc, "ERROR :Closing link" );
163               
164                ipc_to_master_str( "OPERMSG :Client exiting: %s@%s [%s]\r\n",
165                                   irc->user->nick ? irc->user->nick : "(NONE)", irc->root->host, "No reason given" );
166        }
167       
168        irc->status |= USTATUS_SHUTDOWN;
169        if( irc->sendbuffer && !immed )
170        {
171                /* Set up a timeout event that should shut down the connection
172                   in a second, just in case ..._write doesn't do it first. */
173               
174                b_event_remove( irc->r_watch_source_id );
175                irc->r_watch_source_id = 0;
176               
177                b_event_remove( irc->ping_source_id );
178                irc->ping_source_id = b_timeout_add( 1000, (b_event_handler) irc_free, irc );
179        }
180        else
181        {
182                irc_free( irc );
183        }
184}
185
186static gboolean irc_free_hashkey( gpointer key, gpointer value, gpointer data );
187
188void irc_free( irc_t * irc )
189{
190        log_message( LOGLVL_INFO, "Destroying connection with fd %d", irc->fd );
191       
192        /*
193        if( irc->status & USTATUS_IDENTIFIED && set_getbool( &irc->b->set, "save_on_quit" ) )
194                if( storage_save( irc, NULL, TRUE ) != STORAGE_OK )
195                        irc_usermsg( irc, "Error while saving settings!" );
196        */
197       
198        irc_connection_list = g_slist_remove( irc_connection_list, irc );
199       
200        /*
201        while( irc->queries != NULL )
202                query_del( irc, irc->queries );
203        */
204       
205        while( irc->users )
206        {
207                irc_user_t *iu = irc->users->data;
208                irc_user_free( irc, iu->nick );
209        }
210       
211        while( irc->channels )
212                irc_channel_free( irc->channels->data );
213       
214        if( irc->ping_source_id > 0 )
215                b_event_remove( irc->ping_source_id );
216        if( irc->r_watch_source_id > 0 )
217                b_event_remove( irc->r_watch_source_id );
218        if( irc->w_watch_source_id > 0 )
219                b_event_remove( irc->w_watch_source_id );
220       
221        closesocket( irc->fd );
222        irc->fd = -1;
223       
224        g_hash_table_foreach_remove( irc->nick_user_hash, irc_free_hashkey, NULL );
225        g_hash_table_destroy( irc->nick_user_hash );
226       
227        g_hash_table_foreach_remove( irc->watches, irc_free_hashkey, NULL );
228        g_hash_table_destroy( irc->watches );
229       
230        if( irc->iconv != (GIConv) -1 )
231                g_iconv_close( irc->iconv );
232        if( irc->oconv != (GIConv) -1 )
233                g_iconv_close( irc->oconv );
234       
235        g_free( irc->sendbuffer );
236        g_free( irc->readbuffer );
237       
238        g_free( irc->password );
239       
240        g_free( irc );
241       
242        if( global.conf->runmode == RUNMODE_INETD ||
243            global.conf->runmode == RUNMODE_FORKDAEMON ||
244            ( global.conf->runmode == RUNMODE_DAEMON &&
245              global.listen_socket == -1 &&
246              irc_connection_list == NULL ) )
247                b_main_quit();
248}
249
250static gboolean irc_free_hashkey( gpointer key, gpointer value, gpointer data )
251{
252        g_free( key );
253       
254        return( TRUE );
255}
256
257static char **irc_splitlines( char *buffer );
258
259void irc_process( irc_t *irc )
260{
261        char **lines, *temp, **cmd;
262        int i;
263
264        if( irc->readbuffer != NULL )
265        {
266                lines = irc_splitlines( irc->readbuffer );
267               
268                for( i = 0; *lines[i] != '\0'; i ++ )
269                {
270                        char *conv = NULL;
271                       
272                        /* [WvG] If the last line isn't empty, it's an incomplete line and we
273                           should wait for the rest to come in before processing it. */
274                        if( lines[i+1] == NULL )
275                        {
276                                temp = g_strdup( lines[i] );
277                                g_free( irc->readbuffer );
278                                irc->readbuffer = temp;
279                                i ++;
280                                break;
281                        }
282                       
283                        if( irc->iconv != (GIConv) -1 )
284                        {
285                                gsize bytes_read, bytes_written;
286                               
287                                conv = g_convert_with_iconv( lines[i], -1, irc->iconv,
288                                                             &bytes_read, &bytes_written, NULL );
289                               
290                                if( conv == NULL || bytes_read != strlen( lines[i] ) )
291                                {
292                                        /* GLib can do strange things if things are not in the expected charset,
293                                           so let's be a little bit paranoid here: */
294                                        if( irc->status & USTATUS_LOGGED_IN )
295                                        {
296                                                irc_usermsg( irc, "Error: Charset mismatch detected. The charset "
297                                                                  "setting is currently set to %s, so please make "
298                                                                  "sure your IRC client will send and accept text in "
299                                                                  "that charset, or tell BitlBee which charset to "
300                                                                  "expect by changing the charset setting. See "
301                                                                  "`help set charset' for more information. Your "
302                                                                  "message was ignored.",
303                                                                  set_getstr( &irc->b->set, "charset" ) );
304                                               
305                                                g_free( conv );
306                                                conv = NULL;
307                                        }
308                                        else
309                                        {
310                                                irc_write( irc, ":%s NOTICE AUTH :%s", irc->root->host,
311                                                           "Warning: invalid characters received at login time." );
312                                               
313                                                conv = g_strdup( lines[i] );
314                                                for( temp = conv; *temp; temp ++ )
315                                                        if( *temp & 0x80 )
316                                                                *temp = '?';
317                                        }
318                                }
319                                lines[i] = conv;
320                        }
321                       
322                        if( lines[i] && ( cmd = irc_parse_line( lines[i] ) ) )
323                        {
324                                irc_exec( irc, cmd );
325                                g_free( cmd );
326                        }
327                       
328                        g_free( conv );
329                       
330                        /* Shouldn't really happen, but just in case... */
331                        if( !g_slist_find( irc_connection_list, irc ) )
332                        {
333                                g_free( lines );
334                                return;
335                        }
336                }
337               
338                if( lines[i] != NULL )
339                {
340                        g_free( irc->readbuffer );
341                        irc->readbuffer = NULL;
342                }
343               
344                g_free( lines );
345        }
346}
347
348/* Splits a long string into separate lines. The array is NULL-terminated
349   and, unless the string contains an incomplete line at the end, ends with
350   an empty string. Could use g_strsplit() but this one does it in-place.
351   (So yes, it's destructive.) */
352static char **irc_splitlines( char *buffer )
353{
354        int i, j, n = 3;
355        char **lines;
356
357        /* Allocate n+1 elements. */
358        lines = g_new( char *, n + 1 );
359       
360        lines[0] = buffer;
361       
362        /* Split the buffer in several strings, and accept any kind of line endings,
363         * knowing that ERC on Windows may send something interesting like \r\r\n,
364         * and surely there must be clients that think just \n is enough... */
365        for( i = 0, j = 0; buffer[i] != '\0'; i ++ )
366        {
367                if( buffer[i] == '\r' || buffer[i] == '\n' )
368                {
369                        while( buffer[i] == '\r' || buffer[i] == '\n' )
370                                buffer[i++] = '\0';
371                       
372                        lines[++j] = buffer + i;
373                       
374                        if( j >= n )
375                        {
376                                n *= 2;
377                                lines = g_renew( char *, lines, n + 1 );
378                        }
379
380                        if( buffer[i] == '\0' )
381                                break;
382                }
383        }
384       
385        /* NULL terminate our list. */ 
386        lines[++j] = NULL;
387       
388        return lines;
389}
390
391/* Split an IRC-style line into little parts/arguments. */
392char **irc_parse_line( char *line )
393{
394        int i, j;
395        char **cmd;
396       
397        /* Move the line pointer to the start of the command, skipping spaces and the optional prefix. */
398        if( line[0] == ':' )
399        {
400                for( i = 0; line[i] && line[i] != ' '; i ++ );
401                line = line + i;
402        }
403        for( i = 0; line[i] == ' '; i ++ );
404        line = line + i;
405       
406        /* If we're already at the end of the line, return. If not, we're going to need at least one element. */
407        if( line[0] == '\0')
408                return NULL;
409       
410        /* Count the number of char **cmd elements we're going to need. */
411        j = 1;
412        for( i = 0; line[i] != '\0'; i ++ )
413        {
414                if( line[i] == ' ' )
415                {
416                        j ++;
417                       
418                        if( line[i+1] == ':' )
419                                break;
420                }
421        }       
422
423        /* Allocate the space we need. */
424        cmd = g_new( char *, j + 1 );
425        cmd[j] = NULL;
426       
427        /* Do the actual line splitting, format is:
428         * Input: "PRIVMSG #bitlbee :foo bar"
429         * Output: cmd[0]=="PRIVMSG", cmd[1]=="#bitlbee", cmd[2]=="foo bar", cmd[3]==NULL
430         */
431
432        cmd[0] = line;
433        for( i = 0, j = 0; line[i] != '\0'; i ++ )
434        {
435                if( line[i] == ' ' )
436                {
437                        line[i] = '\0';
438                        cmd[++j] = line + i + 1;
439                       
440                        if( line[i+1] == ':' )
441                        {
442                                cmd[j] ++;
443                                break;
444                        }
445                }
446        }
447       
448        return cmd;
449}
450
451/* Converts such an array back into a command string. Mainly used for the IPC code right now. */
452char *irc_build_line( char **cmd )
453{
454        int i, len;
455        char *s;
456       
457        if( cmd[0] == NULL )
458                return NULL;
459       
460        len = 1;
461        for( i = 0; cmd[i]; i ++ )
462                len += strlen( cmd[i] ) + 1;
463       
464        if( strchr( cmd[i-1], ' ' ) != NULL )
465                len ++;
466       
467        s = g_new0( char, len + 1 );
468        for( i = 0; cmd[i]; i ++ )
469        {
470                if( cmd[i+1] == NULL && strchr( cmd[i], ' ' ) != NULL )
471                        strcat( s, ":" );
472               
473                strcat( s, cmd[i] );
474               
475                if( cmd[i+1] )
476                        strcat( s, " " );
477        }
478        strcat( s, "\r\n" );
479       
480        return s;
481}
482
483void irc_write( irc_t *irc, char *format, ... ) 
484{
485        va_list params;
486
487        va_start( params, format );
488        irc_vawrite( irc, format, params );     
489        va_end( params );
490
491        return;
492}
493
494void irc_write_all( int now, char *format, ... )
495{
496        va_list params;
497        GSList *temp;   
498       
499        va_start( params, format );
500       
501        temp = irc_connection_list;
502        while( temp != NULL )
503        {
504                irc_t *irc = temp->data;
505               
506                if( now )
507                {
508                        g_free( irc->sendbuffer );
509                        irc->sendbuffer = g_strdup( "\r\n" );
510                }
511                irc_vawrite( temp->data, format, params );
512                if( now )
513                {
514                        bitlbee_io_current_client_write( irc, irc->fd, GAIM_INPUT_WRITE );
515                }
516                temp = temp->next;
517        }
518       
519        va_end( params );
520        return;
521} 
522
523void irc_vawrite( irc_t *irc, char *format, va_list params )
524{
525        int size;
526        char line[IRC_MAX_LINE+1];
527               
528        /* Don't try to write anything new anymore when shutting down. */
529        if( irc->status & USTATUS_SHUTDOWN )
530                return;
531       
532        memset( line, 0, sizeof( line ) );
533        g_vsnprintf( line, IRC_MAX_LINE - 2, format, params );
534        strip_newlines( line );
535       
536        if( irc->oconv != (GIConv) -1 )
537        {
538                gsize bytes_read, bytes_written;
539                char *conv;
540               
541                conv = g_convert_with_iconv( line, -1, irc->oconv,
542                                             &bytes_read, &bytes_written, NULL );
543
544                if( bytes_read == strlen( line ) )
545                        strncpy( line, conv, IRC_MAX_LINE - 2 );
546               
547                g_free( conv );
548        }
549        g_strlcat( line, "\r\n", IRC_MAX_LINE + 1 );
550       
551        if( irc->sendbuffer != NULL )
552        {
553                size = strlen( irc->sendbuffer ) + strlen( line );
554                irc->sendbuffer = g_renew ( char, irc->sendbuffer, size + 1 );
555                strcpy( ( irc->sendbuffer + strlen( irc->sendbuffer ) ), line );
556        }
557        else
558        {
559                irc->sendbuffer = g_strdup(line);
560        }
561       
562        if( irc->w_watch_source_id == 0 )
563        {
564                /* If the buffer is empty we can probably write, so call the write event handler
565                   immediately. If it returns TRUE, it should be called again, so add the event to
566                   the queue. If it's FALSE, we emptied the buffer and saved ourselves some work
567                   in the event queue. */
568                /* Really can't be done as long as the code doesn't do error checking very well:
569                if( bitlbee_io_current_client_write( irc, irc->fd, GAIM_INPUT_WRITE ) ) */
570               
571                /* So just always do it via the event handler. */
572                irc->w_watch_source_id = b_input_add( irc->fd, GAIM_INPUT_WRITE, bitlbee_io_current_client_write, irc );
573        }
574       
575        return;
576}
577
578int irc_check_login( irc_t *irc )
579{
580        if( irc->user->user && irc->user->nick )
581        {
582                if( global.conf->authmode == AUTHMODE_CLOSED && !( irc->status & USTATUS_AUTHORIZED ) )
583                {
584                        irc_send_num( irc, 464, ":This server is password-protected." );
585                        return 0;
586                }
587                else
588                {
589                        irc_channel_t *ic;
590                        irc_user_t *iu = irc->user;
591                       
592                        irc->user = irc_user_new( irc, iu->nick );
593                        irc->user->user = iu->user;
594                        irc->user->host = iu->host;
595                        irc->user->fullname = iu->fullname;
596                        g_free( iu->nick );
597                        g_free( iu );
598                       
599                        if( global.conf->runmode == RUNMODE_FORKDAEMON || global.conf->runmode == RUNMODE_DAEMON )
600                                ipc_to_master_str( "CLIENT %s %s :%s\r\n", irc->user->host, irc->user->nick, irc->user->fullname );
601                       
602                        irc->status |= USTATUS_LOGGED_IN;
603                       
604                        /* This is for bug #209 (use PASS to identify to NickServ). */
605                        if( irc->password != NULL )
606                        {
607                                char *send_cmd[] = { "identify", g_strdup( irc->password ), NULL };
608                               
609                                /*irc_setpass( irc, NULL );*/
610                                /*root_command( irc, send_cmd );*/
611                                g_free( send_cmd[1] );
612                        }
613                       
614                        irc_send_login( irc );
615                       
616                        irc->umode[0] = '\0';
617                        irc_umode_set( irc, "+" UMODE, TRUE );
618                       
619                        ic = irc_channel_new( irc, ROOT_CHAN );
620                        irc_channel_set_topic( ic, CONTROL_TOPIC, irc->root );
621                        irc_channel_add_user( ic, irc->user );
622                       
623                        return 1;
624                }
625        }
626        else
627        {
628                /* More information needed. */
629                return 0;
630        }
631}
632
633void irc_umode_set( irc_t *irc, const char *s, gboolean allow_priv )
634{
635        /* allow_priv: Set to 0 if s contains user input, 1 if you want
636           to set a "privileged" mode (+o, +R, etc). */
637        char m[128], st = 1;
638        const char *t;
639        int i;
640        char changes[512], *p, st2 = 2;
641        char badflag = 0;
642       
643        memset( m, 0, sizeof( m ) );
644       
645        for( t = irc->umode; *t; t ++ )
646                if( *t < sizeof( m ) )
647                        m[(int)*t] = 1;
648       
649        p = changes;
650        for( t = s; *t; t ++ )
651        {
652                if( *t == '+' || *t == '-' )
653                        st = *t == '+';
654                else if( ( st == 0 && ( !strchr( UMODES_KEEP, *t ) || allow_priv ) ) ||
655                         ( st == 1 && strchr( UMODES, *t ) ) ||
656                         ( st == 1 && allow_priv && strchr( UMODES_PRIV, *t ) ) )
657                {
658                        if( m[(int)*t] != st)
659                        {
660                                if( st != st2 )
661                                        st2 = st, *p++ = st ? '+' : '-';
662                                *p++ = *t;
663                        }
664                        m[(int)*t] = st;
665                }
666                else
667                        badflag = 1;
668        }
669        *p = '\0';
670       
671        memset( irc->umode, 0, sizeof( irc->umode ) );
672       
673        for( i = 'A'; i <= 'z' && strlen( irc->umode ) < ( sizeof( irc->umode ) - 1 ); i ++ )
674                if( m[i] )
675                        irc->umode[strlen(irc->umode)] = i;
676       
677        if( badflag )
678                irc_send_num( irc, 501, ":Unknown MODE flag" );
679        if( *changes )
680                irc_write( irc, ":%s!%s@%s MODE %s :%s", irc->user->nick,
681                           irc->user->user, irc->user->host, irc->user->nick,
682                           changes );
683}
684
685
686
687static char *set_eval_charset( set_t *set, char *value )
688{
689        irc_t *irc = set->data;
690        GIConv ic, oc;
691
692        if( g_strcasecmp( value, "none" ) == 0 )
693                value = g_strdup( "utf-8" );
694
695        if( ( ic = g_iconv_open( "utf-8", value ) ) == (GIConv) -1 )
696        {
697                return NULL;
698        }
699        if( ( oc = g_iconv_open( value, "utf-8" ) ) == (GIConv) -1 )
700        {
701                g_iconv_close( ic );
702                return NULL;
703        }
704       
705        if( irc->iconv != (GIConv) -1 )
706                g_iconv_close( irc->iconv );
707        if( irc->oconv != (GIConv) -1 )
708                g_iconv_close( irc->oconv );
709       
710        irc->iconv = ic;
711        irc->oconv = oc;
712
713        return value;
714}
Note: See TracBrowser for help on using the repository browser.