1 /* cs_seen: provides a seen command by tracking all users
2  *
3  * (C) 2003-2020 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 
SeenInfoSeenInfo35 	SeenInfo() : Serializable("SeenInfo")
36 	{
37 	}
38 
~SeenInfoSeenInfo39 	~SeenInfo()
40 	{
41 		database_map::iterator iter = database.find(nick);
42 		if (iter != database.end() && iter->second == this)
43 			database.erase(iter);
44 	}
45 
SerializeSeenInfo46 	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 
UnserializeSeenInfo57 	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 
FindInfo(const Anope::string & nick)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 
ShouldHide(const Anope::string & channel,User * u)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:
CommandOSSeen(Module * creator)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 
Execute(CommandSource & source,const std::vector<Anope::string> & params)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 
OnHelp(CommandSource & source,const Anope::string & subcommand)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 {
SimpleSeen(CommandSource & source,const std::vector<Anope::string> & params)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:
CommandSeen(Module * creator)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 
Execute(CommandSource & source,const std::vector<Anope::string> & params)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 
OnHelp(CommandSource & source,const Anope::string & subcommand)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:
CSSeen(const Anope::string & modname,const Anope::string & creator)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 
OnReload(Configuration::Conf * conf)384 	void OnReload(Configuration::Conf *conf) anope_override
385 	{
386 		simple = conf->GetModule(this)->Get<bool>("simple");
387 	}
388 
OnExpireTick()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 
OnUserConnect(User * u,bool & exempt)409 	void OnUserConnect(User *u, bool &exempt) anope_override
410 	{
411 		if (!u->Quitting())
412 			UpdateUser(u, NEW, u->nick, "", "", "");
413 	}
414 
OnUserNickChange(User * u,const Anope::string & oldnick)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 
OnUserQuit(User * u,const Anope::string & msg)421 	void OnUserQuit(User *u, const Anope::string &msg) anope_override
422 	{
423 		UpdateUser(u, QUIT, u->nick, "", "", msg);
424 	}
425 
OnJoinChannel(User * u,Channel * c)426 	void OnJoinChannel(User *u, Channel *c) anope_override
427 	{
428 		UpdateUser(u, JOIN, u->nick, "", c->name, "");
429 	}
430 
OnPartChannel(User * u,Channel * c,const Anope::string & channel,const Anope::string & msg)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 
OnPreUserKicked(const MessageSource & source,ChanUserContainer * cu,const Anope::string & msg)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:
UpdateUser(const User * u,const TypeInfo Type,const Anope::string & nick,const Anope::string & nick2,const Anope::string & channel,const Anope::string & message)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)
461