diff options
-rw-r--r-- | data/chanserv.example.conf | 11 | ||||
-rw-r--r-- | modules/commands/cs_seen.cpp | 444 |
2 files changed, 455 insertions, 0 deletions
diff --git a/data/chanserv.example.conf b/data/chanserv.example.conf index 889f1d3e8..048dee6cc 100644 --- a/data/chanserv.example.conf +++ b/data/chanserv.example.conf @@ -460,6 +460,17 @@ module { name = "cs_register" } command { service = "ChanServ"; name = "REGISTER"; command = "chanserv/register"; } /* + * cs_seen + * + * Provides the commands chanserv/seen and operserv/seen. + * + * Logs for each user when he or she was last seen and makes this information publically available. + */ +module { name = "cs_seen" } +command { service = "ChanServ"; name = "SEEN"; command = "chanserv/seen"; } +command { service = "OperServ"; name = "SEEN"; command = "operserv/seen"; } + +/* * cs_set * * Provides the command chanserv/set. diff --git a/modules/commands/cs_seen.cpp b/modules/commands/cs_seen.cpp new file mode 100644 index 000000000..4a552991c --- /dev/null +++ b/modules/commands/cs_seen.cpp @@ -0,0 +1,444 @@ +/* cs_seen: provides a !seen command by tracking all users + * + * (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 "module.h" + +enum TypeInfo +{ + NEW, NICK_TO, NICK_FROM, JOIN, PART, QUIT, KICK +}; + +struct SeenInfo +{ + Anope::string vhost; + TypeInfo type; + Anope::string nick2; // for nickchanges and kicks + Anope::string channel; // for join/part/kick + Anope::string message; // for part/kick/quit + time_t last; // the time when the user was last seen +}; + +class ModuleConfigClass +{ + public: + time_t purgetime; + time_t expiretimeout; +}; +ModuleConfigClass ModuleConfig; + +typedef std::map<Anope::string, SeenInfo *, std::less<ci::string> > database_map; +database_map database; + +SeenInfo *FindInfo(const Anope::string &nick) +{ + database_map::iterator iter = database.find(nick); + if (iter != database.end()) + { + return iter->second; + } + else + { + return NULL; + } +} + +class CommandOSSeen : public Command +{ + public: + CommandOSSeen(Module *creator) : Command(creator, "operserv/seen", 1, 2) + { + this->SetDesc(_("Statistics and maintenance for the BotServ !seen command")); + this->SetSyntax(_("{\037STATS\037 | \037CLEAR\037} \037time\037")); + } + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) + { + if (params[0].equals_ci("STATS")) + { + size_t mem_counter; + mem_counter = sizeof(database_map); + for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end; ++it) + { + mem_counter += (5 * sizeof(Anope::string)) + sizeof(TypeInfo) + sizeof(time_t); + mem_counter += it->first.capacity(); + mem_counter += it->second->vhost.capacity(); + mem_counter += it->second->nick2.capacity(); + mem_counter += it->second->channel.capacity(); + mem_counter += it->second->message.capacity(); + } + source.Reply(_("%lu nicks are stored in the database, using %.2Lf kB of memory"), database.size(), static_cast<long double>(mem_counter) / 1024); + } + else if (params[0].equals_ci("CLEAR")) + { + time_t time = 0; + if ((params.size() < 2) || (0 >= (time = dotime(params[1])))) + { + this->OnSyntaxError(source, params[0]); + return; + } + time = Anope::CurTime - time; + database_map::iterator buf; + size_t counter = 0; + for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end;) + { + buf = it; + ++it; + if (time < buf->second->last) + { + Log(LOG_DEBUG) << buf->first << " was last seen " << do_strftime(buf->second->last) << ", deleting entry"; + database.erase(buf); + counter++; + } + } + Log(LOG_ADMIN, source.u, this) << "CLEAR and removed " << counter << " nicks that were added after " << do_strftime(time, NULL, true); + source.Reply(_("Database cleared, removed %lu nicks that were added after %s"), counter, do_strftime(time, source.u->Account(), true).c_str()); + } + return; + } + bool OnHelp(CommandSource &source, const Anope::string &subcommand) + { + if (subcommand.empty()) + source.Reply(_("Syntax: \002SEEN {\037STATS\037 | \037CLEAR\037} \037time\037\002")); + else if (subcommand.equals_ci("STATS")) + source.Reply(_("Syntax: \002SEEN STATS\002\n" + "Prints out some statistic information about stored nicks and memory usage.")); + else if (subcommand.equals_ci("CLEAR")) + source.Reply(_("Syntax: \002SEEN CLEAR time\002\n" + "This command can clean the database after a botflood attack by removing all\n" + "entries from the database that were added within \002time\002.\n" + "Example: SEEN CLEAR 30m will remove all entries that were added within the last 30 minutes.")); + else + return false; + return true; + } +}; + +class CommandSeen : public Command +{ + public: + CommandSeen(Module *creator) : Command(creator, "chanserv/seen", 1, 2) + { + this->SetFlag(CFLAG_STRIP_CHANNEL); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) + { + const Anope::string &target = params[0]; + Anope::string onlinestatus; + User *u = source.u, *u2 = NULL; + ChannelInfo *ci = (source.c && source.c->ci) ? source.c->ci : NULL; + if (ci) + { + if (!ci->AccessFor(u).HasPriv(CA_FANTASIA)) + { + source.Reply(_(ACCESS_DENIED)); + return; + } + + if (!ci->bi) + { + source.Reply(_(BOT_NOT_ASSIGNED)); + return; + } + + if (!ci->c || !ci->c->FindUser(ci->bi)) + { + source.Reply(_(BOT_NOT_ON_CHANNEL), ci->name.c_str()); + return; + } + + if (target.equals_ci(ci->bi->nick)) + { + source.Reply(_("You found me, %s"), u->nick.c_str()); + return; + } + } + + if (target.length() > Config->NickLen) + { + source.Reply(_("Nick too long, max length is %u chars"), Config->NickLen); + return; + } + if (target.equals_ci(u->nick)) + { + source.Reply(_("You might see yourself in the mirror, %s."), u->nick.c_str()); + return; + } + SeenInfo *info = FindInfo(target); + if (!info) + { + source.Reply(_("Sorry, I have not seen %s."), target.c_str()); + return; + } + if ((u2 = finduser(target.c_str()))) + onlinestatus = "."; + else + { + onlinestatus = Anope::printf(translate(u->Account(), _(" but %s mysteriously dematerialized.")), target.c_str()); + } + + Anope::string timebuf = duration(Anope::CurTime - info->last, u->Account()); + Anope::string timebuf2 = do_strftime(info->last, u->Account(), true); + + if (info->type == NEW) + { + source.Reply(_("%s (%s) was last seen connecting %s ago (%s)%s"), + target.c_str(), info->vhost.c_str(), timebuf.c_str(), timebuf2.c_str(), onlinestatus.c_str()); + } + else if (info->type == NICK_TO) + { + if ((u2 = finduser(info->nick2))) + onlinestatus = Anope::printf(translate(u->Account(), _(". %s is still online.")), u2->nick.c_str()); + else + onlinestatus = Anope::printf(translate(u->Account(), _(", but %s mysteriously dematerialized")), info->nick2.c_str()); + + source.Reply(_("%s (%s) was last seen changing nick to %s %s ago%s"), + target.c_str(), info->vhost.c_str(), info->nick2.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + else if (info->type == NICK_FROM) + { + source.Reply(_("%s (%s) was last seen changing nick from %s to %s %s ago%s"), + target.c_str(), info->vhost.c_str(), info->nick2.c_str(), target.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + else if (info->type == JOIN) + { + Channel *targetchan = findchan(info->channel); + if (!(targetchan && ci && ci->c == targetchan) && ((targetchan && targetchan->HasMode(CMODE_SECRET)) || (u2 && u2->HasMode(UMODE_PRIV)))) + { + source.Reply(_("%s (%s) was last seen joining a secret channel %s ago%s"), + target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + else + { + source.Reply(_("%s (%s) was last seen joining %s %s ago%s"), + target.c_str(), info->vhost.c_str(), info->channel.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + } + else if (info->type == PART) + { + Channel *targetchan = findchan(info->channel); + if (!(targetchan && ci && ci->c == targetchan) && ((targetchan && targetchan->HasMode(CMODE_SECRET)) || (u2 && u2->HasMode(UMODE_PRIV)))) + { + source.Reply(_("%s (%s) was last seen parting a secret channel %s ago%s"), + target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + else + { + source.Reply(_("%s (%s) was last seen parting %s %s ago%s"), + target.c_str(), info->vhost.c_str(), info->channel.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + } + else if (info->type == QUIT) + { + source.Reply(_("%s (%s) was last seen quitting (%s) %s ago (%s)."), + target.c_str(), info->vhost.c_str(), info->message.c_str(), timebuf.c_str(), timebuf2.c_str()); + } + else if (info->type == KICK) + { + Channel *targetchan = findchan(info->channel); + if (!(targetchan && ci && ci->c == targetchan) && ((targetchan && targetchan->HasMode(CMODE_SECRET)) || (u2 && u2->HasMode(UMODE_PRIV)))) + { + source.Reply(_("%s (%s) was kicked from a secret channel %s ago%s"), + target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + else + { + source.Reply(_("%s (%s) was kicked from %s (\"%s\") %s ago%s"), + target.c_str(), info->vhost.c_str(), info->channel.c_str(), info->message.c_str(), timebuf.c_str(), onlinestatus.c_str()); + } + } + return; + } +}; + + + +class DataBasePurger : public CallBack +{ + public: + DataBasePurger(Module *owner) : CallBack(owner, 300, Anope::CurTime, true) { } + + void Tick(time_t) + { + if (noexpire || readonly) + return; + + database_map::iterator buf; + size_t counter = 0; + for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end;) + { + buf = it; + ++it; + if ((Anope::CurTime - buf->second->last) > ModuleConfig.purgetime) + { + Log(LOG_DEBUG) << buf->first << " was last seen " << do_strftime(buf->second->last) << ", purging entry"; + database.erase(buf); + counter++; + } + } + Log(LOG_NORMAL) << "cs_seen: Purged Database, checked " << database.size() << " nicks and removed " << counter << " old entries."; + } +}; + +class CSSeen : public Module +{ + CommandSeen commandseen; + CommandOSSeen commandosseen; + DataBasePurger purger; + public: + CSSeen(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, CORE), commandseen(this), commandosseen(this), purger(this) + { + Implementation eventlist[] = { I_OnReload, + I_OnUserConnect, + I_OnUserNickChange, + I_OnUserQuit, + I_OnJoinChannel, + I_OnPartChannel, + I_OnUserKicked, + I_OnDatabaseRead, + I_OnDatabaseWrite }; + ModuleManager::Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation)); + this->SetAuthor("Anope"); + ModuleManager::RegisterService(&commandseen); + ModuleManager::RegisterService(&commandosseen); + OnReload(); + } + void OnReload() + { + ConfigReader config; + ModuleConfig.purgetime = dotime(config.ReadValue("cs_seen", "purgetime", "30d", 0)); + ModuleConfig.expiretimeout = dotime(config.ReadValue("cs_seen", "expiretimeout", "1d", 0)); + + if (purger.GetSecs() != ModuleConfig.expiretimeout) + purger.SetSecs(ModuleConfig.expiretimeout); + } + void OnUserConnect(User *u) + { + UpdateUser(u, NEW, u->nick, "", "", ""); + } + void OnUserNickChange(User *u, const Anope::string &oldnick) + { + UpdateUser(u, NICK_TO, oldnick, u->nick, "", ""); + UpdateUser(u, NICK_FROM, u->nick, oldnick, "", ""); + } + void OnUserQuit(User *u, const Anope::string &msg) + { + UpdateUser(u, QUIT, u->nick, "", "", msg); + } + void OnJoinChannel(User *u, Channel *c) + { + UpdateUser(u, JOIN, u->nick, "", c->name, ""); + } + void OnPartChannel(User *u, Channel *c, const Anope::string &channel, const Anope::string &msg) + { + UpdateUser(u, PART, u->nick, "", channel, msg); + } + void OnUserKicked(Channel *c, User *target, const Anope::string &source, const Anope::string &msg) + { + UpdateUser(target, KICK, target->nick, source, c->name, msg); + } + void UpdateUser(const User *u, const TypeInfo Type, const Anope::string &nick, const Anope::string &nick2, const Anope::string &channel, const Anope::string &message) + { + SeenInfo *info = FindInfo(nick); + if (!info) + { + info = new SeenInfo; + database.insert(std::pair<Anope::string, SeenInfo *>(nick, info)); + } + info->vhost = u->GetVIdent() + "@" + u->GetDisplayedHost(); + info->type = Type; + info->last = Anope::CurTime; + info->nick2 = nick2; + info->channel = channel; + info->message = message; + } + + EventReturn OnDatabaseRead(const std::vector<Anope::string> ¶ms) + { + if (params[0].equals_ci("SEEN") && (params.size() >= 5)) + { + SeenInfo *info = new SeenInfo; + database.insert(std::pair<Anope::string, SeenInfo *>(params[1], info)); + info->vhost = params[2]; + info->last = params[3].is_pos_number_only() ? convertTo<time_t>(params[3]) : 0 ; + if (params[4].equals_ci("NEW")) + { + info->type = NEW; + } + else if (params[4].equals_ci("NICK_TO") && params.size() == 6) + { + info->type = NICK_TO; + info->nick2 = params[5]; + } + else if (params[4].equals_ci("NICK_FROM") && params.size() == 6) + { + info->type = NICK_FROM; + info->nick2 = params[5]; + } + else if (params[4].equals_ci("JOIN") && params.size() == 6) + { + info->type = JOIN; + info->channel = params[5]; + } + else if (params[4].equals_ci("PART") && params.size() == 7) + { + info->type = PART; + info->channel = params[5]; + info->message = params[6]; + } + else if (params[4].equals_ci("QUIT") && params.size() == 6) + { + info->type = QUIT; + info->message = params[5]; + } + else if (params[4].equals_ci("KICK") && params.size() == 8) + { + info->type = KICK; + info->nick2 = params[5]; + info->channel = params[6]; + info->message = params[7]; + } + return EVENT_STOP; + } + return EVENT_CONTINUE; + } + void OnDatabaseWrite(void (*Write)(const Anope::string &)) + { + std::stringstream buf; + for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end; ++it) + { + buf << "SEEN " << it->first.c_str() << " " << it->second->vhost << " " << it->second->last << " "; + switch (it->second->type) + { + case NEW: + buf << "NEW"; break; + case NICK_TO: + buf << "NICK_TO " << it->second->nick2; break; + case NICK_FROM: + buf << "NICK_FROM " << it->second->nick2; break; + case JOIN: + buf << "JOIN " << it->second->channel; break; + case PART: + buf << "PART " << it->second->channel << " :" << it->second->message; break; + case QUIT: + buf << "QUIT :" << it->second->message; break; + case KICK: + buf << "KICK " << it->second->nick2 << " " << it->second->channel << " :" << it->second->message; break; + } + Write(buf.str()); + buf.str(""); + } + } +}; + +MODULE_INIT(CSSeen) |