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> ¶ms) 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> ¶ms)
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> ¶ms) 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