anope

- supernets anope source code & configuration
git clone git://git.acid.vegas/anope.git
Log | Files | Refs | Archive | README

cs_seen.cpp (14010B)

      1 /* cs_seen: provides a seen command by tracking all users
      2  *
      3  * (C) 2003-2022 Anope Team
      4  * Contact us at team@anope.org
      5  *
      6  * Please read COPYING and README for further details.
      7  *
      8  * Based on the original code of Epona by Lara.
      9  * Based on the original code of Services by Andy Church.
     10  */
     11 
     12 #include "module.h"
     13 
     14 enum TypeInfo
     15 {
     16 	NEW, NICK_TO, NICK_FROM, JOIN, PART, QUIT, KICK
     17 };
     18 
     19 static bool simple;
     20 struct SeenInfo;
     21 static SeenInfo *FindInfo(const Anope::string &nick);
     22 typedef Anope::hash_map<SeenInfo *> database_map;
     23 database_map database;
     24 
     25 struct SeenInfo : Serializable
     26 {
     27 	Anope::string nick;
     28 	Anope::string vhost;
     29 	TypeInfo type;
     30 	Anope::string nick2;    // for nickchanges and kicks
     31 	Anope::string channel;  // for join/part/kick
     32 	Anope::string message;  // for part/kick/quit
     33 	time_t last;            // the time when the user was last seen
     34 
     35 	SeenInfo() : Serializable("SeenInfo")
     36 	{
     37 	}
     38 
     39 	~SeenInfo()
     40 	{
     41 		database_map::iterator iter = database.find(nick);
     42 		if (iter != database.end() && iter->second == this)
     43 			database.erase(iter);
     44 	}
     45 
     46 	void Serialize(Serialize::Data &data) const anope_override
     47 	{
     48 		data["nick"] << nick;
     49 		data["vhost"] << vhost;
     50 		data["type"] << type;
     51 		data["nick2"] << nick2;
     52 		data["channel"] << channel;
     53 		data["message"] << message;
     54 		data.SetType("last", Serialize::Data::DT_INT); data["last"] << last;
     55 	}
     56 
     57 	static Serializable* Unserialize(Serializable *obj, Serialize::Data &data)
     58 	{
     59 		Anope::string snick;
     60 
     61 		data["nick"] >> snick;
     62 
     63 		SeenInfo *s;
     64 		if (obj)
     65 			s = anope_dynamic_static_cast<SeenInfo *>(obj);
     66 		else
     67 		{
     68 			SeenInfo* &info = database[snick];
     69 			if (!info)
     70 				info = new SeenInfo();
     71 			s = info;
     72 		}
     73 
     74 		s->nick = snick;
     75 		data["vhost"] >> s->vhost;
     76 		unsigned int n;
     77 		data["type"] >> n;
     78 		s->type = static_cast<TypeInfo>(n);
     79 		data["nick2"] >> s->nick2;
     80 		data["channel"] >> s->channel;
     81 		data["message"] >> s->message;
     82 		data["last"] >> s->last;
     83 
     84 		if (!obj)
     85 			database[s->nick] = s;
     86 		return s;
     87 	}
     88 };
     89 
     90 static SeenInfo *FindInfo(const Anope::string &nick)
     91 {
     92 	database_map::iterator iter = database.find(nick);
     93 	if (iter != database.end())
     94 		return iter->second;
     95 	return NULL;
     96 }
     97 
     98 static bool ShouldHide(const Anope::string &channel, User *u)
     99 {
    100 	Channel *targetchan = Channel::Find(channel);
    101 	const ChannelInfo *targetchan_ci = targetchan ? *targetchan->ci : ChannelInfo::Find(channel);
    102 
    103 	if (targetchan && targetchan->HasMode("SECRET"))
    104 		return true;
    105 	else if (targetchan_ci && targetchan_ci->HasExt("CS_PRIVATE"))
    106 		return true;
    107 	else if (u && u->HasMode("PRIV"))
    108 		return true;
    109 	return false;
    110 }
    111 
    112 class CommandOSSeen : public Command
    113 {
    114  public:
    115 	CommandOSSeen(Module *creator) : Command(creator, "operserv/seen", 1, 2)
    116 	{
    117 		this->SetDesc(_("Statistics and maintenance for seen data"));
    118 		this->SetSyntax("STATS");
    119 		this->SetSyntax(_("CLEAR \037time\037"));
    120 	}
    121 
    122 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
    123 	{
    124 		if (params[0].equals_ci("STATS"))
    125 		{
    126 			size_t mem_counter;
    127 			mem_counter = sizeof(database_map);
    128 			for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end; ++it)
    129 			{
    130 				mem_counter += (5 * sizeof(Anope::string)) + sizeof(TypeInfo) + sizeof(time_t);
    131 				mem_counter += it->first.capacity();
    132 				mem_counter += it->second->vhost.capacity();
    133 				mem_counter += it->second->nick2.capacity();
    134 				mem_counter += it->second->channel.capacity();
    135 				mem_counter += it->second->message.capacity();
    136 			}
    137 			source.Reply(_("%lu nicks are stored in the database, using %.2Lf kB of memory."), database.size(), static_cast<long double>(mem_counter) / 1024);
    138 		}
    139 		else if (params[0].equals_ci("CLEAR"))
    140 		{
    141 			time_t time = 0;
    142 			if ((params.size() < 2) || (0 >= (time = Anope::DoTime(params[1]))))
    143 			{
    144 				this->OnSyntaxError(source, params[0]);
    145 				return;
    146 			}
    147 			time = Anope::CurTime - time;
    148 			database_map::iterator buf;
    149 			size_t counter = 0;
    150 			for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end;)
    151 			{
    152 				buf = it;
    153 				++it;
    154 				if (time < buf->second->last)
    155 				{
    156 					Log(LOG_DEBUG) << buf->first << " was last seen " << Anope::strftime(buf->second->last) << ", deleting entry";
    157 					delete buf->second;
    158 					counter++;
    159 				}
    160 			}
    161 			Log(LOG_ADMIN, source, this) << "CLEAR and removed " << counter << " nicks that were added after " << Anope::strftime(time, NULL, true);
    162 			source.Reply(_("Database cleared, removed %lu nicks that were added after %s."), counter, Anope::strftime(time, source.nc, true).c_str());
    163 		}
    164 		else
    165 			this->SendSyntax(source);
    166 	}
    167 
    168 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
    169 	{
    170 		this->SendSyntax(source);
    171 		source.Reply(" ");
    172 		source.Reply(_("The \002STATS\002 command prints out statistics about stored nicks and memory usage."));
    173 		source.Reply(_("The \002CLEAR\002 command lets you clean the database by removing all entries from the\n"
    174 				"database that were added within \037time\037.\n"
    175 				" \n"
    176 				"Example:\n"
    177 				" %s CLEAR 30m\n"
    178 				" Will remove all entries that were added within the last 30 minutes."), source.command.c_str());
    179 		return true;
    180 	}
    181 };
    182 
    183 class CommandSeen : public Command
    184 {
    185 	void SimpleSeen(CommandSource &source, const std::vector<Anope::string> &params)
    186 	{
    187 		if (!source.c || !source.c->ci)
    188 		{
    189 			if (source.IsOper())
    190 				source.Reply("Seen in simple mode is designed as a fantasy command only!");
    191 			return;
    192 		}
    193 
    194 		BotInfo *bi = BotInfo::Find(params[0], true);
    195 		if (bi)
    196 		{
    197 			if (bi == source.c->ci->bi)
    198 				source.Reply(_("You found me, %s!"), source.GetNick().c_str());
    199 			else
    200 				source.Reply(_("%s is a network service."), bi->nick.c_str());
    201 			return;
    202 		}
    203 
    204 		NickAlias *na = NickAlias::Find(params[0]);
    205 		if (!na)
    206 		{
    207 			source.Reply(_("I don't know who %s is."), params[0].c_str());
    208 			return;
    209 		}
    210 
    211 		if (source.GetAccount() == na->nc)
    212 		{
    213 			source.Reply(_("Looking for yourself, eh %s?"), source.GetNick().c_str());
    214 			return;
    215 		}
    216 
    217 		User *target = User::Find(params[0], true);
    218 
    219 		if (target && source.c->FindUser(target))
    220 		{
    221 			source.Reply(_("%s is on the channel right now!"), target->nick.c_str());
    222 			return;
    223 		}
    224 
    225 		for (Channel::ChanUserList::const_iterator it = source.c->users.begin(), it_end = source.c->users.end(); it != it_end; ++it)
    226 		{
    227 			ChanUserContainer *uc = it->second;
    228 			User *u = uc->user;
    229 
    230 			if (u->Account() == na->nc)
    231 			{
    232 				source.Reply(_("%s is on the channel right now (as %s)!"), params[0].c_str(), u->nick.c_str());
    233 				return;
    234 			}
    235 		}
    236 
    237 		AccessGroup ag = source.c->ci->AccessFor(na->nc);
    238 		time_t last = 0;
    239 		for (unsigned int i = 0; i < ag.paths.size(); ++i)
    240 		{
    241 			ChanAccess::Path &p = ag.paths[i];
    242 
    243 			if (p.empty())
    244 				continue;
    245 
    246 			ChanAccess *a = p[p.size() - 1];
    247 
    248 			if (a->GetAccount() == na->nc && a->last_seen > last)
    249 				last = a->last_seen;
    250 		}
    251 
    252 		if (last > Anope::CurTime || !last)
    253 			source.Reply(_("I've never seen %s on this channel."), na->nick.c_str());
    254 		else
    255 			source.Reply(_("%s was last seen here %s ago."), na->nick.c_str(), Anope::Duration(Anope::CurTime - last, source.GetAccount()).c_str());
    256 	}
    257 
    258  public:
    259 	CommandSeen(Module *creator) : Command(creator, "chanserv/seen", 1, 2)
    260 	{
    261 		this->SetDesc(_("Tells you about the last time a user was seen"));
    262 		this->SetSyntax(_("\037nick\037"));
    263 		this->AllowUnregistered(true);
    264 	}
    265 
    266 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
    267 	{
    268 		const Anope::string &target = params[0];
    269 
    270 		if (simple)
    271 			return this->SimpleSeen(source, params);
    272 
    273 		if (target.length() > Config->GetBlock("networkinfo")->Get<unsigned>("nicklen"))
    274 		{
    275 			source.Reply(_("Nick too long, max length is %u characters."), Config->GetBlock("networkinfo")->Get<unsigned>("nicklen"));
    276 			return;
    277 		}
    278 
    279 		if (BotInfo::Find(target, true) != NULL)
    280 		{
    281 			source.Reply(_("%s is a client on services."), target.c_str());
    282 			return;
    283 		}
    284 
    285 		if (target.equals_ci(source.GetNick()))
    286 		{
    287 			source.Reply(_("You might see yourself in the mirror, %s."), source.GetNick().c_str());
    288 			return;
    289 		}
    290 
    291 		SeenInfo *info = FindInfo(target);
    292 		if (!info)
    293 		{
    294 			source.Reply(_("Sorry, I have not seen %s."), target.c_str());
    295 			return;
    296 		}
    297 
    298 		User *u2 = User::Find(target, true);
    299 		Anope::string onlinestatus;
    300 		if (u2)
    301 			onlinestatus = ".";
    302 		else
    303 			onlinestatus = Anope::printf(Language::Translate(source.nc, _(" but %s mysteriously dematerialized.")), target.c_str());
    304 
    305 		Anope::string timebuf = Anope::Duration(Anope::CurTime - info->last, source.nc);
    306 		Anope::string timebuf2 = Anope::strftime(info->last, source.nc, true);
    307 
    308 		if (info->type == NEW)
    309 		{
    310 			source.Reply(_("%s (%s) was last seen connecting %s ago (%s)%s"),
    311 				target.c_str(), info->vhost.c_str(), timebuf.c_str(), timebuf2.c_str(), onlinestatus.c_str());
    312 		}
    313 		else if (info->type == NICK_TO)
    314 		{
    315 			u2 = User::Find(info->nick2, true);
    316 			if (u2)
    317 				onlinestatus = Anope::printf(Language::Translate(source.nc, _(". %s is still online.")), u2->nick.c_str());
    318 			else
    319 				onlinestatus = Anope::printf(Language::Translate(source.nc, _(", but %s mysteriously dematerialized.")), info->nick2.c_str());
    320 
    321 			source.Reply(_("%s (%s) was last seen changing nick to %s %s ago%s"),
    322 				target.c_str(), info->vhost.c_str(), info->nick2.c_str(), timebuf.c_str(), onlinestatus.c_str());
    323 		}
    324 		else if (info->type == NICK_FROM)
    325 		{
    326 			source.Reply(_("%s (%s) was last seen changing nick from %s to %s %s ago%s"),
    327 				target.c_str(), info->vhost.c_str(), info->nick2.c_str(), target.c_str(), timebuf.c_str(), onlinestatus.c_str());
    328 		}
    329 		else if (info->type == JOIN)
    330 		{
    331 			if (ShouldHide(info->channel, u2))
    332 				source.Reply(_("%s (%s) was last seen joining a secret channel %s ago%s"),
    333 					target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str());
    334 			else
    335 				source.Reply(_("%s (%s) was last seen joining %s %s ago%s"),
    336 					target.c_str(), info->vhost.c_str(), info->channel.c_str(), timebuf.c_str(), onlinestatus.c_str());
    337 		}
    338 		else if (info->type == PART)
    339 		{
    340 			if (ShouldHide(info->channel, u2))
    341 				source.Reply(_("%s (%s) was last seen parting a secret channel %s ago%s"),
    342 					target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str());
    343 			else
    344 				source.Reply(_("%s (%s) was last seen parting %s %s ago%s"),
    345 					target.c_str(), info->vhost.c_str(), info->channel.c_str(), timebuf.c_str(), onlinestatus.c_str());
    346 		}
    347 		else if (info->type == QUIT)
    348 		{
    349 			source.Reply(_("%s (%s) was last seen quitting (%s) %s ago (%s)."),
    350 					target.c_str(), info->vhost.c_str(), info->message.c_str(), timebuf.c_str(), timebuf2.c_str());
    351 		}
    352 		else if (info->type == KICK)
    353 		{
    354 			if (ShouldHide(info->channel, u2))
    355 				source.Reply(_("%s (%s) was kicked from a secret channel %s ago%s"),
    356 					target.c_str(), info->vhost.c_str(), timebuf.c_str(), onlinestatus.c_str());
    357 			else
    358 				source.Reply(_("%s (%s) was kicked from %s (\"%s\") %s ago%s"),
    359 					target.c_str(), info->vhost.c_str(), info->channel.c_str(), info->message.c_str(), timebuf.c_str(), onlinestatus.c_str());
    360 		}
    361 	}
    362 
    363 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
    364 	{
    365 		this->SendSyntax(source);
    366 		source.Reply(" ");
    367 		source.Reply(_("Checks for the last time \037nick\037 was seen joining, leaving,\n"
    368 				"or changing nick on the network and tells you when and, depending\n"
    369 				"on channel or user settings, where it was."));
    370 		return true;
    371 	}
    372 };
    373 
    374 class CSSeen : public Module
    375 {
    376 	Serialize::Type seeninfo_type;
    377 	CommandSeen commandseen;
    378 	CommandOSSeen commandosseen;
    379  public:
    380 	CSSeen(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), seeninfo_type("SeenInfo", SeenInfo::Unserialize), commandseen(this), commandosseen(this)
    381 	{
    382 	}
    383 
    384 	void OnReload(Configuration::Conf *conf) anope_override
    385 	{
    386 		simple = conf->GetModule(this)->Get<bool>("simple");
    387 	}
    388 
    389 	void OnExpireTick() anope_override
    390 	{
    391 		size_t previous_size = database.size();
    392 		time_t purgetime = Config->GetModule(this)->Get<time_t>("purgetime");
    393 		if (!purgetime)
    394 			purgetime = Anope::DoTime("30d");
    395 		for (database_map::iterator it = database.begin(), it_end = database.end(); it != it_end;)
    396 		{
    397 			database_map::iterator cur = it;
    398 			++it;
    399 
    400 			if ((Anope::CurTime - cur->second->last) > purgetime)
    401 			{
    402 				Log(LOG_DEBUG) << cur->first << " was last seen " << Anope::strftime(cur->second->last) << ", purging entries";
    403 				delete cur->second;
    404 			}
    405 		}
    406 		Log(LOG_DEBUG) << "cs_seen: Purged database, checked " << previous_size << " nicks and removed " << (previous_size - database.size()) << " old entries.";
    407 	}
    408 
    409 	void OnUserConnect(User *u, bool &exempt) anope_override
    410 	{
    411 		if (!u->Quitting())
    412 			UpdateUser(u, NEW, u->nick, "", "", "");
    413 	}
    414 
    415 	void OnUserNickChange(User *u, const Anope::string &oldnick) anope_override
    416 	{
    417 		UpdateUser(u, NICK_TO, oldnick, u->nick, "", "");
    418 		UpdateUser(u, NICK_FROM, u->nick, oldnick, "", "");
    419 	}
    420 
    421 	void OnUserQuit(User *u, const Anope::string &msg) anope_override
    422 	{
    423 		UpdateUser(u, QUIT, u->nick, "", "", msg);
    424 	}
    425 
    426 	void OnJoinChannel(User *u, Channel *c) anope_override
    427 	{
    428 		UpdateUser(u, JOIN, u->nick, "", c->name, "");
    429 	}
    430 
    431 	void OnPartChannel(User *u, Channel *c, const Anope::string &channel, const Anope::string &msg) anope_override
    432 	{
    433 		UpdateUser(u, PART, u->nick, "", channel, msg);
    434 	}
    435 
    436 	void OnPreUserKicked(const MessageSource &source, ChanUserContainer *cu, const Anope::string &msg) anope_override
    437 	{
    438 		UpdateUser(cu->user, KICK, cu->user->nick, source.GetSource(), cu->chan->name, msg);
    439 	}
    440 
    441  private:
    442 	void UpdateUser(const User *u, const TypeInfo Type, const Anope::string &nick, const Anope::string &nick2, const Anope::string &channel, const Anope::string &message)
    443 	{
    444 		if (simple || !u->server->IsSynced())
    445 			return;
    446 
    447 		SeenInfo* &info = database[nick];
    448 		if (!info)
    449 			info = new SeenInfo();
    450 		info->nick = nick;
    451 		info->vhost = u->GetVIdent() + "@" + u->GetDisplayedHost();
    452 		info->type = Type;
    453 		info->last = Anope::CurTime;
    454 		info->nick2 = nick2;
    455 		info->channel = channel;
    456 		info->message = message;
    457 	}
    458 };
    459 
    460 MODULE_INIT(CSSeen)