#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 { Mutex mutex; Anope::string vhost; // realhost, only for admins TypeInfo type; Anope::string nick2; // only for nickchanges 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; Mutex database_mutex; SeenInfo *FindInfo(const Anope::string &nick) { database_mutex.Lock(); database_map::iterator iter = database.find(nick); if (iter != database.end()) { database_mutex.Unlock(); return iter->second; } else { database_mutex.Unlock(); return NULL; } } class UpdateThread : public Thread { private: TypeInfo Type; Anope::string Nick, Nick2, Channel, Message, Vhost; public: UpdateThread(const User *u, const TypeInfo type, const Anope::string &nick, const Anope::string &nick2, const Anope::string &channel, const Anope::string &message) : Thread(), Type(type), Nick(nick), Nick2(nick2), Channel(channel), Message(message) { Vhost = u->GetVIdent() + "@" + u->GetDisplayedHost(); } ~UpdateThread() { } void Run() { SeenInfo *info = FindInfo(Nick); if (!info) { info = new SeenInfo; database_mutex.Lock(); database.insert(std::pair(Nick, info)); database_mutex.Unlock(); } Log() << "Thread: " << this->Handle << " locked mutex for " << Nick; info->mutex.Lock(); info->vhost = Vhost; info->type = Type; info->last = Anope::CurTime; info->nick2 = Nick2; info->channel = Channel; info->message = Message; info->mutex.Unlock(); Log() << "Thread: " << this->Handle << " unlocked mutex for " << Nick; } }; class SaveDBThread : public Thread, public Mutex { private: Anope::string Filename; bool dbproblem; public: SaveDBThread(const Anope::string &filename, bool nothread) : Thread(), Filename(filename) { dbproblem = false; if (nothread) this->Run(); } ~SaveDBThread() { if (dbproblem) ircdproto->SendGlobops(NULL, "Unable to open %s for writing.", Filename.c_str()); } void Run() { this->Lock(); // make sure we are not having 2 threads running at the same time. std::stringstream db_buffer; database_mutex.Lock(); for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end; ++it) { it->second->mutex.Lock(); 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; } it->second->mutex.Unlock(); db_buffer << std::endl; } database_mutex.Unlock(); std::fstream db; db.open(Filename.c_str(), std::ios_base::out | std::ios_base::trunc); if (!db.is_open()) { dbproblem = true; } else { db << db_buffer.str(); db_buffer.str(""); db.close(); } this->Unlock(); } }; class PurgeDBThread : public Thread { private: time_t Time; public: PurgeDBThread(const time_t time) : Thread(), Time(time) { } ~PurgeDBThread() { } void Run() { // remove old entries from the database } }; class CommandCSSeen : public Command { public: CommandCSSeen() : Command("SEEN", 2, 3) { this->SetDesc("Makes the Bot say when a given User was last seen by services."); } CommandReturn Execute(CommandSource &source, const std::vector ¶ms) { const Anope::string &target = params[1]; Anope::string onlinestatus; User *u = source.u, *u2 = NULL; ChannelInfo *ci = source.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.length() > Config->NickLen) { source.Reply(_("Nick too long, max length is %u chars"), Config->NickLen); 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.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; } info->mutex.Lock(); 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 && (ci->c != targetchan) && targetchan->HasMode(CMODE_SECRET)) { 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->c != targetchan) && targetchan->HasMode(CMODE_SECRET)) { 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->c != targetchan) && targetchan->HasMode(CMODE_SECRET)) { 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()); } } info->mutex.Unlock(); // never ever return without Unlock()! return MOD_CONT; } void OnSyntaxError(CommandSource &source, const Anope::string &subcommand) { SyntaxError(source, "SEEN", _("SEEN \037channel\037 \037nick\037")); } }; class BSSeen : public Module { CommandCSSeen commandcsseen; 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, &commandcsseen); LoadDatabase(FILENAME); } ~BSSeen() { SaveDBThread(FILENAME, true); } EventReturn OnSaveDatabase() { threadEngine.Start(new SaveDBThread(FILENAME, false)); return EVENT_CONTINUE; } void OnDatabaseExpire() { threadEngine.Start(new PurgeDBThread(PURGETIME)); } void OnUserConnect(User *u) { threadEngine.Start(new UpdateThread(u, NEW, u->nick, "", "", "")); } void OnUserNickChange(User *u, const Anope::string &oldnick) { threadEngine.Start(new UpdateThread(u, NICK_TO, oldnick, u->nick, "", "")); threadEngine.Start(new UpdateThread(u, NICK_FROM, u->nick, oldnick, "", "")); } void OnUserQuit(User *u, const Anope::string &msg) { threadEngine.Start(new UpdateThread(u, QUIT, u->nick, "", "", msg)); } void OnJoinChannel(User *u, Channel *c) { threadEngine.Start(new UpdateThread(u, JOIN, u->nick, "", c->name, "")); } void OnPartChannel(User *u, Channel *c, const Anope::string &channel, const Anope::string &msg) { threadEngine.Start(new UpdateThread(u, PART, u->nick, "", channel, msg)); } void OnUserKicked(Channel *c, User *target, const Anope::string &source, const Anope::string &msg) { threadEngine.Start(new UpdateThread(target, KICK, target->nick, source, c->name, msg)); } void LoadDatabase(const Anope::string &Filename) { std::fstream db; db.open(Filename.c_str(), std::ios_base::in); if (!db.is_open()) { Log() << "Unable to open " << Filename << " 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() }; MODULE_INIT(BSSeen)