#include "module.h" #define FILENAME "seen.db" // TODO: read it from config #define PURGETIME 15552000 // 180 days - TODO: read from config, "180d" or "6M" 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 }; typedef std::map > 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 CommandSeen : public Command { public: CommandSeen() : Command("SEEN", 1, 2) { this->SetFlag(CFLAG_STRIP_CHANNEL); } CommandReturn Execute(CommandSource &source, const std::vector ¶ms) { const Anope::string &target = params[0]; Anope::string onlinestatus; User *u = source.u, *u2 = NULL; ChannelInfo *ci = source.ci; if (ci) { if (!check_access(u, ci, CA_FANTASIA)) { source.Reply(_(ACCESS_DENIED)); return MOD_CONT; } if (!ci->bi) { source.Reply(_(BOT_NOT_ASSIGNED)); return MOD_CONT; } if (!ci->c || !ci->c->FindUser(ci->bi)) { source.Reply(_(BOT_NOT_ON_CHANNEL), ci->name.c_str()); return MOD_CONT; } if (target.equals_ci(ci->bi->nick)) { source.Reply(_("You found me, %s"), u->nick.c_str()); return MOD_CONT; } } if (target.length() > Config->NickLen) { source.Reply(_("Nick too long, max length is %u chars"), Config->NickLen); return MOD_CONT; } if (target.equals_ci(u->nick)) { source.Reply(_("You might see yourself in the mirror, %s."), u->nick.c_str()); return MOD_CONT; } SeenInfo *info = FindInfo(target); if (!info) { source.Reply(_("Sorry, I have not seen %s."), target.c_str()); return MOD_CONT; } if ((u2 = finduser(target.c_str()))) onlinestatus = "."; else { onlinestatus = Anope::printf(GetString(u->Account(), _(" but %s mysteriously dematerialized.")).c_str(), target.c_str()); } struct tm *lastbuf; size_t tbuf, days, hours, minutes, seconds = 0; Anope::string timebuf, timebuf2; tbuf = (Anope::CurTime - info->last); days = (tbuf / 86400); hours = (tbuf / 3600) % 24; minutes = (tbuf / 60) % 60; seconds = (tbuf) % 60; lastbuf = localtime(&info->last); if (days) timebuf = Anope::printf(GetString(u->Account(), _("%u days, %u hours and %u minutes")).c_str(), days, hours, minutes); else if (hours) timebuf = Anope::printf(GetString(u->Account(), _("%u hours and %u minutes")).c_str(), hours, minutes); else if (minutes) timebuf = Anope::printf(GetString(u->Account(), _("%u minutes")).c_str(), minutes); else timebuf = Anope::printf(GetString(u->Account(), _("%u seconds")).c_str(), seconds); timebuf2 = do_strftime(info->last); 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(GetString(u->Account(),_(". %s is still online.")).c_str(), u2->nick.c_str()); else onlinestatus = Anope::printf(GetString(u->Account(),_(", but %s mysteriously dematerialized")).c_str(), 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 && targetchan->HasMode(CMODE_SECRET) && !(ci && ci->c == targetchan)) { 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 && targetchan->HasMode(CMODE_SECRET) && !(ci && ci->c == targetchan)) { 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 && targetchan->HasMode(CMODE_SECRET) && !(ci && ci->c == targetchan)) { 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 MOD_CONT; } void OnSyntaxError(CommandSource &source, const Anope::string &subcommand) { SyntaxError(source, "SEEN", _("SEEN \037channel\037 \037nick\037")); } void OnServHelp(CommandSource &source) { } }; class BSSeen : public Module { CommandSeen commandseen; public: BSSeen(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator) { Implementation eventlist[] = { I_OnUserConnect, I_OnUserNickChange, I_OnUserQuit, I_OnJoinChannel, I_OnPartChannel, I_OnUserKicked, I_OnSaveDatabase, I_OnDatabaseExpire }; ModuleManager::Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation)); this->SetAuthor("Anope"); this->SetType(SUPPORTED); AddCommand(ChanServ, &commandseen); AddCommand(NickServ, &commandseen); LoadDatabase(FILENAME); } ~BSSeen() { SaveDatabase(FILENAME); } EventReturn OnSaveDatabase() { SaveDatabase(FILENAME); return EVENT_CONTINUE; } void OnDatabaseExpire() { PurgeDatabase(PURGETIME); } 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(nick, info)); } info->vhost = u->GetVIdent() + "@" + u->GetDisplayedHost(); info->type = Type; info->last = Anope::CurTime; info->nick2 = nick2; info->channel = channel; info->message = message; } void LoadDatabase(const Anope::string &dbfile) { std::fstream db; db.open(dbfile.c_str(), std::ios_base::in); if (!db.is_open()) { Log() << "Unable to open " << dbfile << " for reading."; return; } Anope::string buf; while (std::getline(db, buf.str())) { if (buf.empty()) continue; spacesepstream sep(buf); std::vector params; while (sep.GetToken(buf)) { if (buf[0] == ':') { buf.erase(buf.begin()); if (!buf.empty() && !sep.StreamEnd()) params.push_back(buf + " " + sep.GetRemaining()); else if (!sep.StreamEnd()) params.push_back(sep.GetRemaining()); else if (!buf.empty()) params.push_back(buf); break; } else params.push_back(buf); } if (params.size() >= 4) { SeenInfo *info = new SeenInfo; database.insert(std::pair(params[0], info)); info->vhost = params[1]; info->last = params[2].is_pos_number_only() ? convertTo(params[2]) : 0 ; if (params[3].equals_ci("NEW")) { info->type = NEW; } else if (params[3].equals_ci("NICK_TO") && params.size() == 5) { info->type = NICK_TO; info->nick2 = params[4]; } else if (params[3].equals_ci("NICK_FROM") && params.size() == 5) { info->type = NICK_FROM; info->nick2 = params[4]; } else if (params[3].equals_ci("JOIN") && params.size() == 5) { info->type = JOIN; info->channel = params[4]; } else if (params[3].equals_ci("PART") && params.size() == 6) { info->type = PART; info->channel = params[4]; info->message = params[5]; } else if (params[3].equals_ci("QUIT") && params.size() == 5) { info->type = QUIT; info->message = params[4]; } else if (params[3].equals_ci("KICK") && params.size() == 7) { info->type = KICK; info->nick2 = params[4]; info->channel = params[5]; info->message = params[6]; } } // if params.size() } // while getline } // LoadDatabase() void SaveDatabase(const Anope::string &dbfile) { std::stringstream db_buffer; for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end; ++it) { db_buffer << it->first.c_str() << " " << it->second->vhost << " " << it->second->last << " "; switch (it->second->type) { case NEW: db_buffer << "NEW"; break; case NICK_TO: db_buffer << "NICK_TO " << it->second->nick2; break; case NICK_FROM: db_buffer << "NICK_FROM " << it->second->nick2; break; case JOIN: db_buffer << "JOIN " << it->second->channel; break; case PART: db_buffer << "PART " << it->second->channel << " :" << it->second->message; break; case QUIT: db_buffer << "QUIT :" << it->second->message; break; case KICK: db_buffer << "KICK " << it->second->nick2 << " " << it->second->channel << " :" << it->second->message; break; } db_buffer << std::endl; } std::fstream db; db.open(dbfile.c_str(), std::ios_base::out | std::ios_base::trunc); if (!db.is_open()) { ircdproto->SendGlobops(NULL, "Unable to open %s for writing.", dbfile.c_str()); } else { db << db_buffer.str(); db_buffer.str(""); db.close(); } } // SaveDatabase() void PurgeDatabase(const time_t &purgetime) { // TODO: remove old entries from the database } }; MODULE_INIT(BSSeen)