/* BotServ functions * * (C) 2003-2011 Anope Team * Contact us at team@anope.org * * Please read COPYING and README for further details. * * Based on the original code of Epona by Lara. * Based on the original code of Services by Andy Church. */ /*************************************************************************/ #include "services.h" #include "modules.h" static UserData *get_user_data(Channel *c, User *u); E void moduleAddBotServCmds(); /*************************************************************************/ void moduleAddBotServCmds() { ModuleManager::LoadModuleList(Config->BotServCoreModules); } /*************************************************************************/ /*************************************************************************/ /* Return information on memory use. Assumes pointers are valid. */ void get_botserv_stats(long *nrec, long *memuse) { long count = 0, mem = 0; for (Anope::insensitive_map::const_iterator it = BotListByNick.begin(), it_end = BotListByNick.end(); it != it_end; ++it) { BotInfo *bi = it->second; ++count; mem += sizeof(*bi); mem += bi->nick.length() + 1; mem += bi->GetIdent().length() + 1; mem += bi->host.length() + 1; mem += bi->realname.length() + 1; } *nrec = count; *memuse = mem; } /*************************************************************************/ /*************************************************************************/ /* BotServ initialization. */ void bs_init() { if (!Config->s_BotServ.empty()) moduleAddBotServCmds(); } /*************************************************************************/ /* Handles all messages that are sent to registered channels where a * bot is on. */ void botchanmsgs(User *u, ChannelInfo *ci, const Anope::string &buf) { if (!u || !ci || !ci->c || buf.empty()) return; /* Answer to ping if needed */ if (buf.substr(0, 6).equals_ci("\1PING ") && buf[buf.length() - 1] == '\1') { Anope::string ctcp = buf; ctcp.erase(ctcp.begin()); ctcp.erase(ctcp.length() - 1); ircdproto->SendCTCP(ci->bi, u->nick, "%s", ctcp.c_str()); } bool was_action = false; Anope::string realbuf = buf; /* If it's a /me, cut the CTCP part because the ACTION will cause * problems with the caps or badwords kicker */ if (realbuf.substr(0, 8).equals_ci("\1ACTION ") && realbuf[realbuf.length() - 1] == '\1') { realbuf.erase(0, 8); realbuf.erase(realbuf.length() - 1); was_action = true; } if (realbuf.empty()) return; /* Now we can make kicker stuff. We try to order the checks * from the fastest one to the slowest one, since there's * no need to process other kickers if a user is kicked before * the last kicker check. * * But FIRST we check whether the user is protected in any * way. */ bool Allow = true; if (check_access(u, ci, CA_NOKICK)) Allow = false; else if (ci->botflags.HasFlag(BS_DONTKICKOPS) && (ci->c->HasUserStatus(u, CMODE_HALFOP) || ci->c->HasUserStatus(u, CMODE_OP) || ci->c->HasUserStatus(u, CMODE_PROTECT) || ci->c->HasUserStatus(u, CMODE_OWNER))) Allow = false; else if (ci->botflags.HasFlag(BS_DONTKICKVOICES) && ci->c->HasUserStatus(u, CMODE_VOICE)) Allow = false; EventReturn MOD_RESULT; FOREACH_RESULT(I_OnPrivmsg, OnPrivmsg(u, ci, realbuf, Allow)); if (MOD_RESULT == EVENT_STOP) return; if (Allow) { /* Bolds kicker */ if (ci->botflags.HasFlag(BS_KICK_BOLDS) && realbuf.find(2) != Anope::string::npos) { check_ban(ci, u, TTB_BOLDS); bot_kick(ci, u, _("Don't use bolds on this channel!")); return; } /* Color kicker */ if (ci->botflags.HasFlag(BS_KICK_COLORS) && realbuf.find(3) != Anope::string::npos) { check_ban(ci, u, TTB_COLORS); bot_kick(ci, u, _("Don't use colors on this channel!")); return; } /* Reverses kicker */ if (ci->botflags.HasFlag(BS_KICK_REVERSES) && realbuf.find(22) != Anope::string::npos) { check_ban(ci, u, TTB_REVERSES); bot_kick(ci, u, _("Don't use reverses on this channel!")); return; } /* Italics kicker */ if (ci->botflags.HasFlag(BS_KICK_ITALICS) && realbuf.find(29) != Anope::string::npos) { check_ban(ci, u, TTB_ITALICS); bot_kick(ci, u, _("Don't use italics on this channel!")); return; } /* Underlines kicker */ if (ci->botflags.HasFlag(BS_KICK_UNDERLINES) && realbuf.find(31) != Anope::string::npos) { check_ban(ci, u, TTB_UNDERLINES); bot_kick(ci, u, _("Don't use underlines on this channel!")); return; } /* Caps kicker */ if (ci->botflags.HasFlag(BS_KICK_CAPS) && realbuf.length() >= ci->capsmin) { int i = 0, l = 0; for (unsigned j = 0, end = realbuf.length(); j < end; ++j) { if (isupper(realbuf[j])) ++i; else if (islower(realbuf[j])) ++l; } /* i counts uppercase chars, l counts lowercase chars. Only * alphabetic chars (so islower || isupper) qualify for the * percentage of caps to kick for; the rest is ignored. -GD */ if ((i || l) && i >= ci->capsmin && i * 100 / (i + l) >= ci->capspercent) { check_ban(ci, u, TTB_CAPS); bot_kick(ci, u, _("Turn caps lock OFF!")); return; } } /* Bad words kicker */ if (ci->botflags.HasFlag(BS_KICK_BADWORDS)) { bool mustkick = false; /* Normalize the buffer */ Anope::string nbuf = normalizeBuffer(realbuf); for (unsigned i = 0, end = ci->GetBadWordCount(); i < end; ++i) { BadWord *bw = ci->GetBadWord(i); if (bw->type == BW_ANY && ((Config->BSCaseSensitive && nbuf.find(bw->word) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(bw->word) != Anope::string::npos))) mustkick = true; else if (bw->type == BW_SINGLE) { size_t len = bw->word.length(); if ((Config->BSCaseSensitive && bw->word.equals_cs(nbuf)) || (!Config->BSCaseSensitive && bw->word.equals_ci(nbuf))) mustkick = true; else if (nbuf.find(' ') == len && ((Config->BSCaseSensitive && bw->word.equals_cs(nbuf)) || (!Config->BSCaseSensitive && bw->word.equals_ci(nbuf)))) mustkick = true; else { if (nbuf.rfind(' ') == nbuf.length() - len - 1 && ((Config->BSCaseSensitive && nbuf.find(bw->word) == nbuf.length() - len) || (!Config->BSCaseSensitive && nbuf.find_ci(bw->word) == nbuf.length() - len))) mustkick = true; else { Anope::string wordbuf = " " + bw->word + " "; if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } } else if (bw->type == BW_START) { size_t len = bw->word.length(); if ((Config->BSCaseSensitive && nbuf.substr(0, len).equals_cs(bw->word)) || (!Config->BSCaseSensitive && nbuf.substr(0, len).equals_ci(bw->word))) mustkick = true; else { Anope::string wordbuf = " " + bw->word; if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } else if (bw->type == BW_END) { size_t len = bw->word.length(); if ((Config->BSCaseSensitive && nbuf.substr(nbuf.length() - len).equals_cs(bw->word)) || (!Config->BSCaseSensitive && nbuf.substr(nbuf.length() - len).equals_ci(bw->word))) mustkick = true; else { Anope::string wordbuf = bw->word + " "; if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos)) mustkick = true; } } if (mustkick) { check_ban(ci, u, TTB_BADWORDS); if (Config->BSGentleBWReason) bot_kick(ci, u, _("Watch your language!")); else bot_kick(ci, u, _("Don't use the word \"%s\" on this channel!"), bw->word.c_str()); return; } } } /* Flood kicker */ if (ci->botflags.HasFlag(BS_KICK_FLOOD)) { UserData *ud = get_user_data(ci->c, u); if (!ud) return; if (Anope::CurTime - ud->last_start > ci->floodsecs) { ud->last_start = Anope::CurTime; ud->lines = 0; } ++ud->lines; if (ud->lines >= ci->floodlines) { check_ban(ci, u, TTB_FLOOD); bot_kick(ci, u, _("Stop flooding!")); return; } } /* Repeat kicker */ if (ci->botflags.HasFlag(BS_KICK_REPEAT)) { UserData *ud = get_user_data(ci->c, u); if (!ud) return; if (!ud->lastline.empty() && !ud->lastline.equals_ci(buf)) { ud->lastline = buf; ud->times = 0; } else { if (ud->lastline.empty()) ud->lastline = buf; ++ud->times; } if (ud->times >= ci->repeattimes) { check_ban(ci, u, TTB_REPEAT); bot_kick(ci, u, _("Stop repeating yourself!")); return; } } } /* Fantaisist commands */ if (ci->botflags.HasFlag(BS_FANTASY) && buf[0] == Config->BSFantasyCharacter[0] && !was_action) { Anope::string message = buf; /* Strip off the fantasy character */ message.erase(message.begin()); size_t space = message.find(' '); Anope::string command, rest; if (space == Anope::string::npos) command = message; else { command = message.substr(0, space); rest = message.substr(space + 1); } if (check_access(u, ci, CA_FANTASIA)) { Command *cmd = FindCommand(ChanServ, command); /* Command exists and can be called by fantasy */ if (cmd && !cmd->HasFlag(CFLAG_DISABLE_FANTASY)) { Anope::string params = rest; /* Some commands don't need the channel name added.. eg !help */ if (!cmd->HasFlag(CFLAG_STRIP_CHANNEL)) params = ci->name + " " + params; params = command + " " + params; mod_run_cmd(ChanServ, u, ci, params); } FOREACH_MOD(I_OnBotFantasy, OnBotFantasy(command, u, ci, rest)); } else { FOREACH_MOD(I_OnBotNoFantasyAccess, OnBotNoFantasyAccess(command, u, ci, rest)); } } } /*************************************************************************/ BotInfo *findbot(const Anope::string &nick) { BotInfo *bi = NULL; if (isdigit(nick[0]) && ircd->ts6) { Anope::map::iterator it = BotListByUID.find(nick); if (it != BotListByUID.end()) bi = it->second; } else { Anope::insensitive_map::iterator it = BotListByNick.find(nick); if (it != BotListByNick.end()) bi = it->second; } FOREACH_MOD(I_OnFindBot, OnFindBot(nick)); return bi; } /*************************************************************************/ /* Returns ban data associated with a user if it exists, allocates it otherwise. */ static BanData *get_ban_data(Channel *c, User *u) { if (!c || !u) return NULL; Anope::string mask = u->GetIdent() + "@" + u->GetDisplayedHost(); /* XXX This should really be on some sort of timer/garbage collector, and use std::map */ for (std::list::iterator it = c->bd.begin(), it_end = c->bd.end(), it_next; it != it_end; it = it_next) { it_next = it; ++it_next; if (Anope::CurTime - (*it)->last_use > Config->BSKeepData) { delete *it; c->bd.erase(it); continue; } if ((*it)->mask.equals_ci(mask)) { (*it)->last_use = Anope::CurTime; return *it; } } /* If we fall here it is that we haven't found the record */ BanData *bd = new BanData(); bd->mask = mask; bd->last_use = Anope::CurTime; for (int x = 0; x < TTB_SIZE; ++x) bd->ttb[x] = 0; c->bd.push_front(bd); return bd; } /*************************************************************************/ /* Returns BotServ data associated with a user on a given channel. * Allocates it if necessary. */ static UserData *get_user_data(Channel *c, User *u) { if (!c || !u) return NULL; for (CUserList::iterator it = c->users.begin(), it_end = c->users.end(); it != it_end; ++it) { UserContainer *uc = *it; if (uc->user == u) { /* Checks whether data is obsolete */ if (Anope::CurTime - uc->ud.last_use > Config->BSKeepData) { /* We should not free and realloc, but reset to 0 instead. */ uc->ud.Clear(); uc->ud.last_use = Anope::CurTime; } return &uc->ud; } } return NULL; } /*************************************************************************/ /** Check if a user should be banned by botserv * @param ci The channel the user is on * @param u The user * @param ttbtype The type of bot kicker the user should be checked against */ void check_ban(ChannelInfo *ci, User *u, int ttbtype) { BanData *bd = get_ban_data(ci->c, u); if (!bd) return; /* Don't ban ulines */ if (u->server->IsULined()) return; ++bd->ttb[ttbtype]; if (ci->ttb[ttbtype] && bd->ttb[ttbtype] >= ci->ttb[ttbtype]) { /* Should not use == here because bd->ttb[ttbtype] could possibly be > ci->ttb[ttbtype] * if the TTB was changed after it was not set (0) before and the user had already been * kicked a few times. Bug #1056 - Adam */ Anope::string mask; bd->ttb[ttbtype] = 0; get_idealban(ci, u, mask); if (ci->c) ci->c->SetMode(NULL, CMODE_BAN, mask); FOREACH_MOD(I_OnBotBan, OnBotBan(u, ci, mask)); } } /*************************************************************************/ /* This makes a bot kick a user. Works somewhat like notice_lang in fact ;) */ void bot_kick(ChannelInfo *ci, User *u, const char *message, ...) { va_list args; char buf[1024]; if (!ci || !ci->bi || !ci->c || !u) return; Anope::string fmt = GetString(u->Account(), message); va_start(args, message); if (fmt.empty()) return; vsnprintf(buf, sizeof(buf), fmt.c_str(), args); va_end(args); ci->c->Kick(ci->bi, u, "%s", buf); } /*************************************************************************/ /* Makes a simple ban and kicks the target * @param requester The user requesting the kickban * @param ci The channel * @param u The user being kicked * @param reason The reason */ void bot_raw_ban(User *requester, ChannelInfo *ci, User *u, const Anope::string &reason) { if (!u || !ci) return; if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u) { ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _(ACCESS_DENIED)).c_str()); return; } ChanAccess *u_access = ci->GetAccess(u), *req_access = ci->GetAccess(requester); int16 u_level = u_access ? u_access->level : 0, req_level = req_access ? req_access->level : 0; if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(u->nick) && u_level >= req_level) return; if (matches_list(ci->c, u, CMODE_EXCEPT)) { ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _("User matches channel except.")).c_str()); return; } Anope::string mask; get_idealban(ci, u, mask); ci->c->SetMode(NULL, CMODE_BAN, mask); /* Check if we need to do a signkick or not -GD */ if (ci->HasFlag(CI_SIGNKICK) || (ci->HasFlag(CI_SIGNKICK_LEVEL) && !check_access(requester, ci, CA_SIGNKICK))) ci->c->Kick(ci->bi, u, "%s (%s)", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str(), requester->nick.c_str()); else ci->c->Kick(ci->bi, u, "%s", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str()); } /*************************************************************************/ /* Makes a kick with a "dynamic" reason ;) * @param requester The user requesting the kick * @param ci The channel * @param u The user being kicked * @param reason The reason for the kick */ void bot_raw_kick(User *requester, ChannelInfo *ci, User *u, const Anope::string &reason) { if (!u || !ci || !ci->c || !ci->c->FindUser(u)) return; if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u) { ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _(ACCESS_DENIED)).c_str()); return; } ChanAccess *u_access = ci->GetAccess(u), *req_access = ci->GetAccess(requester); int16 u_level = u_access ? u_access->level : 0, req_level = req_access ? req_access->level : 0; if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(u->nick) && u_level >= req_level) return; if (ci->HasFlag(CI_SIGNKICK) || (ci->HasFlag(CI_SIGNKICK_LEVEL) && !check_access(requester, ci, CA_SIGNKICK))) ci->c->Kick(ci->bi, u, "%s (%s)", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str(), requester->nick.c_str()); else ci->c->Kick(ci->bi, u, "%s", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str()); } /*************************************************************************/ /** * Normalize buffer stripping control characters and colors * @param A string to be parsed for control and color codes * @return A string stripped of control and color codes */ Anope::string normalizeBuffer(const Anope::string &buf) { Anope::string newbuf; for (unsigned i = 0, end = buf.length(); i < end; ++i) { switch (buf[i]) { /* ctrl char */ case 1: break; /* Bold ctrl char */ case 2: break; /* Color ctrl char */ case 3: /* If the next character is a digit, its also removed */ if (isdigit(buf[i + 1])) { ++i; /* not the best way to remove colors * which are two digit but no worse then * how the Unreal does with +S - TSL */ if (isdigit(buf[i + 1])) ++i; /* Check for background color code * and remove it as well */ if (buf[i + 1] == ',') { ++i; if (isdigit(buf[i + 1])) ++i; /* not the best way to remove colors * which are two digit but no worse then * how the Unreal does with +S - TSL */ if (isdigit(buf[i + 1])) ++i; } } break; /* line feed char */ case 10: break; /* carriage returns char */ case 13: break; /* Reverse ctrl char */ case 22: break; /* Underline ctrl char */ case 31: break; /* Italic ctrl char */ case 29: break; /* A valid char gets copied into the new buffer */ default: newbuf += buf[i]; } } return newbuf; }