source: irc_im.c @ 73e2b69

Last change on this file since 73e2b69 was 7801298, checked in by dequis <dx@…>, at 2016-12-27T17:24:50Z

Per-account handle_unknown

Credit for the idea goes to russian XMPP spammers. Thanks!

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