/** * main.C: This file contains the main() routine. * * Copyright © 2007 Pippijn van Steenhoven / The Ermyth Team * Rights to this code are as documented in COPYING. * * * Portions of this file were derived from sources bearing the following license: * Copyright © 2005 Atheme Development Group * Rights to this code are documented in doc/pod/license.pod. * * $Id: main.C,v 1.9 2007/09/22 14:27:27 pippijn Exp $ */ #include #include "atheme.h" #include #include #include "confparse.h" #include "servers.h" #include #include #include #include static char const rcsid[] = "$Id: main.C,v 1.9 2007/09/22 14:27:27 pippijn Exp $"; REGISTER_MODULE ("chanserv/main", false, "The Ermyth Team "); cmdvec cs_cmdtree; helpvec cs_helptree; E list_t mychan; static void join_registered (bool all) { mychan_t *mc; foreach (mychan_t::pair_type &mp, mychan_t::map) { mc = mp.second; if (!(mc->flags & MC_GUARD)) continue; if (all) { join (mc->name, chansvs.nick); continue; } else if (mc->chan != NULL && mc->chan->members.count != 0) { join (mc->name, chansvs.nick); continue; } } } /* main services client routine */ static void chanserv (sourceinfo_t *si, int parc, char *parv[]) { mychan_t *mc = NULL; char orig[BUFSIZE]; char newargs[BUFSIZE]; char *cmd; char *args; /* this should never happen */ if (parv[parc - 2][0] == '&') { slog (LG_ERROR, "services(): got parv with local channel: %s", parv[0]); return; } /* is this a fantasy command? */ if (parv[parc - 2][0] == '#') { metadata *md; if (chansvs.fantasy == false) { /* *all* fantasy disabled */ return; } mc = mychan_t::find (parv[parc - 2]); if (!mc) { /* unregistered, NFI how we got this message, but let's leave it alone! */ return; } md = mc->find_metadata ("disable_fantasy"); if (md) { /* fantasy disabled on this channel. don't message them, just bail. */ return; } } /* make a copy of the original for debugging */ strlcpy (orig, parv[parc - 1], BUFSIZE); /* lets go through this to get the command */ cmd = strtok (parv[parc - 1], " "); if (!cmd) return; if (*cmd == '\001') { handle_ctcp_common (si, cmd, strtok (NULL, "")); return; } /* take the command through the hash table */ if (mc == NULL) command_exec_split (si->service, si, cmd, strtok (NULL, ""), cs_cmdtree); else { if (strlen (cmd) > 2 && (strchr (chansvs.trigger, *cmd) != NULL && isalpha (*++cmd))) { /* XXX not really nice to look up the command twice * -- jilles */ if (cs_cmdtree.find (cmd) == NULL) return; if (floodcheck (si->su, si->service->me)) return; /* construct */ strlcpy (newargs, parv[parc - 2], sizeof newargs); args = strtok (NULL, ""); if (args != NULL) { strlcat (newargs, " ", sizeof newargs); strlcat (newargs, args, sizeof newargs); } /* let the command know it's called as fantasy cmd */ si->c = mc->chan; /* fantasy commands are always verbose * (a little ugly but this way we can !set verbose) */ mc->flags |= MC_FORCEVERBOSE; command_exec_split (si->service, si, cmd, newargs, cs_cmdtree); mc->flags &= ~MC_FORCEVERBOSE; } else if (!strncasecmp (cmd, chansvs.nick, strlen (chansvs.nick)) && (cmd = strtok (NULL, "")) != NULL) { char *pptr; strlcpy (newargs, parv[parc - 2], sizeof newargs); if ((pptr = strchr (cmd, ' ')) != NULL) { *pptr = '\0'; strlcat (newargs, " ", sizeof newargs); strlcat (newargs, ++pptr, sizeof newargs); } if (cs_cmdtree.find (cmd) == NULL) return; if (floodcheck (si->su, si->service->me)) return; si->c = mc->chan; /* fantasy commands are always verbose * (a little ugly but this way we can !set verbose) */ mc->flags |= MC_FORCEVERBOSE; command_exec_split (si->service, si, cmd, newargs, cs_cmdtree); mc->flags &= ~MC_FORCEVERBOSE; } } } static bool cs_join (chanuser_t *cu) { user_t *u; channel_t *chan; mychan_t *mc; unsigned int flags; bool noop; bool secure; metadata *md; chanacs_t *ca2; char akickreason[120] = "User is banned from this channel", *p; if (cu == NULL || is_internal_client (cu->user)) return true; u = cu->user; chan = cu->chan; /* first check if this is a registered channel at all */ mc = mychan_t::find (chan->name); if (mc == NULL) return true; if (chan->nummembers == 1 && mc->flags & MC_GUARD) join (chan->name, chansvs.nick); flags = chanacs_user_flags (mc, u); noop = mc->flags & MC_NOOP || (u->myuser != NULL && u->myuser->flags & MU_NOOP); /* attempt to deop people recreating channels, if the more * sophisticated mechanism is disabled */ secure = mc->flags & MC_SECURE || (!chansvs.changets && chan->nummembers == 1 && chan->ts > NOW - 300); /* auto stuff */ if ((mc->flags & MC_STAFFONLY) && !has_priv_user (u, PRIV_JOIN_STAFFONLY)) { /* Stay on channel if this would empty it -- jilles */ if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1)) { mc->flags |= MC_INHABIT; if (!(mc->flags & MC_GUARD)) join (chan->name, chansvs.nick); } ban (chansvs.me->me, chan, u); remove_ban_exceptions (chansvs.me->me, chan, u); phandler->kick (chansvs.nick, chan->name, u->nick, "You are not authorized to be on this channel"); return false; } if (flags & CA_AKICK && !(flags & CA_REMOVE)) { /* Stay on channel if this would empty it -- jilles */ if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1)) { mc->flags |= MC_INHABIT; if (!(mc->flags & MC_GUARD)) join (chan->name, chansvs.nick); } /* use a user-given ban mask if possible -- jilles */ ca2 = chanacs_find_host_by_user (mc, u, CA_AKICK); if (ca2 != NULL) { if (chanban_find (chan, ca2->host, 'b') == NULL) { char str[512]; chanban_add (chan, ca2->host, 'b'); snprintf (str, sizeof str, "+b %s", ca2->host); /* ban immediately */ phandler->mode_sts (chansvs.nick, chan, str); } } else { /* XXX this could be done more efficiently */ ca2 = chanacs_find (mc, u->myuser, CA_AKICK); ban (chansvs.me->me, chan, u); } remove_ban_exceptions (chansvs.me->me, chan, u); if (ca2 != NULL) { md = ca2->find_metadata ("reason"); if (md != NULL && *md->value != '|') { snprintf (akickreason, sizeof akickreason, "Banned: %s", md->value); p = strchr (akickreason, '|'); if (p != NULL) *p = '\0'; else p = akickreason + strlen (akickreason); /* strip trailing spaces, so as not to * disclose the existence of an oper reason */ p--; while (p > akickreason && *p == ' ') p--; p[1] = '\0'; } } phandler->kick (chansvs.nick, chan->name, u->nick, akickreason); return false; } /* Kick out users who may be recreating channels mlocked +i. * Users with +i flag are allowed to join, as are users matching * an invite exception (the latter only works if the channel already * exists because members are sent before invite exceptions). * Because most networks do not use kick_on_split_riding or * no_create_on_split, do not trust users coming back from splits. * Unfortunately, this kicks users who have been invited by a channel * operator, after a split or services restart. */ 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))) { if (chan->nummembers <= (mc->flags & MC_GUARD ? 2 : 1)) { mc->flags |= MC_INHABIT; if (!(mc->flags & MC_GUARD)) join (chan->name, chansvs.nick); } if (!(chan->modes & CMODE_INVITE)) check_modes (mc, true); modestack_flush_channel (chan); phandler->kick (chansvs.nick, chan->name, u->nick, "Invite only channel"); return false; } /* A user joined and was not kicked; we do not need * to stay on the channel artificially. */ if (mc->flags & MC_INHABIT) { mc->flags &= ~MC_INHABIT; if (!(mc->flags & MC_GUARD) && (!config_options.chan || irccasecmp (chan->name, config_options.chan)) && chanuser_find (chan, chansvs.me->me)) part (chan->name, chansvs.nick); } if (ircd->uses_owner) { if (flags & CA_FOUNDER) { if (flags & CA_AUTOOP && !(noop || cu->modes & ircd->owner_mode)) { modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, ircd->owner_mchar[1], CLIENT_NAME (u)); cu->modes |= ircd->owner_mode; } } else if (secure && (cu->modes & ircd->owner_mode)) { modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, ircd->owner_mchar[1], CLIENT_NAME (u)); cu->modes &= ~ircd->owner_mode; } } /* XXX still uses should_protect() */ if (ircd->uses_protect) { if (u->myuser != NULL && u->myuser->should_protect (mc)) { if (flags & CA_AUTOOP && !(noop || cu->modes & ircd->protect_mode)) { modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, ircd->protect_mchar[1], CLIENT_NAME (u)); cu->modes |= ircd->protect_mode; } } else if (secure && (cu->modes & ircd->protect_mode)) { modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, ircd->protect_mchar[1], CLIENT_NAME (u)); cu->modes &= ~ircd->protect_mode; } } if (flags & CA_AUTOOP) { if (!(noop || cu->modes & CMODE_OP)) { modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'o', CLIENT_NAME (u)); cu->modes |= CMODE_OP; } } else if (secure && (cu->modes & CMODE_OP) && !(flags & CA_OP)) { modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, 'o', CLIENT_NAME (u)); cu->modes &= ~CMODE_OP; } if (ircd->uses_halfops) { if (flags & CA_AUTOHALFOP) { if (!(noop || cu->modes & (CMODE_OP | ircd->halfops_mode))) { modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'h', CLIENT_NAME (u)); cu->modes |= ircd->halfops_mode; } } else if (secure && (cu->modes & ircd->halfops_mode) && !(flags & CA_HALFOP)) { modestack_mode_param (chansvs.nick, chan, MTYPE_DEL, 'h', CLIENT_NAME (u)); cu->modes &= ~ircd->halfops_mode; } } if (flags & CA_AUTOVOICE) { if (!(noop || cu->modes & (CMODE_OP | ircd->halfops_mode | CMODE_VOICE))) { modestack_mode_param (chansvs.nick, chan, MTYPE_ADD, 'v', CLIENT_NAME (u)); cu->modes |= CMODE_VOICE; } } if (u->server->flags & SF_EOB && (md = mc->find_metadata ("private:entrymsg"))) notice (chansvs.nick, cu->user->nick, "[%s] %s", mc->name, md->value); if (u->server->flags & SF_EOB && (md = mc->find_metadata ("url"))) phandler->numeric_sts (me.name, 328, cu->user->nick, "%s :%s", mc->name, md->value); if (flags & CA_USEDUPDATE) mc->used = NOW; return true; } static void cs_part (chanuser_t *cu) { mychan_t *mc; if (cu == NULL) return; mc = mychan_t::find (cu->chan->name); if (mc == NULL) return; if (NOW - mc->used >= 3600) if (chanacs_user_flags (mc, cu->user) & CA_USEDUPDATE) mc->used = NOW; /* * When channel_part is fired, we haven't yet removed the * user from the room. So, the channel will have two members * if ChanServ is joining channels: the triggering user and * itself. * * Do not part if we're enforcing an akick/close in an otherwise * empty channel (MC_INHABIT). -- jilles */ if ((mc->flags & MC_GUARD) && config_options.leave_chans && !(mc->flags & MC_INHABIT) && (cu->chan->nummembers <= 2) && !is_internal_client (cu->user)) part (cu->chan->name, chansvs.nick); } static void cs_register (mychan_t *mc, sourceinfo_t *si) { if (mc->chan) { if (mc->flags & MC_GUARD) join (mc->name, chansvs.nick); check_modes (mc, true); } } /* Called on every set of a topic, after updating our internal structures */ static void cs_keeptopic_topicset (channel_t *c) { mychan_t *mc; metadata *md; mc = mychan_t::find (c->name); if (mc == NULL) return; md = mc->find_metadata ("private:topic:text"); if (md != NULL) { if (c->topic != NULL && !strcmp (md->value, c->topic)) return; mc->del_metadata ("private:topic:text"); } if (mc->find_metadata ("private:topic:setter")) mc->del_metadata ("private:topic:setter"); if (mc->find_metadata ("private:topic:ts")) mc->del_metadata ("private:topic:ts"); if (c->topic && c->topic_setter) { slog (LG_DEBUG, "KeepTopic: topic set for %s by %s: %s", c->name, c->topic_setter, c->topic); mc->add_metadata ("private:topic:setter", c->topic_setter); mc->add_metadata ("private:topic:text", c->topic); mc->add_metadata ("private:topic:ts", itoa (c->topicts)); } else slog (LG_DEBUG, "KeepTopic: topic cleared for %s", c->name); } /* Called when a topic change is received from the network, before altering * our internal structures */ static bool cs_topiccheck (channel_t *c, user_t *u, server_t *s, char const * const topic, time_t ts, char const * const setter) { mychan_t *mc = mychan_t::find (c->name); unsigned int accessfl = 0; bool approved = true; if (mc == NULL) return false; if ((mc->flags & (MC_KEEPTOPIC | MC_TOPICLOCK)) == (MC_KEEPTOPIC | MC_TOPICLOCK)) { if (u == NULL || !((accessfl = chanacs_user_flags (mc, u)) & CA_TOPIC)) { /* topic burst or unauthorized user, revert it */ approved = false; slog (LG_DEBUG, "cs_topiccheck(): reverting topic change on channel %s by %s", c->name, u != NULL ? u->nick : ""); if (u != NULL && !(mc->mlock_off & CMODE_TOPIC)) { /* they do not have access to be opped either, * deop them and set +t */ /* note: channel_mode() takes nicks, not UIDs * when used with a non-NULL source */ if (ircd->uses_halfops && !(accessfl & (CA_OP | CA_AUTOOP | CA_HALFOP | CA_AUTOHALFOP))) channel_mode_va (chansvs.me->me, c, 3, "+t-oh", u->nick, u->nick); else if (!ircd->uses_halfops && !(accessfl & (CA_OP | CA_AUTOOP))) channel_mode_va (chansvs.me->me, c, 2, "+t-o", u->nick); } } } return approved; } /* Called on creation of a channel */ static void cs_newchan (channel_t *c) { mychan_t *mc; chanuser_t *cu; metadata *md; char *setter; char *text; time_t channelts = 0; time_t topicts; char str[21]; if (!(mc = mychan_t::find (c->name))) return; /* schedule a mode lock check when we know the current modes * -- jilles */ mc->flags |= MC_MLOCK_CHECK; md = mc->find_metadata ("private:channelts"); if (md != NULL) channelts = atol (md->value); if (channelts == 0) channelts = mc->registered; if (chansvs.changets && c->ts > channelts && channelts > 0) { /* Stop the splitrider -- jilles */ c->ts = channelts; clear_simple_modes (c); c->modes |= CMODE_NOEXT | CMODE_TOPIC; check_modes (mc, false); /* No ops to clear */ phandler->chan_lowerts (c, chansvs.me->me); cu = chanuser_add (c, CLIENT_NAME (chansvs.me->me)); cu->modes |= CMODE_OP; /* make sure it parts again sometime (empty SJOIN etc) */ mc->flags |= MC_INHABIT; } else if (c->ts != channelts) { snprintf (str, sizeof str, "%lu", (unsigned long) c->ts); mc->add_metadata ("private:channelts", str); } else if (!(MC_TOPICLOCK & mc->flags) && LIST_LENGTH (&c->members) == 0) /* Same channel, let's assume ircd has kept topic. * However, if topiclock is enabled, we must change it back * regardless. * Also, if there is someone in this channel already, it is * being created by a service and we must restore. * -- jilles */ return; if (!(MC_KEEPTOPIC & mc->flags)) return; md = mc->find_metadata ("private:topic:setter"); if (md == NULL) return; setter = md->value; md = mc->find_metadata ("private:topic:text"); if (md == NULL) return; text = md->value; md = mc->find_metadata ("private:topic:ts"); if (md == NULL) return; topicts = atol (md->value); handle_topic (c, setter, topicts, text); phandler->topic_sts (c, setter, topicts, 0, text); } static void cs_tschange (channel_t *c) { mychan_t *mc; char str[21]; if (!(mc = mychan_t::find (c->name))) return; /* store new TS */ snprintf (str, sizeof str, "%lu", (unsigned long) c->ts); mc->add_metadata ("private:channelts", str); /* schedule a mode lock check when we know the new modes * -- jilles */ mc->flags |= MC_MLOCK_CHECK; } static void cs_leave_empty (void *unused) { mychan_t *mc; foreach (mychan_t::pair_type &mp, mychan_t::map) { mc = mp.second; if (!(mc->flags & MC_INHABIT)) continue; mc->flags &= ~MC_INHABIT; 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)) { slog (LG_DEBUG, "cs_leave_empty(): leaving %s", mc->chan->name); part (mc->chan->name, chansvs.nick); } } } static void on_config_ready (void) { if (chansvs.me) del_service (chansvs.me); chansvs.me = add_service (chansvs.nick, chansvs.user, chansvs.host, chansvs.real, chanserv, cs_cmdtree); chansvs.disp = chansvs.me->disp; chansvs.me->set_chanmsg (true); if (me.connected) join_registered (!config_options.leave_chans); } bool _modinit (module *m) { ConfTable::callback.ready.attach (on_config_ready); if (!cold_start) { chansvs.me = add_service (chansvs.nick, chansvs.user, chansvs.host, chansvs.real, chanserv, cs_cmdtree); chansvs.disp = chansvs.me->disp; chansvs.me->set_chanmsg (true); if (me.connected) join_registered (!config_options.leave_chans); } channel_t::callback.add.attach (cs_newchan); chanuser_t::callback.join.attach (cs_join); chanuser_t::callback.part.attach (cs_part); mychan_t::callback.registered.attach (cs_register); channel_t::callback.topic.attach (cs_keeptopic_topicset); channel_t::callback.can_change_topic.attach (cs_topiccheck); channel_t::callback.tschange.attach (cs_tschange); event_add ("cs_leave_empty", cs_leave_empty, NULL, 300); return true; } void _moddeinit () { if (chansvs.me) { del_service (chansvs.me); chansvs.me = NULL; } channel_t::callback.add.detach (cs_newchan); chanuser_t::callback.join.detach (cs_join); chanuser_t::callback.part.detach (cs_part); mychan_t::callback.registered.detach (cs_register); channel_t::callback.topic.detach (cs_keeptopic_topicset); channel_t::callback.can_change_topic.detach (cs_topiccheck); channel_t::callback.tschange.detach (cs_tschange); event_delete (cs_leave_empty, NULL); ConfTable::callback.ready.detach (on_config_ready); }