ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/ermyth/modules/chanserv/main.C
Revision: 1.9
Committed: Sat Sep 22 14:27:27 2007 UTC (16 years, 8 months ago) by pippijn
Content type: text/plain
Branch: MAIN
CVS Tags: HEAD
Changes since 1.8: +5 -9 lines
Log Message:
split up ermyth into ermyth-modules, libermyth (currently just ermyth-util) and ermyth-core

File Contents

# Content
1 /**
2 * main.C: This file contains the main() routine.
3 *
4 * Copyright © 2007 Pippijn van Steenhoven / The Ermyth Team
5 * Rights to this code are as documented in COPYING.
6 *
7 *
8 * Portions of this file were derived from sources bearing the following license:
9 * Copyright © 2005 Atheme Development Group
10 * Rights to this code are documented in doc/pod/license.pod.
11 *
12 * $Id: main.C,v 1.8 2007-09-16 18:54:43 pippijn Exp $
13 */
14
15 #include <boost/foreach.hpp>
16
17 #include "atheme.h"
18 #include <util/numeric.h>
19 #include <libermyth.h>
20 #include "confparse.h"
21 #include "servers.h"
22 #include <ermyth/module.h>
23 #include <account/myuser.h>
24 #include <account/chanacs.h>
25 #include <account/mychan.h>
26
27 static char const rcsid[] = "$Id: main.C,v 1.8 2007-09-16 18:54:43 pippijn Exp $";
28
29 REGISTER_MODULE ("chanserv/main", false, "The Ermyth Team <http://ermyth.xinutec.org>");
30
31 cmdvec cs_cmdtree;
32 helpvec cs_helptree;
33
34 E list_t mychan;
35
36 static void
37 join_registered (bool all)
38 {
39 mychan_t *mc;
40
41 foreach (mychan_t::pair_type &mp, mychan_t::map)
42 {
43 mc = mp.second;
44 if (!(mc->flags & MC_GUARD))
45 continue;
46
47 if (all)
48 {
49 join (mc->name, chansvs.nick);
50 continue;
51 }
52 else if (mc->chan != NULL && mc->chan->members.count != 0)
53 {
54 join (mc->name, chansvs.nick);
55 continue;
56 }
57 }
58 }
59
60 /* main services client routine */
61 static void
62 chanserv (sourceinfo_t *si, int parc, char *parv[])
63 {
64 mychan_t *mc = NULL;
65 char orig[BUFSIZE];
66 char newargs[BUFSIZE];
67 char *cmd;
68 char *args;
69
70 /* this should never happen */
71 if (parv[parc - 2][0] == '&')
72 {
73 slog (LG_ERROR, "services(): got parv with local channel: %s", parv[0]);
74 return;
75 }
76
77 /* is this a fantasy command? */
78 if (parv[parc - 2][0] == '#')
79 {
80 metadata *md;
81
82 if (chansvs.fantasy == false)
83 {
84 /* *all* fantasy disabled */
85 return;
86 }
87
88 mc = mychan_t::find (parv[parc - 2]);
89 if (!mc)
90 {
91 /* unregistered, NFI how we got this message, but let's leave it alone! */
92 return;
93 }
94
95 md = mc->find_metadata ("disable_fantasy");
96 if (md)
97 {
98 /* fantasy disabled on this channel. don't message them, just bail. */
99 return;
100 }
101 }
102
103 /* make a copy of the original for debugging */
104 strlcpy (orig, parv[parc - 1], BUFSIZE);
105
106 /* lets go through this to get the command */
107 cmd = strtok (parv[parc - 1], " ");
108
109 if (!cmd)
110 return;
111 if (*cmd == '\001')
112 {
113 handle_ctcp_common (si, cmd, strtok (NULL, ""));
114 return;
115 }
116
117 /* take the command through the hash table */
118 if (mc == NULL)
119 command_exec_split (si->service, si, cmd, strtok (NULL, ""), cs_cmdtree);
120 else
121 {
122 if (strlen (cmd) > 2 && (strchr (chansvs.trigger, *cmd) != NULL && isalpha (*++cmd)))
123 {
124 /* XXX not really nice to look up the command twice
125 * -- jilles */
126 if (cs_cmdtree.find (cmd) == NULL)
127 return;
128 if (floodcheck (si->su, si->service->me))
129 return;
130 /* construct <channel> <args> */
131 strlcpy (newargs, parv[parc - 2], sizeof newargs);
132 args = strtok (NULL, "");
133 if (args != NULL)
134 {
135 strlcat (newargs, " ", sizeof newargs);
136 strlcat (newargs, args, sizeof newargs);
137 }
138 /* let the command know it's called as fantasy cmd */
139 si->c = mc->chan;
140 /* fantasy commands are always verbose
141 * (a little ugly but this way we can !set verbose)
142 */
143 mc->flags |= MC_FORCEVERBOSE;
144 command_exec_split (si->service, si, cmd, newargs, cs_cmdtree);
145 mc->flags &= ~MC_FORCEVERBOSE;
146 }
147 else if (!strncasecmp (cmd, chansvs.nick, strlen (chansvs.nick)) && (cmd = strtok (NULL, "")) != NULL)
148 {
149 char *pptr;
150
151 strlcpy (newargs, parv[parc - 2], sizeof newargs);
152 if ((pptr = strchr (cmd, ' ')) != NULL)
153 {
154 *pptr = '\0';
155
156 strlcat (newargs, " ", sizeof newargs);
157 strlcat (newargs, ++pptr, sizeof newargs);
158 }
159
160 if (cs_cmdtree.find (cmd) == NULL)
161 return;
162 if (floodcheck (si->su, si->service->me))
163 return;
164
165 si->c = mc->chan;
166
167 /* fantasy commands are always verbose
168 * (a little ugly but this way we can !set verbose)
169 */
170 mc->flags |= MC_FORCEVERBOSE;
171 command_exec_split (si->service, si, cmd, newargs, cs_cmdtree);
172 mc->flags &= ~MC_FORCEVERBOSE;
173 }
174 }
175 }
176
177 static bool
178 cs_join (chanuser_t *cu)
179 {
180 user_t *u;
181 channel_t *chan;
182 mychan_t *mc;
183 unsigned int flags;
184 bool noop;
185 bool secure;
186 metadata *md;
187 chanacs_t *ca2;
188 char akickreason[120] = "User is banned from this channel", *p;
189
190 if (cu == NULL || is_internal_client (cu->user))
191 return true;
192 u = cu->user;
193 chan = cu->chan;
194
195 /* first check if this is a registered channel at all */
196 mc = mychan_t::find (chan->name);
197 if (mc == NULL)
198 return true;
199
200 if (chan->nummembers == 1 && mc->flags & MC_GUARD)
201 join (chan->name, chansvs.nick);
202
203 flags = chanacs_user_flags (mc, u);
204 noop = mc->flags & MC_NOOP || (u->myuser != NULL && u->myuser->flags & MU_NOOP);
205 /* attempt to deop people recreating channels, if the more
206 * sophisticated mechanism is disabled */
207 secure = mc->flags & MC_SECURE || (!chansvs.changets && chan->nummembers == 1 && chan->ts > NOW - 300);
208
209 /* auto stuff */
210 if ((mc->flags & MC_STAFFONLY) && !has_priv_user (u, PRIV_JOIN_STAFFONLY))
211 {
212 /* Stay on channel if this would empty it -- jilles */
213 if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1))
214 {
215 mc->flags |= MC_INHABIT;
216 if (!(mc->flags & MC_GUARD))
217 join (chan->name, chansvs.nick);
218 }
219 ban (chansvs.me->me, chan, u);
220 remove_ban_exceptions (chansvs.me->me, chan, u);
221 phandler->kick (chansvs.nick, chan->name, u->nick, "You are not authorized to be on this channel");
222 return false;
223 }
224
225 if (flags & CA_AKICK && !(flags & CA_REMOVE))
226 {
227 /* Stay on channel if this would empty it -- jilles */
228 if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1))
229 {
230 mc->flags |= MC_INHABIT;
231 if (!(mc->flags & MC_GUARD))
232 join (chan->name, chansvs.nick);
233 }
234 /* use a user-given ban mask if possible -- jilles */
235 ca2 = chanacs_find_host_by_user (mc, u, CA_AKICK);
236 if (ca2 != NULL)
237 {
238 if (chanban_find (chan, ca2->host, 'b') == NULL)
239 {
240 char str[512];
241
242 chanban_add (chan, ca2->host, 'b');
243 snprintf (str, sizeof str, "+b %s", ca2->host);
244 /* ban immediately */
245 phandler->mode_sts (chansvs.nick, chan, str);
246 }
247 }
248 else
249 {
250 /* XXX this could be done more efficiently */
251 ca2 = chanacs_find (mc, u->myuser, CA_AKICK);
252 ban (chansvs.me->me, chan, u);
253 }
254 remove_ban_exceptions (chansvs.me->me, chan, u);
255 if (ca2 != NULL)
256 {
257 md = ca2->find_metadata ("reason");
258 if (md != NULL && *md->value != '|')
259 {
260 snprintf (akickreason, sizeof akickreason, "Banned: %s", md->value);
261 p = strchr (akickreason, '|');
262 if (p != NULL)
263 *p = '\0';
264 else
265 p = akickreason + strlen (akickreason);
266 /* strip trailing spaces, so as not to
267 * disclose the existence of an oper reason */
268 p--;
269 while (p > akickreason && *p == ' ')
270 p--;
271 p[1] = '\0';
272 }
273 }
274 phandler->kick (chansvs.nick, chan->name, u->nick, akickreason);
275 return false;
276 }
277
278 /* Kick out users who may be recreating channels mlocked +i.
279 * Users with +i flag are allowed to join, as are users matching
280 * an invite exception (the latter only works if the channel already
281 * exists because members are sent before invite exceptions).
282 * Because most networks do not use kick_on_split_riding or
283 * no_create_on_split, do not trust users coming back from splits.
284 * Unfortunately, this kicks users who have been invited by a channel
285 * operator, after a split or services restart.
286 */
287 if (mc->mlock_on & CMODE_INVITE && !(flags & CA_INVITE) && (!(u->server->flags & SF_EOB) || (chan->nummembers <= 2 && (chan->nummembers <= 1 || chanuser_find (chan, chansvs.me->me)))) && (!ircd->invex_mchar || !phandler->next_matching_ban (chan, u, ircd->invex_mchar, chan->bans.head)))
288 {
289 if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1))
290 {
291 mc->flags |= MC_INHABIT;
292 if (!(mc->flags & MC_GUARD))
293 join (chan->name, chansvs.nick);
294 }
295 if (!(chan->modes & CMODE_INVITE))
296 check_modes (mc, true);
297 modestack_flush_channel (chan);
298 phandler->kick (chansvs.nick, chan->name, u->nick, "Invite only channel");
299 return false;
300 }
301
302 /* A user joined and was not kicked; we do not need
303 * to stay on the channel artificially. */
304 if (mc->flags & MC_INHABIT)
305 {
306 mc->flags &= ~MC_INHABIT;
307 if (!(mc->flags & MC_GUARD) && (!config_options.chan || irccasecmp (chan->name, config_options.chan)) && chanuser_find (chan, chansvs.me->me))
308 part (chan->name, chansvs.nick);
309 }
310
311 if (ircd->uses_owner)
312 {
313 if (flags & CA_FOUNDER)
314 {
315 if (flags & CA_AUTOOP && !(noop || cu->modes & ircd->owner_mode))
316 {
317 modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, ircd->owner_mchar[1], CLIENT_NAME (u));
318 cu->modes |= ircd->owner_mode;
319 }
320 }
321 else if (secure && (cu->modes & ircd->owner_mode))
322 {
323 modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, ircd->owner_mchar[1], CLIENT_NAME (u));
324 cu->modes &= ~ircd->owner_mode;
325 }
326 }
327
328 /* XXX still uses should_protect() */
329 if (ircd->uses_protect)
330 {
331 if (u->myuser != NULL && u->myuser->should_protect (mc))
332 {
333 if (flags & CA_AUTOOP && !(noop || cu->modes & ircd->protect_mode))
334 {
335 modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, ircd->protect_mchar[1], CLIENT_NAME (u));
336 cu->modes |= ircd->protect_mode;
337 }
338 }
339 else if (secure && (cu->modes & ircd->protect_mode))
340 {
341 modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, ircd->protect_mchar[1], CLIENT_NAME (u));
342 cu->modes &= ~ircd->protect_mode;
343 }
344 }
345
346 if (flags & CA_AUTOOP)
347 {
348 if (!(noop || cu->modes & CMODE_OP))
349 {
350 modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'o', CLIENT_NAME (u));
351 cu->modes |= CMODE_OP;
352 }
353 }
354 else if (secure && (cu->modes & CMODE_OP) && !(flags & CA_OP))
355 {
356 modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, 'o', CLIENT_NAME (u));
357 cu->modes &= ~CMODE_OP;
358 }
359
360 if (ircd->uses_halfops)
361 {
362 if (flags & CA_AUTOHALFOP)
363 {
364 if (!(noop || cu->modes & (CMODE_OP | ircd->halfops_mode)))
365 {
366 modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'h', CLIENT_NAME (u));
367 cu->modes |= ircd->halfops_mode;
368 }
369 }
370 else if (secure && (cu->modes & ircd->halfops_mode) && !(flags & CA_HALFOP))
371 {
372 modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, 'h', CLIENT_NAME (u));
373 cu->modes &= ~ircd->halfops_mode;
374 }
375 }
376
377 if (flags & CA_AUTOVOICE)
378 {
379 if (!(noop || cu->modes & (CMODE_OP | ircd->halfops_mode | CMODE_VOICE)))
380 {
381 modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'v', CLIENT_NAME (u));
382 cu->modes |= CMODE_VOICE;
383 }
384 }
385
386 if (u->server->flags & SF_EOB && (md = mc->find_metadata ("private:entrymsg")))
387 notice (chansvs.nick, cu->user->nick, "[%s] %s", mc->name, md->value);
388
389 if (u->server->flags & SF_EOB && (md = mc->find_metadata ("url")))
390 phandler->numeric_sts (me.name, 328, cu->user->nick, "%s :%s", mc->name, md->value);
391
392 if (flags & CA_USEDUPDATE)
393 mc->used = NOW;
394
395 return true;
396 }
397
398 static void
399 cs_part (chanuser_t *cu)
400 {
401 mychan_t *mc;
402
403 if (cu == NULL)
404 return;
405 mc = mychan_t::find (cu->chan->name);
406 if (mc == NULL)
407 return;
408 if (NOW - mc->used >= 3600)
409 if (chanacs_user_flags (mc, cu->user) & CA_USEDUPDATE)
410 mc->used = NOW;
411 /*
412 * When channel_part is fired, we haven't yet removed the
413 * user from the room. So, the channel will have two members
414 * if ChanServ is joining channels: the triggering user and
415 * itself.
416 *
417 * Do not part if we're enforcing an akick/close in an otherwise
418 * empty channel (MC_INHABIT). -- jilles
419 */
420 if ((mc->flags & MC_GUARD) && config_options.leave_chans && !(mc->flags & MC_INHABIT) && (cu->chan->nummembers <= 2) && !is_internal_client (cu->user))
421 part (cu->chan->name, chansvs.nick);
422 }
423
424 static void
425 cs_register (mychan_t *mc, sourceinfo_t *si)
426 {
427 if (mc->chan)
428 {
429 if (mc->flags & MC_GUARD)
430 join (mc->name, chansvs.nick);
431
432 check_modes (mc, true);
433 }
434 }
435
436 /* Called on every set of a topic, after updating our internal structures */
437 static void
438 cs_keeptopic_topicset (channel_t *c)
439 {
440 mychan_t *mc;
441 metadata *md;
442
443 mc = mychan_t::find (c->name);
444
445 if (mc == NULL)
446 return;
447
448 md = mc->find_metadata ("private:topic:text");
449 if (md != NULL)
450 {
451 if (c->topic != NULL && !strcmp (md->value, c->topic))
452 return;
453 mc->del_metadata ("private:topic:text");
454 }
455
456 if (mc->find_metadata ("private:topic:setter"))
457 mc->del_metadata ("private:topic:setter");
458
459 if (mc->find_metadata ("private:topic:ts"))
460 mc->del_metadata ("private:topic:ts");
461
462 if (c->topic && c->topic_setter)
463 {
464 slog (LG_DEBUG, "KeepTopic: topic set for %s by %s: %s", c->name, c->topic_setter, c->topic);
465 mc->add_metadata ("private:topic:setter", c->topic_setter);
466 mc->add_metadata ("private:topic:text", c->topic);
467 mc->add_metadata ("private:topic:ts", itoa (c->topicts));
468 }
469 else
470 slog (LG_DEBUG, "KeepTopic: topic cleared for %s", c->name);
471 }
472
473 /* Called when a topic change is received from the network, before altering
474 * our internal structures */
475 static bool
476 cs_topiccheck (channel_t *c, user_t *u, server_t *s, char const * const topic, time_t ts, char const * const setter)
477 {
478 mychan_t *mc = mychan_t::find (c->name);
479 unsigned int accessfl = 0;
480 bool approved = true;
481
482 if (mc == NULL)
483 return false;
484
485 if ((mc->flags & (MC_KEEPTOPIC | MC_TOPICLOCK)) == (MC_KEEPTOPIC | MC_TOPICLOCK))
486 {
487 if (u == NULL || !((accessfl = chanacs_user_flags (mc, u)) & CA_TOPIC))
488 {
489 /* topic burst or unauthorized user, revert it */
490 approved = false;
491 slog (LG_DEBUG, "cs_topiccheck(): reverting topic change on channel %s by %s", c->name, u != NULL ? u->nick : "<server>");
492
493 if (u != NULL && !(mc->mlock_off & CMODE_TOPIC))
494 {
495 /* they do not have access to be opped either,
496 * deop them and set +t */
497 /* note: channel_mode() takes nicks, not UIDs
498 * when used with a non-NULL source */
499 if (ircd->uses_halfops && !(accessfl & (CA_OP | CA_AUTOOP | CA_HALFOP | CA_AUTOHALFOP)))
500 channel_mode_va (chansvs.me->me, c, 3, "+t-oh", u->nick, u->nick);
501 else if (!ircd->uses_halfops && !(accessfl & (CA_OP | CA_AUTOOP)))
502 channel_mode_va (chansvs.me->me, c, 2, "+t-o", u->nick);
503 }
504 }
505 }
506
507 return approved;
508 }
509
510 /* Called on creation of a channel */
511 static void
512 cs_newchan (channel_t *c)
513 {
514 mychan_t *mc;
515 chanuser_t *cu;
516 metadata *md;
517 char *setter;
518 char *text;
519 time_t channelts = 0;
520 time_t topicts;
521 char str[21];
522
523 if (!(mc = mychan_t::find (c->name)))
524 return;
525
526 /* schedule a mode lock check when we know the current modes
527 * -- jilles */
528 mc->flags |= MC_MLOCK_CHECK;
529
530 md = mc->find_metadata ("private:channelts");
531 if (md != NULL)
532 channelts = atol (md->value);
533 if (channelts == 0)
534 channelts = mc->registered;
535
536 if (chansvs.changets && c->ts > channelts && channelts > 0)
537 {
538 /* Stop the splitrider -- jilles */
539 c->ts = channelts;
540 clear_simple_modes (c);
541 c->modes |= CMODE_NOEXT | CMODE_TOPIC;
542 check_modes (mc, false);
543 /* No ops to clear */
544 phandler->chan_lowerts (c, chansvs.me->me);
545 cu = chanuser_add (c, CLIENT_NAME (chansvs.me->me));
546 cu->modes |= CMODE_OP;
547 /* make sure it parts again sometime (empty SJOIN etc) */
548 mc->flags |= MC_INHABIT;
549 }
550 else if (c->ts != channelts)
551 {
552 snprintf (str, sizeof str, "%lu", (unsigned long) c->ts);
553 mc->add_metadata ("private:channelts", str);
554 }
555 else if (!(MC_TOPICLOCK & mc->flags) && LIST_LENGTH (&c->members) == 0)
556 /* Same channel, let's assume ircd has kept topic.
557 * However, if topiclock is enabled, we must change it back
558 * regardless.
559 * Also, if there is someone in this channel already, it is
560 * being created by a service and we must restore.
561 * -- jilles */
562 return;
563
564 if (!(MC_KEEPTOPIC & mc->flags))
565 return;
566
567 md = mc->find_metadata ("private:topic:setter");
568 if (md == NULL)
569 return;
570 setter = md->value;
571
572 md = mc->find_metadata ("private:topic:text");
573 if (md == NULL)
574 return;
575 text = md->value;
576
577 md = mc->find_metadata ("private:topic:ts");
578 if (md == NULL)
579 return;
580 topicts = atol (md->value);
581
582 handle_topic (c, setter, topicts, text);
583 phandler->topic_sts (c, setter, topicts, 0, text);
584 }
585
586 static void
587 cs_tschange (channel_t *c)
588 {
589 mychan_t *mc;
590 char str[21];
591
592 if (!(mc = mychan_t::find (c->name)))
593 return;
594
595 /* store new TS */
596 snprintf (str, sizeof str, "%lu", (unsigned long) c->ts);
597 mc->add_metadata ("private:channelts", str);
598
599 /* schedule a mode lock check when we know the new modes
600 * -- jilles */
601 mc->flags |= MC_MLOCK_CHECK;
602 }
603
604 static void
605 cs_leave_empty (void *unused)
606 {
607 mychan_t *mc;
608
609 foreach (mychan_t::pair_type &mp, mychan_t::map)
610 {
611 mc = mp.second;
612 if (!(mc->flags & MC_INHABIT))
613 continue;
614 mc->flags &= ~MC_INHABIT;
615 if (mc->chan != NULL && (!config_options.chan || irccasecmp (mc->name, config_options.chan)) && (!(mc->flags & MC_GUARD) || (config_options.leave_chans && mc->chan->nummembers == 1) || mc->find_metadata ("private:close:closer")) && chanuser_find (mc->chan, chansvs.me->me))
616 {
617 slog (LG_DEBUG, "cs_leave_empty(): leaving %s", mc->chan->name);
618 part (mc->chan->name, chansvs.nick);
619 }
620 }
621 }
622
623 static void
624 on_config_ready (void)
625 {
626 if (chansvs.me)
627 del_service (chansvs.me);
628
629 chansvs.me = add_service (chansvs.nick, chansvs.user, chansvs.host, chansvs.real, chanserv, cs_cmdtree);
630 chansvs.disp = chansvs.me->disp;
631
632 chansvs.me->set_chanmsg (true);
633
634 if (me.connected)
635 join_registered (!config_options.leave_chans);
636 }
637
638 bool
639 _modinit (module *m)
640 {
641 ConfTable::callback.ready.attach (on_config_ready);
642
643 if (!cold_start)
644 {
645 chansvs.me = add_service (chansvs.nick, chansvs.user, chansvs.host, chansvs.real, chanserv, cs_cmdtree);
646 chansvs.disp = chansvs.me->disp;
647
648 chansvs.me->set_chanmsg (true);
649
650 if (me.connected)
651 join_registered (!config_options.leave_chans);
652 }
653
654 channel_t::callback.add.attach (cs_newchan);
655 chanuser_t::callback.join.attach (cs_join);
656 chanuser_t::callback.part.attach (cs_part);
657 mychan_t::callback.registered.attach (cs_register);
658 channel_t::callback.topic.attach (cs_keeptopic_topicset);
659 channel_t::callback.can_change_topic.attach (cs_topiccheck);
660 channel_t::callback.tschange.attach (cs_tschange);
661 event_add ("cs_leave_empty", cs_leave_empty, NULL, 300);
662
663 return true;
664 }
665
666 void
667 _moddeinit ()
668 {
669 if (chansvs.me)
670 {
671 del_service (chansvs.me);
672 chansvs.me = NULL;
673 }
674
675 channel_t::callback.add.detach (cs_newchan);
676 chanuser_t::callback.join.detach (cs_join);
677 chanuser_t::callback.part.detach (cs_part);
678 mychan_t::callback.registered.detach (cs_register);
679 channel_t::callback.topic.detach (cs_keeptopic_topicset);
680 channel_t::callback.can_change_topic.detach (cs_topiccheck);
681 channel_t::callback.tschange.detach (cs_tschange);
682 event_delete (cs_leave_empty, NULL);
683
684 ConfTable::callback.ready.detach (on_config_ready);
685 }