1
0
mirror of https://github.com/anope/anope.git synced 2026-06-25 16:06:37 +02:00
Files
anope/src/botserv.cpp
T

673 lines
18 KiB
C++

/* BotServ functions
*
* (C) 2003-2010 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);
static void check_ban(ChannelInfo *ci, User *u, int ttbtype);
static void bot_kick(ChannelInfo *ci, User *u, LanguageString message, ...);
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 (patricia_tree<BotInfo *>::const_iterator it = BotListByNick.begin(), it_end = BotListByNick.end(); it != it_end; ++it)
{
BotInfo *bi = *it;
++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[buf.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 an user is kicked before
* the last kicker check.
*
* But FIRST we check whether the user is protected in any
* way.
*/
bool Allow = false;
if (!ci->botflags.HasFlag(BS_DONTKICKOPS) && !ci->botflags.HasFlag(BS_DONTKICKVOICES))
Allow = true;
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 = true;
else if (ci->botflags.HasFlag(BS_DONTKICKVOICES) && ci->c->HasUserStatus(u, CMODE_VOICE))
Allow = true;
if (!check_access(u, ci, CA_NOKICK) && 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, BOT_REASON_BOLD);
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, BOT_REASON_COLOR);
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, BOT_REASON_REVERSE);
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, BOT_REASON_ITALIC);
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, BOT_REASON_UNDERLINE);
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, BOT_REASON_CAPS);
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, BOT_REASON_BADWORD_GENTLE);
else
bot_kick(ci, u, BOT_REASON_BADWORD, 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, BOT_REASON_FLOOD);
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, BOT_REASON_REPEAT);
return;
}
}
}
/* return if the user is on the ignore list */
if (get_ignore(u->nick))
return;
/* Fantaisist commands */
if (ci->botflags.HasFlag(BS_FANTASY) && buf[0] == Config->BSFantasyCharacter[0] && !was_action)
{
spacesepstream sep(buf);
Anope::string command;
if (sep.GetToken(command) && command[0] == Config->BSFantasyCharacter[0])
{
/* Strip off the fantasy character */
command.erase(command.begin());
if (check_access(u, ci, CA_FANTASIA))
{
Anope::string message = sep.GetRemaining();
EventReturn MOD_RESULT;
FOREACH_RESULT(I_OnPreCommandRun, OnPreCommandRun(u, ci->bi, command, message, true));
if (MOD_RESULT == EVENT_STOP)
return;
Command *cmd = FindCommand(ChanServ, command);
/* Command exists and can be called by fantasy */
if (cmd && !cmd->HasFlag(CFLAG_DISABLE_FANTASY))
{
/* Some commands don't need the channel name added.. eg !help */
if (!cmd->HasFlag(CFLAG_STRIP_CHANNEL))
message = ci->name + " " + message;
message = command + " " + message;
mod_run_cmd(ChanServ, u, message, true);
}
FOREACH_MOD(I_OnBotFantasy, OnBotFantasy(command, u, ci, sep.GetRemaining()));
}
else
{
FOREACH_MOD(I_OnBotNoFantasyAccess, OnBotNoFantasyAccess(command, u, ci, sep.GetRemaining()));
}
}
}
}
/*************************************************************************/
BotInfo *findbot(const Anope::string &nick)
{
if (isdigit(nick[0]) && ircd->ts6)
return BotListByUID.find(nick);
return BotListByNick.find(nick);
}
/*************************************************************************/
/* Returns ban data associated with an 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<BanData *>::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 an 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
*/
static 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 an user. Works somewhat like notice_lang in fact ;) */
static void bot_kick(ChannelInfo *ci, User *u, LanguageString message, ...)
{
va_list args;
char buf[1024];
if (!ci || !ci->bi || !ci->c || !u)
return;
Anope::string fmt = GetString(u, 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 */
void bot_raw_ban(User *requester, ChannelInfo *ci, const Anope::string &nick, const Anope::string &reason)
{
Anope::string mask;
User *u = finduser(nick);
if (!u)
return;
if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u)
{
ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester, ACCESS_DENIED).c_str());
return;
}
if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(nick) && get_access(u, ci) >= get_access(requester, ci))
return;
if (ModeManager::FindChannelModeByName(CMODE_EXCEPT) && is_excepted(ci, u) == 1)
{
ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester, BOT_EXCEPT).c_str());
return;
}
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 ;) */
void bot_raw_kick(User *requester, ChannelInfo *ci, const Anope::string &nick, const Anope::string &reason)
{
User *u = finduser(nick);
if (!u || !ci->c->FindUser(u))
return;
if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u)
{
ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester, ACCESS_DENIED).c_str());
return;
}
if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(nick) && get_access(u, ci) >= get_access(requester, ci))
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());
}
/*************************************************************************/
/* Makes a mode operation on a channel for a nick */
void bot_raw_mode(User *requester, ChannelInfo *ci, const Anope::string &mode, const Anope::string &nick)
{
char buf[BUFSIZE] = "";
User *u;
u = finduser(nick);
if (!u || !ci->c->FindUser(u))
return;
snprintf(buf, BUFSIZE - 1, "%ld", static_cast<long>(Anope::CurTime));
if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && mode[0] == '-' && requester != u)
{
ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester, ACCESS_DENIED).c_str());
return;
}
if (mode[0] == '-' && ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(nick) && get_access(u, ci) >= get_access(requester, ci))
return;
ci->c->SetModes(NULL, "%s %s", mode.c_str(), 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;
}