source: irc_im.c @ 098a75b

Last change on this file since 098a75b was 098a75b, checked in by dequis <dx@…>, at 2015-03-22T13:35:08Z

Fix a bunch of memory leaks

  • irc_im.c:
    • bee_irc_user_msg: strdup leaks when otr swallows messages
    • bee_irc_user_action_response: GString leak in all ctcp replies
  • otr.c:
    • call g_slist_free() on the list of the otr_policy setting
    • otr_filter_msg_in: call otrl_tlv_free() if "tlvs" are returned
    • otr_filter_msg_out: don't g_strdup() if the message should be ignored
    • log_otr_message: g_strdup_vprintf() leaks always
  • nogaim.c:
    • imcb_ask_auth/imcb_ask_add: leaks in g_strdup_printf()
    • imcb_ask_add leaks imcb_ask_cb_data if the user already exists
    • add imcb_ask_cb_free() to correctly free its data
  • msn_util.c: add msn_buddy_ask_free(), ditto
  • storage_xml.c: pass_cr/password if base64_decode or arc_decode fail
  • ssl_gnutls.c: conn->hostname leak in error conditions, like invalid certs
  • jabber_util.c: jabber_buddy_by_ext_jid() leaks jid if it's not an ext jid
  • Property mode set to 100644
File size: 27.2 KB
RevLine 
[5ebff60]1/********************************************************************\
[81e04e1]2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
[0e788f5]4  * Copyright 2002-2012 Wilmer van der Gaast and others                *
[81e04e1]5  \********************************************************************/
6
7/* Some glue to put the IRC and the IM stuff together.                  */
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;
[6f10697]22  if not, write to the Free Software Foundation, Inc., 51 Franklin St.,
23  Fifth Floor, Boston, MA  02110-1301  USA
[81e04e1]24*/
25
26#include "bitlbee.h"
[17a6ee9]27#include "dcc.h"
[d860a8d]28
[e4816ea]29/* IM->IRC callbacks: Simple IM/buddy-related stuff. */
[d860a8d]30
[81e04e1]31static const struct irc_user_funcs irc_user_im_funcs;
32
[5ebff60]33static void bee_irc_imc_connected(struct im_connection *ic)
[5c7b45c]34{
[5ebff60]35        irc_t *irc = (irc_t *) ic->bee->ui_data;
36
37        irc_channel_auto_joins(irc, ic->acc);
[5c7b45c]38}
39
[5ebff60]40static void bee_irc_imc_disconnected(struct im_connection *ic)
[5c7b45c]41{
42        /* Maybe try to send /QUITs here instead of later on. */
43}
44
[5ebff60]45static gboolean bee_irc_user_new(bee_t *bee, bee_user_t *bu)
[81e04e1]46{
47        irc_user_t *iu;
[5ebff60]48        irc_t *irc = (irc_t *) bee->ui_data;
49        char nick[MAX_NICK_LENGTH + 1], *s;
50
51        memset(nick, 0, MAX_NICK_LENGTH + 1);
52        strcpy(nick, nick_get(bu));
53
54        bu->ui_data = iu = irc_user_new(irc, nick);
[d860a8d]55        iu->bu = bu;
[5ebff60]56
57        if (set_getbool(&irc->b->set, "private")) {
[bb151f7]58                iu->last_channel = NULL;
[5ebff60]59        } else {
60                iu->last_channel = irc_channel_with_user(irc, iu);
61        }
62
63        if ((s = strchr(bu->handle, '@'))) {
64                iu->host = g_strdup(s + 1);
65                iu->user = g_strndup(bu->handle, s - bu->handle);
66        } else {
67                iu->user = g_strdup(bu->handle);
68                if (bu->ic->acc->server) {
69                        iu->host = g_strdup(bu->ic->acc->server);
70                } else {
[c92ee728]71                        char *s;
[5ebff60]72                        for (s = bu->ic->acc->tag; g_ascii_isalnum(*s); s++) {
73                                ;
74                        }
[c92ee728]75                        /* Only use the tag if we got to the end of the string.
76                           (So allow alphanumerics only. Hopefully not too
77                           restrictive.) */
[5ebff60]78                        if (*s) {
79                                iu->host = g_strdup(bu->ic->acc->prpl->name);
80                        } else {
81                                iu->host = g_strdup(bu->ic->acc->tag);
82                        }
[c92ee728]83                }
[81e04e1]84        }
[5ebff60]85
86        while ((s = strchr(iu->user, ' '))) {
[495d21b]87                *s = '_';
[5ebff60]88        }
89
90        if (bu->flags & BEE_USER_LOCAL) {
91                char *s = set_getstr(&bee->set, "handle_unknown");
92
93                if (strcmp(s, "add_private") == 0) {
[92c8d41]94                        iu->last_channel = NULL;
[5ebff60]95                } else if (strcmp(s, "add_channel") == 0) {
[92c8d41]96                        iu->last_channel = irc->default_channel;
[5ebff60]97                }
[ad404ab]98        }
[5ebff60]99
[81e04e1]100        iu->f = &irc_user_im_funcs;
[5ebff60]101
[81e04e1]102        return TRUE;
103}
104
[5ebff60]105static gboolean bee_irc_user_free(bee_t *bee, bee_user_t *bu)
[d860a8d]106{
[5ebff60]107        return irc_user_free(bee->ui_data, (irc_user_t *) bu->ui_data);
[d860a8d]108}
[81e04e1]109
[5ebff60]110static gboolean bee_irc_user_status(bee_t *bee, bee_user_t *bu, bee_user_t *old)
[d860a8d]111{
[231b08b]112        irc_t *irc = bee->ui_data;
[003a12b]113        irc_user_t *iu = bu->ui_data;
[5ebff60]114
[eb50495]115        /* Do this outside the if below since away state can change without
116           the online state changing. */
117        iu->flags &= ~IRC_USER_AWAY;
[5ebff60]118        if (bu->flags & BEE_USER_AWAY || !(bu->flags & BEE_USER_ONLINE)) {
[eb50495]119                iu->flags |= IRC_USER_AWAY;
[5ebff60]120        }
121
122        if ((bu->flags & BEE_USER_ONLINE) != (old->flags & BEE_USER_ONLINE)) {
123                if (bu->flags & BEE_USER_ONLINE) {
124                        if (g_hash_table_lookup(irc->watches, iu->key)) {
125                                irc_send_num(irc, 600, "%s %s %s %d :%s", iu->nick, iu->user,
126                                             iu->host, (int) time(NULL), "logged online");
127                        }
128                } else {
129                        if (g_hash_table_lookup(irc->watches, iu->key)) {
130                                irc_send_num(irc, 601, "%s %s %s %d :%s", iu->nick, iu->user,
131                                             iu->host, (int) time(NULL), "logged offline");
132                        }
133
[0bd948e]134                        /* Send a QUIT since those will also show up in any
135                           query windows the user may have, plus it's only
136                           one QUIT instead of possibly many (in case of
137                           multiple control chans). If there's a channel that
138                           shows offline people, a JOIN will follow. */
[5ebff60]139                        if (set_getbool(&bee->set, "offline_user_quits")) {
140                                irc_user_quit(iu, "Leaving...");
141                        }
[003a12b]142                }
[231b08b]143        }
[5ebff60]144
[1c8e5f7]145        /* Reset this one since the info may have changed. */
146        iu->away_reply_timeout = 0;
[5ebff60]147
148        bee_irc_channel_update(irc, NULL, iu);
149
[d860a8d]150        return TRUE;
151}
[81e04e1]152
[5ebff60]153void bee_irc_channel_update(irc_t *irc, irc_channel_t *ic, irc_user_t *iu)
[13c1a9f]154{
155        GSList *l;
[5ebff60]156
157        if (ic == NULL) {
158                for (l = irc->channels; l; l = l->next) {
[13c1a9f]159                        ic = l->data;
160                        /* TODO: Just add a type flag or so.. */
[5ebff60]161                        if (ic->f == irc->default_channel->f &&
162                            (ic->flags & IRC_CHANNEL_JOINED)) {
163                                bee_irc_channel_update(irc, ic, iu);
164                        }
[13c1a9f]165                }
166                return;
167        }
[5ebff60]168        if (iu == NULL) {
169                for (l = irc->users; l; l = l->next) {
[13c1a9f]170                        iu = l->data;
[5ebff60]171                        if (iu->bu) {
172                                bee_irc_channel_update(irc, ic, l->data);
173                        }
[13c1a9f]174                }
175                return;
176        }
[5ebff60]177
178        if (!irc_channel_wants_user(ic, iu)) {
179                irc_channel_del_user(ic, iu, IRC_CDU_PART, NULL);
180        } else {
[ac2717b]181                struct irc_control_channel *icc = ic->data;
[94d5da9c]182                int mode = 0;
[5ebff60]183
184                if (!(iu->bu->flags & BEE_USER_ONLINE)) {
[94d5da9c]185                        mode = icc->modes[0];
[5ebff60]186                } else if (iu->bu->flags & BEE_USER_AWAY) {
[94d5da9c]187                        mode = icc->modes[1];
[5ebff60]188                } else if (iu->bu->flags & BEE_USER_SPECIAL) {
[94d5da9c]189                        mode = icc->modes[2];
[5ebff60]190                } else {
[7b8238d]191                        mode = icc->modes[3];
[5ebff60]192                }
193
194                if (!mode) {
195                        irc_channel_del_user(ic, iu, IRC_CDU_PART, NULL);
196                } else {
197                        irc_channel_add_user(ic, iu);
198                        irc_channel_user_set_mode(ic, iu, mode);
[94d5da9c]199                }
[13c1a9f]200        }
201}
202
[5ebff60]203static gboolean bee_irc_user_msg(bee_t *bee, bee_user_t *bu, const char *msg_, time_t sent_at)
[f012a9f]204{
205        irc_t *irc = bee->ui_data;
206        irc_user_t *iu = (irc_user_t *) bu->ui_data;
[e67e513]207        const char *dst;
208        char *prefix = NULL;
[21c87a7]209        char *wrapped, *ts = NULL;
[5ebff60]210        char *msg = g_strdup(msg_);
[934db064]211        GSList *l;
[5ebff60]212
213        if (sent_at > 0 && set_getbool(&irc->b->set, "display_timestamps")) {
214                ts = irc_format_timestamp(irc, sent_at);
[f012a9f]215        }
[5ebff60]216
217        dst = irc_user_msgdest(iu);
218        if (dst != irc->user->nick) {
219                /* if not messaging directly, call user by name */
220                prefix = g_strdup_printf("%s%s%s", irc->user->nick, set_getstr(&bee->set, "to_char"), ts ? : "");
221        } else {
[92c8d41]222                prefix = ts;
[3864c08]223                ts = NULL;      /* don't double-free */
[f012a9f]224        }
[5ebff60]225
226        for (l = irc_plugins; l; l = l->next) {
[934db064]227                irc_plugin_t *p = l->data;
[5ebff60]228                if (p->filter_msg_in) {
229                        char *s = p->filter_msg_in(iu, msg, 0);
230                        if (s) {
231                                if (s != msg) {
232                                        g_free(msg);
233                                }
[934db064]234                                msg = s;
[5ebff60]235                        } else {
[934db064]236                                /* Modules can swallow messages. */
[098a75b]237                                goto cleanup;
[934db064]238                        }
239                }
240        }
[5ebff60]241
242        if ((g_strcasecmp(set_getstr(&bee->set, "strip_html"), "always") == 0) ||
243            ((bu->ic->flags & OPT_DOES_HTML) && set_getbool(&bee->set, "strip_html"))) {
244                char *s = g_strdup(msg);
245                strip_html(s);
246                g_free(msg);
[934db064]247                msg = s;
248        }
[5ebff60]249
250        wrapped = word_wrap(msg, 425);
251        irc_send_msg(iu, "PRIVMSG", dst, wrapped, prefix);
252        g_free(wrapped);
[098a75b]253
254cleanup:
[5ebff60]255        g_free(prefix);
256        g_free(msg);
257        g_free(ts);
258
[f012a9f]259        return TRUE;
260}
261
[5ebff60]262static gboolean bee_irc_user_typing(bee_t *bee, bee_user_t *bu, uint32_t flags)
[573dab0]263{
264        irc_t *irc = (irc_t *) bee->ui_data;
[5ebff60]265
266        if (set_getbool(&bee->set, "typing_notice")) {
267                irc_send_msg_f((irc_user_t *) bu->ui_data, "PRIVMSG", irc->user->nick,
268                               "\001TYPING %d\001", (flags >> 8) & 3);
269        } else {
[573dab0]270                return FALSE;
[5ebff60]271        }
272
[573dab0]273        return TRUE;
274}
275
[5ebff60]276static gboolean bee_irc_user_action_response(bee_t *bee, bee_user_t *bu, const char *action, char * const args[],
277                                             void *data)
[d88c92a]278{
279        irc_t *irc = (irc_t *) bee->ui_data;
[5ebff60]280        GString *msg = g_string_new("\001");
281
282        g_string_append(msg, action);
283        while (*args) {
284                if (strchr(*args, ' ')) {
285                        g_string_append_printf(msg, " \"%s\"", *args);
286                } else {
287                        g_string_append_printf(msg, " %s", *args);
288                }
289                args++;
290        }
291        g_string_append_c(msg, '\001');
292
293        irc_send_msg((irc_user_t *) bu->ui_data, "NOTICE", irc->user->nick, msg->str, NULL);
294
[098a75b]295        g_string_free(msg, TRUE);
296
[d88c92a]297        return TRUE;
298}
299
[5ebff60]300static gboolean bee_irc_user_nick_update(irc_user_t *iu);
[6ef9065]301
[5ebff60]302static gboolean bee_irc_user_fullname(bee_t *bee, bee_user_t *bu)
[1d39159]303{
304        irc_user_t *iu = (irc_user_t *) bu->ui_data;
305        char *s;
[5ebff60]306
307        if (iu->fullname != iu->nick) {
308                g_free(iu->fullname);
309        }
310        iu->fullname = g_strdup(bu->fullname);
311
[1d39159]312        /* Strip newlines (unlikely, but IRC-unfriendly so they must go)
313           TODO(wilmer): Do the same with away msgs again! */
[5ebff60]314        for (s = iu->fullname; *s; s++) {
315                if (g_ascii_isspace(*s)) {
316                        *s = ' ';
317                }
318        }
319
320        if ((bu->ic->flags & OPT_LOGGED_IN) && set_getbool(&bee->set, "display_namechanges")) {
[fda55fa]321                /* People don't like this /NOTICE. Meh, let's go back to the old one.
[1d39159]322                char *msg = g_strdup_printf( "<< \002BitlBee\002 - Changed name to `%s' >>", iu->fullname );
323                irc_send_msg( iu, "NOTICE", irc->user->nick, msg, NULL );
[fda55fa]324                */
[5ebff60]325                imcb_log(bu->ic, "User `%s' changed name to `%s'", iu->nick, iu->fullname);
[1d39159]326        }
[5ebff60]327
328        bee_irc_user_nick_update(iu);
329
[1d39159]330        return TRUE;
331}
332
[5ebff60]333static gboolean bee_irc_user_nick_hint(bee_t *bee, bee_user_t *bu, const char *hint)
[6ef9065]334{
[5ebff60]335        bee_irc_user_nick_update((irc_user_t *) bu->ui_data);
336
[badd148]337        return TRUE;
338}
339
[5ebff60]340static gboolean bee_irc_user_group(bee_t *bee, bee_user_t *bu)
[badd148]341{
342        irc_user_t *iu = (irc_user_t *) bu->ui_data;
343        irc_t *irc = (irc_t *) bee->ui_data;
[51a3d12]344        bee_user_flags_t online;
[5ebff60]345
[51a3d12]346        /* Take the user offline temporarily so we can change the nick (if necessary). */
[5ebff60]347        if ((online = bu->flags & BEE_USER_ONLINE)) {
[51a3d12]348                bu->flags &= ~BEE_USER_ONLINE;
[5ebff60]349        }
350
351        bee_irc_channel_update(irc, NULL, iu);
352        bee_irc_user_nick_update(iu);
353
354        if (online) {
[51a3d12]355                bu->flags |= online;
[5ebff60]356                bee_irc_channel_update(irc, NULL, iu);
[51a3d12]357        }
[5ebff60]358
[badd148]359        return TRUE;
360}
361
[5ebff60]362static gboolean bee_irc_user_nick_update(irc_user_t *iu)
[badd148]363{
364        bee_user_t *bu = iu->bu;
365        char *newnick;
[5ebff60]366
367        if (bu->flags & BEE_USER_ONLINE) {
[6ef9065]368                /* Ignore if the user is visible already. */
369                return TRUE;
[5ebff60]370        }
371
372        if (nick_saved(bu)) {
[6ef9065]373                /* The user already assigned a nickname to this person. */
374                return TRUE;
[5ebff60]375        }
376
377        newnick = nick_get(bu);
378
379        if (strcmp(iu->nick, newnick) != 0) {
380                nick_dedupe(bu, newnick);
381                irc_user_set_nick(iu, newnick);
382        }
383
[6ef9065]384        return TRUE;
385}
386
[5ebff60]387void bee_irc_user_nick_reset(irc_user_t *iu)
[a429907]388{
389        bee_user_t *bu = iu->bu;
390        bee_user_flags_t online;
[5ebff60]391
392        if (bu == FALSE) {
[a429907]393                return;
[5ebff60]394        }
395
[a429907]396        /* In this case, pretend the user is offline. */
[5ebff60]397        if ((online = bu->flags & BEE_USER_ONLINE)) {
[a429907]398                bu->flags &= ~BEE_USER_ONLINE;
[5ebff60]399        }
400
401        nick_del(bu);
402        bee_irc_user_nick_update(iu);
403
[a429907]404        bu->flags |= online;
405}
406
[e4816ea]407/* IRC->IM calls */
408
[5ebff60]409static gboolean bee_irc_user_privmsg_cb(gpointer data, gint fd, b_input_condition cond);
[619dd18]410
[5ebff60]411static gboolean bee_irc_user_privmsg(irc_user_t *iu, const char *msg)
[e4816ea]412{
[1c8e5f7]413        const char *away;
[5ebff60]414
415        if (iu->bu == NULL) {
[e4816ea]416                return FALSE;
[5ebff60]417        }
418
419        if ((away = irc_user_get_away(iu)) &&
420            time(NULL) >= iu->away_reply_timeout) {
421                irc_send_num(iu->irc, 301, "%s :%s", iu->nick, away);
422                iu->away_reply_timeout = time(NULL) +
423                                         set_getint(&iu->irc->b->set, "away_reply_timeout");
424        }
425
426        if (iu->pastebuf == NULL) {
427                iu->pastebuf = g_string_new(msg);
428        } else {
429                b_event_remove(iu->pastebuf_timer);
430                g_string_append_printf(iu->pastebuf, "\n%s", msg);
431        }
432
433        if (set_getbool(&iu->irc->b->set, "paste_buffer")) {
[619dd18]434                int delay;
[5ebff60]435
436                if ((delay = set_getint(&iu->irc->b->set, "paste_buffer_delay")) <= 5) {
[619dd18]437                        delay *= 1000;
[5ebff60]438                }
439
440                iu->pastebuf_timer = b_timeout_add(delay, bee_irc_user_privmsg_cb, iu);
441
[619dd18]442                return TRUE;
[5ebff60]443        } else {
444                bee_irc_user_privmsg_cb(iu, 0, 0);
445
[934db064]446                return TRUE;
447        }
[619dd18]448}
449
[5ebff60]450static gboolean bee_irc_user_privmsg_cb(gpointer data, gint fd, b_input_condition cond)
[619dd18]451{
452        irc_user_t *iu = data;
[911d97a]453        char *msg;
[934db064]454        GSList *l;
[5ebff60]455
456        msg = g_string_free(iu->pastebuf, FALSE);
[911d97a]457        iu->pastebuf = NULL;
458        iu->pastebuf_timer = 0;
[5ebff60]459
460        for (l = irc_plugins; l; l = l->next) {
[934db064]461                irc_plugin_t *p = l->data;
[5ebff60]462                if (p->filter_msg_out) {
463                        char *s = p->filter_msg_out(iu, msg, 0);
464                        if (s) {
465                                if (s != msg) {
466                                        g_free(msg);
467                                }
[934db064]468                                msg = s;
[5ebff60]469                        } else {
[934db064]470                                /* Modules can swallow messages. */
471                                iu->pastebuf = NULL;
[5ebff60]472                                g_free(msg);
[934db064]473                                return FALSE;
474                        }
475                }
476        }
[5ebff60]477
478        bee_user_msg(iu->irc->b, iu->bu, msg, 0);
479
480        g_free(msg);
481
[619dd18]482        return FALSE;
[e4816ea]483}
484
[5ebff60]485static gboolean bee_irc_user_ctcp(irc_user_t *iu, char *const *ctcp)
[e4816ea]486{
[5ebff60]487        if (ctcp[1] && g_strcasecmp(ctcp[0], "DCC") == 0
488            && g_strcasecmp(ctcp[1], "SEND") == 0) {
489                if (iu->bu && iu->bu->ic && iu->bu->ic->acc->prpl->transfer_request) {
490                        file_transfer_t *ft = dcc_request(iu->bu->ic, ctcp);
491                        if (ft) {
492                                iu->bu->ic->acc->prpl->transfer_request(iu->bu->ic, ft, iu->bu->handle);
493                        }
494
[e4816ea]495                        return TRUE;
496                }
[5ebff60]497        } else if (g_strcasecmp(ctcp[0], "TYPING") == 0) {
498                if (iu->bu && iu->bu->ic && iu->bu->ic->acc->prpl->send_typing && ctcp[1]) {
[e4816ea]499                        int st = ctcp[1][0];
[5ebff60]500                        if (st >= '0' && st <= '2') {
[e4816ea]501                                st <<= 8;
[5ebff60]502                                iu->bu->ic->acc->prpl->send_typing(iu->bu->ic, iu->bu->handle, st);
[e4816ea]503                        }
[5ebff60]504
[e4816ea]505                        return TRUE;
506                }
[5ebff60]507        } else if (g_strcasecmp(ctcp[0], "HELP") == 0 && iu->bu) {
508                GString *supp = g_string_new("Supported CTCPs:");
[a97a336]509                GList *l;
[5ebff60]510
511                if (iu->bu->ic && iu->bu->ic->acc->prpl->transfer_request) {
512                        g_string_append(supp, " DCC SEND,");
513                }
514                if (iu->bu->ic && iu->bu->ic->acc->prpl->send_typing) {
515                        g_string_append(supp, " TYPING,");
516                }
517                if (iu->bu->ic->acc->prpl->buddy_action_list) {
518                        for (l = iu->bu->ic->acc->prpl->buddy_action_list(iu->bu); l; l = l->next) {
[a97a336]519                                struct buddy_action *ba = l->data;
[5ebff60]520                                g_string_append_printf(supp, " %s (%s),",
521                                                       ba->name, ba->description);
[a97a336]522                        }
[5ebff60]523                }
524                g_string_truncate(supp, supp->len - 1);
525                irc_send_msg_f(iu, "NOTICE", iu->irc->user->nick, "\001HELP %s\001", supp->str);
526                g_string_free(supp, TRUE);
527        } else if (iu->bu && iu->bu->ic && iu->bu->ic->acc->prpl->buddy_action) {
528                iu->bu->ic->acc->prpl->buddy_action(iu->bu, ctcp[0], ctcp + 1, NULL);
[d88c92a]529        }
[5ebff60]530
[e4816ea]531        return FALSE;
532}
533
534static const struct irc_user_funcs irc_user_im_funcs = {
535        bee_irc_user_privmsg,
536        bee_irc_user_ctcp,
537};
538
[aea8b68]539
[e4816ea]540/* IM->IRC: Groupchats */
[5a75d15]541const struct irc_channel_funcs irc_channel_im_chat_funcs;
[a87754b]542
[5ebff60]543static gboolean bee_irc_chat_new(bee_t *bee, struct groupchat *c)
[aea8b68]544{
545        irc_t *irc = bee->ui_data;
546        irc_channel_t *ic;
547        char *topic;
[eb37735]548        GSList *l;
[aea8b68]549        int i;
[5ebff60]550
[eb37735]551        /* Try to find a channel that expects to receive a groupchat.
[52a2521]552           This flag is set earlier in our current call trace. */
[5ebff60]553        for (l = irc->channels; l; l = l->next) {
[eb37735]554                ic = l->data;
[5ebff60]555                if (ic->flags & IRC_CHANNEL_CHAT_PICKME) {
[eb37735]556                        break;
[5ebff60]557                }
[eb37735]558        }
[5ebff60]559
[eb37735]560        /* If we found none, just generate some stupid name. */
[5ebff60]561        if (l == NULL) {
562                for (i = 0; i <= 999; i++) {
563                        char name[16];
564                        sprintf(name, "#chat_%03d", i);
565                        if ((ic = irc_channel_new(irc, name))) {
566                                break;
567                        }
568                }
[aea8b68]569        }
[5ebff60]570
571        if (ic == NULL) {
[aea8b68]572                return FALSE;
[5ebff60]573        }
574
[aea8b68]575        c->ui_data = ic;
576        ic->data = c;
[5ebff60]577
578        topic = g_strdup_printf(
579                "BitlBee groupchat: \"%s\". Please keep in mind that root-commands won't work here. Have fun!",
580                c->title);
581        irc_channel_set_topic(ic, topic, irc->root);
582        g_free(topic);
583
[aea8b68]584        return TRUE;
585}
586
[5ebff60]587static gboolean bee_irc_chat_free(bee_t *bee, struct groupchat *c)
[aea8b68]588{
589        irc_channel_t *ic = c->ui_data;
[5ebff60]590
591        if (ic == NULL) {
[b1af3e8]592                return FALSE;
[5ebff60]593        }
594
595        if (ic->flags & IRC_CHANNEL_JOINED) {
596                irc_channel_printf(ic, "Cleaning up channel, bye!");
597        }
598
[5a75d15]599        ic->data = NULL;
[6963230]600        c->ui_data = NULL;
[5ebff60]601        irc_channel_del_user(ic, ic->irc->user, IRC_CDU_KICK, "Chatroom closed by server");
602
[aea8b68]603        return TRUE;
604}
605
[5ebff60]606static gboolean bee_irc_chat_log(bee_t *bee, struct groupchat *c, const char *text)
[aea8b68]607{
[27e2c66]608        irc_channel_t *ic = c->ui_data;
[5ebff60]609
610        if (ic == NULL) {
[b1af3e8]611                return FALSE;
[5ebff60]612        }
613
614        irc_channel_printf(ic, "%s", text);
615
[b17ce85]616        return TRUE;
[aea8b68]617}
618
[5ebff60]619static gboolean bee_irc_chat_msg(bee_t *bee, struct groupchat *c, bee_user_t *bu, const char *msg, time_t sent_at)
[aea8b68]620{
[27e2c66]621        irc_t *irc = bee->ui_data;
622        irc_user_t *iu = bu->ui_data;
623        irc_channel_t *ic = c->ui_data;
[172aa37f]624        char *wrapped, *ts = NULL;
[5ebff60]625
626        if (ic == NULL) {
[b1af3e8]627                return FALSE;
[5ebff60]628        }
629
630        if (sent_at > 0 && set_getbool(&bee->set, "display_timestamps")) {
631                ts = irc_format_timestamp(irc, sent_at);
632        }
633
634        wrapped = word_wrap(msg, 425);
635        irc_send_msg(iu, "PRIVMSG", ic->name, wrapped, ts);
636        g_free(ts);
637        g_free(wrapped);
638
[27e2c66]639        return TRUE;
[aea8b68]640}
641
[5ebff60]642static gboolean bee_irc_chat_add_user(bee_t *bee, struct groupchat *c, bee_user_t *bu)
[aea8b68]643{
644        irc_t *irc = bee->ui_data;
[b1af3e8]645        irc_channel_t *ic = c->ui_data;
[5ebff60]646
647        if (ic == NULL) {
[b1af3e8]648                return FALSE;
[5ebff60]649        }
650
651        irc_channel_add_user(ic, bu == bee->user ? irc->user : bu->ui_data);
652
[b17ce85]653        return TRUE;
[aea8b68]654}
655
[5ebff60]656static gboolean bee_irc_chat_remove_user(bee_t *bee, struct groupchat *c, bee_user_t *bu)
[aea8b68]657{
[b17ce85]658        irc_t *irc = bee->ui_data;
[b1af3e8]659        irc_channel_t *ic = c->ui_data;
[5ebff60]660
661        if (ic == NULL || bu == NULL) {
[b1af3e8]662                return FALSE;
[5ebff60]663        }
664
[1c40aa7]665        /* TODO: Possible bug here: If a module removes $user here instead of just
666           using imcb_chat_free() and the channel was IRC_CHANNEL_TEMP, we get into
667           a broken state around here. */
[5ebff60]668        irc_channel_del_user(ic, bu == bee->user ? irc->user : bu->ui_data, IRC_CDU_PART, NULL);
669
[b17ce85]670        return TRUE;
[aea8b68]671}
672
[5ebff60]673static gboolean bee_irc_chat_topic(bee_t *bee, struct groupchat *c, const char *new, bee_user_t *bu)
[9e27f18]674{
[b1af3e8]675        irc_channel_t *ic = c->ui_data;
[9e27f18]676        irc_t *irc = bee->ui_data;
677        irc_user_t *iu;
[5ebff60]678
679        if (ic == NULL) {
[b1af3e8]680                return FALSE;
[5ebff60]681        }
682
683        if (bu == NULL) {
[9e27f18]684                iu = irc->root;
[5ebff60]685        } else if (bu == bee->user) {
[9e27f18]686                iu = irc->user;
[5ebff60]687        } else {
[9e27f18]688                iu = bu->ui_data;
[5ebff60]689        }
690
691        irc_channel_set_topic(ic, new, iu);
692
[9e27f18]693        return TRUE;
694}
695
[5ebff60]696static gboolean bee_irc_chat_name_hint(bee_t *bee, struct groupchat *c, const char *name)
[d343eaa]697{
698        irc_t *irc = bee->ui_data;
[52a2521]699        irc_channel_t *ic = c->ui_data, *oic;
[5ebff60]700        char stripped[MAX_NICK_LENGTH + 1], *full_name;
701
702        if (ic == NULL) {
[b1af3e8]703                return FALSE;
[5ebff60]704        }
705
[d343eaa]706        /* Don't rename a channel if the user's in it already. */
[5ebff60]707        if (ic->flags & IRC_CHANNEL_JOINED) {
[d343eaa]708                return FALSE;
[5ebff60]709        }
710
711        strncpy(stripped, name, MAX_NICK_LENGTH);
[d343eaa]712        stripped[MAX_NICK_LENGTH] = '\0';
[5ebff60]713        irc_channel_name_strip(stripped);
714        if (set_getbool(&bee->set, "lcnicks")) {
715                nick_lc(irc, stripped);
716        }
717
718        if (stripped[0] == '\0') {
[52a2521]719                return FALSE;
[5ebff60]720        }
721
722        full_name = g_strdup_printf("#%s", stripped);
723        if ((oic = irc_channel_by_name(irc, full_name))) {
[52a2521]724                char *type, *chat_type;
[5ebff60]725
726                type = set_getstr(&oic->set, "type");
727                chat_type = set_getstr(&oic->set, "chat_type");
728
729                if (type && chat_type && oic->data == FALSE &&
730                    strcmp(type, "chat") == 0 &&
731                    strcmp(chat_type, "groupchat") == 0) {
[52a2521]732                        /* There's a channel with this name already, but it looks
733                           like it's not in use yet. Most likely the IRC client
734                           rejoined the channel after a reconnect. Remove it so
735                           we can reuse its name. */
[5ebff60]736                        irc_channel_free(oic);
737                } else {
738                        g_free(full_name);
[52a2521]739                        return FALSE;
740                }
[d343eaa]741        }
[5ebff60]742
743        g_free(ic->name);
[52a2521]744        ic->name = full_name;
[5ebff60]745
[d343eaa]746        return TRUE;
747}
748
[5ebff60]749static gboolean bee_irc_chat_invite(bee_t *bee, bee_user_t *bu, const char *name, const char *msg)
[1aa74f55]750{
751        char *channel, *s;
752        irc_t *irc = bee->ui_data;
753        irc_user_t *iu = bu->ui_data;
754        irc_channel_t *chan;
[5ebff60]755
756        if (strchr(CTYPES, name[0])) {
757                channel = g_strdup(name);
758        } else {
759                channel = g_strdup_printf("#%s", name);
760        }
761
762        if ((s = strchr(channel, '@'))) {
[1aa74f55]763                *s = '\0';
[5ebff60]764        }
765
766        if (strlen(channel) > MAX_NICK_LENGTH) {
[1aa74f55]767                /* If the channel name is very long (like those insane GTalk
768                   UUID names), try if we can use the inviter's nick. */
[5ebff60]769                s = g_strdup_printf("#%s", iu->nick);
770                if (irc_channel_by_name(irc, s) == NULL) {
771                        g_free(channel);
[1aa74f55]772                        channel = s;
773                }
774        }
[5ebff60]775
776        if ((chan = irc_channel_new(irc, channel)) &&
777            set_setstr(&chan->set, "type", "chat") &&
778            set_setstr(&chan->set, "chat_type", "room") &&
779            set_setstr(&chan->set, "account", bu->ic->acc->tag) &&
780            set_setstr(&chan->set, "room", (char *) name)) {
[1aa74f55]781                /* I'm assuming that if the user didn't "chat add" the room
782                   himself but got invited, it's temporary, so make this a
783                   temporary mapping that is removed as soon as we /PART. */
784                chan->flags |= IRC_CHANNEL_TEMP;
[5ebff60]785        } else {
786                irc_channel_free(chan);
[1aa74f55]787                chan = NULL;
788        }
[5ebff60]789        g_free(channel);
790
791        irc_send_msg_f(iu, "PRIVMSG", irc->user->nick, "<< \002BitlBee\002 - Invitation to chatroom %s >>", name);
792        if (msg) {
793                irc_send_msg(iu, "PRIVMSG", irc->user->nick, msg, NULL);
794        }
795        if (chan) {
796                irc_send_msg_f(iu, "PRIVMSG", irc->user->nick, "To join the room, just /join %s", chan->name);
797                irc_send_invite(iu, chan);
798        }
799
[1aa74f55]800        return TRUE;
801}
802
[a87754b]803/* IRC->IM */
[5ebff60]804static gboolean bee_irc_channel_chat_privmsg_cb(gpointer data, gint fd, b_input_condition cond);
[619dd18]805
[5ebff60]806static gboolean bee_irc_channel_chat_privmsg(irc_channel_t *ic, const char *msg)
[a87754b]807{
808        struct groupchat *c = ic->data;
[69b896b]809        char *trans = NULL, *s;
[5ebff60]810
811        if (c == NULL) {
[5a75d15]812                return FALSE;
[5ebff60]813        }
814
815        if (set_getbool(&ic->set, "translate_to_nicks")) {
816                char nick[MAX_NICK_LENGTH + 1];
[69b896b]817                irc_user_t *iu;
[5ebff60]818
819                strncpy(nick, msg, MAX_NICK_LENGTH);
[69b896b]820                nick[MAX_NICK_LENGTH] = '\0';
[5ebff60]821                if ((s = strchr(nick, ':')) || (s = strchr(nick, ','))) {
[69b896b]822                        *s = '\0';
[5ebff60]823                        if ((iu = irc_user_by_name(ic->irc, nick)) && iu->bu &&
824                            iu->bu->nick && irc_channel_has_user(ic, iu)) {
825                                trans = g_strconcat(iu->bu->nick, msg + (s - nick), NULL);
[69b896b]826                                msg = trans;
827                        }
828                }
829        }
[5ebff60]830
831        if (set_getbool(&ic->irc->b->set, "paste_buffer")) {
[619dd18]832                int delay;
[5ebff60]833
834                if (ic->pastebuf == NULL) {
835                        ic->pastebuf = g_string_new(msg);
836                } else {
837                        b_event_remove(ic->pastebuf_timer);
838                        g_string_append_printf(ic->pastebuf, "\n%s", msg);
[619dd18]839                }
[5ebff60]840
841                if ((delay = set_getint(&ic->irc->b->set, "paste_buffer_delay")) <= 5) {
[619dd18]842                        delay *= 1000;
[5ebff60]843                }
844
845                ic->pastebuf_timer = b_timeout_add(delay, bee_irc_channel_chat_privmsg_cb, ic);
846
847                g_free(trans);
[619dd18]848                return TRUE;
[5ebff60]849        } else {
850                bee_chat_msg(ic->irc->b, c, msg, 0);
[619dd18]851        }
[5ebff60]852
853        g_free(trans);
[a87754b]854        return TRUE;
[5a75d15]855}
856
[5ebff60]857static gboolean bee_irc_channel_chat_privmsg_cb(gpointer data, gint fd, b_input_condition cond)
[619dd18]858{
859        irc_channel_t *ic = data;
[5ebff60]860
861        if (ic->data) {
862                bee_chat_msg(ic->irc->b, ic->data, ic->pastebuf->str, 0);
863        }
864
865        g_string_free(ic->pastebuf, TRUE);
[619dd18]866        ic->pastebuf = 0;
867        ic->pastebuf_timer = 0;
[5ebff60]868
[619dd18]869        return FALSE;
870}
871
[5ebff60]872static gboolean bee_irc_channel_chat_join(irc_channel_t *ic)
[5a75d15]873{
874        char *acc_s, *room;
875        account_t *acc;
[5ebff60]876
877        if (strcmp(set_getstr(&ic->set, "chat_type"), "room") != 0) {
[5a75d15]878                return TRUE;
[5ebff60]879        }
880
881        if ((acc_s = set_getstr(&ic->set, "account")) &&
882            (room = set_getstr(&ic->set, "room")) &&
883            (acc = account_get(ic->irc->b, acc_s)) &&
884            acc->ic && acc->prpl->chat_join) {
[5a75d15]885                char *nick;
[5ebff60]886
887                if (!(nick = set_getstr(&ic->set, "nick"))) {
[5a75d15]888                        nick = ic->irc->user->nick;
[5ebff60]889                }
890
[5a75d15]891                ic->flags |= IRC_CHANNEL_CHAT_PICKME;
[5ebff60]892                acc->prpl->chat_join(acc->ic, room, nick, NULL, &ic->set);
[5a75d15]893                ic->flags &= ~IRC_CHANNEL_CHAT_PICKME;
[5ebff60]894
[5a75d15]895                return FALSE;
[5ebff60]896        } else {
897                irc_send_num(ic->irc, 403, "%s :Can't join channel, account offline?", ic->name);
[5a75d15]898                return FALSE;
899        }
[a87754b]900}
901
[5ebff60]902static gboolean bee_irc_channel_chat_part(irc_channel_t *ic, const char *msg)
[bfb99ee]903{
904        struct groupchat *c = ic->data;
[5ebff60]905
906        if (c && c->ic->acc->prpl->chat_leave) {
907                c->ic->acc->prpl->chat_leave(c);
908        }
909
[664bac3]910        /* Remove the reference. We don't need it anymore. */
[cc20520]911        ic->data = NULL;
[5ebff60]912
[bfb99ee]913        return TRUE;
914}
915
[5ebff60]916static gboolean bee_irc_channel_chat_topic(irc_channel_t *ic, const char *new)
[9e27f18]917{
[4469e7e]918        struct groupchat *c = ic->data;
[5ebff60]919
920        if (c == NULL) {
[5a75d15]921                return FALSE;
[5ebff60]922        }
923
924        if (c->ic->acc->prpl->chat_topic == NULL) {
925                irc_send_num(ic->irc, 482, "%s :IM network does not support channel topics", ic->name);
926        } else {
[5a75d15]927                /* TODO: Need more const goodness here, sigh */
[5ebff60]928                char *topic = g_strdup(new);
929                c->ic->acc->prpl->chat_topic(c, topic);
930                g_free(topic);
[4469e7e]931        }
[5ebff60]932
[41e0c00]933        /* Whatever happened, the IM module should ack the topic change. */
[4469e7e]934        return FALSE;
[9e27f18]935}
936
[5ebff60]937static gboolean bee_irc_channel_chat_invite(irc_channel_t *ic, irc_user_t *iu)
[66b9e36a]938{
939        struct groupchat *c = ic->data;
[5a75d15]940        bee_user_t *bu = iu->bu;
[5ebff60]941
942        if (bu == NULL) {
[5a75d15]943                return FALSE;
[5ebff60]944        }
945
946        if (c) {
947                if (iu->bu->ic != c->ic) {
948                        irc_send_num(ic->irc, 482, "%s :Can't mix different IM networks in one groupchat", ic->name);
949                } else if (c->ic->acc->prpl->chat_invite) {
950                        c->ic->acc->prpl->chat_invite(c, iu->bu->handle, NULL);
951                } else {
952                        irc_send_num(ic->irc, 482, "%s :IM protocol does not support room invitations", ic->name);
953                }
954        } else if (bu->ic->acc->prpl->chat_with &&
955                   strcmp(set_getstr(&ic->set, "chat_type"), "groupchat") == 0) {
[5a75d15]956                ic->flags |= IRC_CHANNEL_CHAT_PICKME;
[5ebff60]957                iu->bu->ic->acc->prpl->chat_with(bu->ic, bu->handle);
[5a75d15]958                ic->flags &= ~IRC_CHANNEL_CHAT_PICKME;
[5ebff60]959        } else {
960                irc_send_num(ic->irc, 482, "%s :IM protocol does not support room invitations", ic->name);
[5a75d15]961        }
[5ebff60]962
[66b9e36a]963        return TRUE;
964}
965
[5ebff60]966static void bee_irc_channel_chat_kick(irc_channel_t *ic, irc_user_t *iu, const char *msg)
[7821ee8]967{
968        struct groupchat *c = ic->data;
969        bee_user_t *bu = iu->bu;
[5ebff60]970
971        if ((c == NULL) || (bu == NULL)) {
[7821ee8]972                return;
[5ebff60]973        }
974
975        if (!c->ic->acc->prpl->chat_kick) {
976                irc_send_num(ic->irc, 482, "%s :IM protocol does not support room kicking", ic->name);
[7821ee8]977                return;
978        }
[5ebff60]979
980        c->ic->acc->prpl->chat_kick(c, iu->bu->handle, msg);
[7821ee8]981}
982
[5ebff60]983static char *set_eval_room_account(set_t *set, char *value);
984static char *set_eval_chat_type(set_t *set, char *value);
[5a75d15]985
[5ebff60]986static gboolean bee_irc_channel_init(irc_channel_t *ic)
[5a75d15]987{
[57a65600]988        set_t *s;
989
[5ebff60]990        set_add(&ic->set, "account", NULL, set_eval_room_account, ic);
991        set_add(&ic->set, "chat_type", "groupchat", set_eval_chat_type, ic);
[57a65600]992
[5ebff60]993        s = set_add(&ic->set, "nick", NULL, NULL, ic);
[57a65600]994        s->flags |= SET_NULL_OK;
995
[5ebff60]996        set_add(&ic->set, "room", NULL, NULL, ic);
997        set_add(&ic->set, "translate_to_nicks", "true", set_eval_bool, ic);
998
[1c40aa7]999        /* chat_type == groupchat */
1000        ic->flags |= IRC_CHANNEL_TEMP;
[5ebff60]1001
[5a75d15]1002        return TRUE;
1003}
1004
[5ebff60]1005static char *set_eval_room_account(set_t *set, char *value)
[5a75d15]1006{
1007        struct irc_channel *ic = set->data;
[03f3828]1008        account_t *acc, *oa;
[5ebff60]1009
1010        if (!(acc = account_get(ic->irc->b, value))) {
[5a75d15]1011                return SET_INVALID;
[5ebff60]1012        } else if (!acc->prpl->chat_join) {
1013                irc_rootmsg(ic->irc, "Named chatrooms not supported on that account.");
[5a75d15]1014                return SET_INVALID;
1015        }
[5ebff60]1016
1017        if (set->value && (oa = account_get(ic->irc->b, set->value)) &&
1018            oa->prpl->chat_free_settings) {
1019                oa->prpl->chat_free_settings(oa, &ic->set);
1020        }
1021
1022        if (acc->prpl->chat_add_settings) {
1023                acc->prpl->chat_add_settings(acc, &ic->set);
1024        }
1025
1026        return g_strdup(acc->tag);
[5a75d15]1027}
1028
[5ebff60]1029static char *set_eval_chat_type(set_t *set, char *value)
[1c40aa7]1030{
1031        struct irc_channel *ic = set->data;
[5ebff60]1032
1033        if (strcmp(value, "groupchat") == 0) {
[1c40aa7]1034                ic->flags |= IRC_CHANNEL_TEMP;
[5ebff60]1035        } else if (strcmp(value, "room") == 0) {
[1c40aa7]1036                ic->flags &= ~IRC_CHANNEL_TEMP;
[5ebff60]1037        } else {
[1c40aa7]1038                return NULL;
[5ebff60]1039        }
1040
[1c40aa7]1041        return value;
1042}
1043
[5ebff60]1044static gboolean bee_irc_channel_free(irc_channel_t *ic)
[5a75d15]1045{
[b1af3e8]1046        struct groupchat *c = ic->data;
[5ebff60]1047
1048        set_del(&ic->set, "account");
1049        set_del(&ic->set, "chat_type");
1050        set_del(&ic->set, "nick");
1051        set_del(&ic->set, "room");
1052        set_del(&ic->set, "translate_to_nicks");
1053
[1c40aa7]1054        ic->flags &= ~IRC_CHANNEL_TEMP;
[5ebff60]1055
[b1af3e8]1056        /* That one still points at this channel. Don't. */
[5ebff60]1057        if (c) {
[b1af3e8]1058                c->ui_data = NULL;
[5ebff60]1059        }
1060
[5a75d15]1061        return TRUE;
1062}
1063
1064const struct irc_channel_funcs irc_channel_im_chat_funcs = {
[a87754b]1065        bee_irc_channel_chat_privmsg,
[5a75d15]1066        bee_irc_channel_chat_join,
[bfb99ee]1067        bee_irc_channel_chat_part,
[9e27f18]1068        bee_irc_channel_chat_topic,
[66b9e36a]1069        bee_irc_channel_chat_invite,
[7821ee8]1070        bee_irc_channel_chat_kick,
[5a75d15]1071
1072        bee_irc_channel_init,
1073        bee_irc_channel_free,
[a87754b]1074};
1075
[aea8b68]1076
[e4816ea]1077/* IM->IRC: File transfers */
[5ebff60]1078static file_transfer_t *bee_irc_ft_in_start(bee_t *bee, bee_user_t *bu, const char *file_name, size_t file_size)
[17a6ee9]1079{
[5ebff60]1080        return dccs_send_start(bu->ic, (irc_user_t *) bu->ui_data, file_name, file_size);
[17a6ee9]1081}
1082
[5ebff60]1083static gboolean bee_irc_ft_out_start(struct im_connection *ic, file_transfer_t *ft)
[17a6ee9]1084{
[5ebff60]1085        return dccs_recv_start(ft);
[17a6ee9]1086}
1087
[5ebff60]1088static void bee_irc_ft_close(struct im_connection *ic, file_transfer_t *ft)
[17a6ee9]1089{
[5ebff60]1090        return dcc_close(ft);
[17a6ee9]1091}
1092
[5ebff60]1093static void bee_irc_ft_finished(struct im_connection *ic, file_transfer_t *file)
[17a6ee9]1094{
1095        dcc_file_transfer_t *df = file->priv;
1096
[5ebff60]1097        if (file->bytes_transferred >= file->file_size) {
1098                dcc_finish(file);
1099        } else {
[17a6ee9]1100                df->proto_finished = TRUE;
[5ebff60]1101        }
[17a6ee9]1102}
1103
[d860a8d]1104const struct bee_ui_funcs irc_ui_funcs = {
[5c7b45c]1105        bee_irc_imc_connected,
1106        bee_irc_imc_disconnected,
[5ebff60]1107
[81e04e1]1108        bee_irc_user_new,
[d860a8d]1109        bee_irc_user_free,
[1d39159]1110        bee_irc_user_fullname,
[6ef9065]1111        bee_irc_user_nick_hint,
[7e83e8e4]1112        bee_irc_user_group,
[d860a8d]1113        bee_irc_user_status,
[f012a9f]1114        bee_irc_user_msg,
[573dab0]1115        bee_irc_user_typing,
[d88c92a]1116        bee_irc_user_action_response,
[5ebff60]1117
[aea8b68]1118        bee_irc_chat_new,
1119        bee_irc_chat_free,
[27e2c66]1120        bee_irc_chat_log,
1121        bee_irc_chat_msg,
[aea8b68]1122        bee_irc_chat_add_user,
[b17ce85]1123        bee_irc_chat_remove_user,
[9e27f18]1124        bee_irc_chat_topic,
[d343eaa]1125        bee_irc_chat_name_hint,
[1aa74f55]1126        bee_irc_chat_invite,
[5ebff60]1127
[17a6ee9]1128        bee_irc_ft_in_start,
1129        bee_irc_ft_out_start,
1130        bee_irc_ft_close,
1131        bee_irc_ft_finished,
[81e04e1]1132};
Note: See TracBrowser for help on using the repository browser.