source: irc_im.c @ 727a68b

Last change on this file since 727a68b was 30093fa, checked in by dequis <dx@…>, at 2016-11-12T02:30:32Z

otr: word_wrap long system messages ("unencrypted message received")

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