diff options
| author | Adam <Adam@anope.org> | 2014-01-03 19:54:14 -0500 |
|---|---|---|
| committer | Adam <Adam@anope.org> | 2014-01-03 19:54:14 -0500 |
| commit | e1ce6174cee90b71dc461be39d2bf20965ff69e0 (patch) | |
| tree | 6f8788ab40a533f3ae791f30cfe4a1f94fadf193 /modules/extra/stats | |
| parent | 2781b6946daa82b88b08cdd6ab3c6478f7c70aa1 (diff) | |
Move modules/stats under extras because it depends on m_mysql, update its config a bit to look similar to all of the other config files
Diffstat (limited to 'modules/extra/stats')
| -rw-r--r-- | modules/extra/stats/cs_fantasy_stats.cpp | 179 | ||||
| -rw-r--r-- | modules/extra/stats/cs_fantasy_top.cpp | 191 | ||||
| -rw-r--r-- | modules/extra/stats/irc2sql/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | modules/extra/stats/irc2sql/irc2sql.cpp | 285 | ||||
| -rw-r--r-- | modules/extra/stats/irc2sql/irc2sql.h | 77 | ||||
| -rw-r--r-- | modules/extra/stats/irc2sql/tables.cpp | 382 | ||||
| -rw-r--r-- | modules/extra/stats/irc2sql/utils.cpp | 61 | ||||
| -rw-r--r-- | modules/extra/stats/m_chanstats.cpp | 630 |
8 files changed, 1807 insertions, 0 deletions
diff --git a/modules/extra/stats/cs_fantasy_stats.cpp b/modules/extra/stats/cs_fantasy_stats.cpp new file mode 100644 index 000000000..43f7eb04a --- /dev/null +++ b/modules/extra/stats/cs_fantasy_stats.cpp @@ -0,0 +1,179 @@ +/* Chanstats core functions + * + * (C) 2003-2014 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/sql.h" + +class MySQLInterface : public SQL::Interface +{ + public: + MySQLInterface(Module *o) : SQL::Interface(o) { } + + void OnResult(const SQL::Result &r) anope_override + { + } + + void OnError(const SQL::Result &r) anope_override + { + if (!r.GetQuery().query.empty()) + Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError(); + else + Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError(); + } +}; + + +class CommandCSStats : public Command +{ + public: + CommandCSStats(Module *creator) : Command (creator, "chanserv/stats", 0, 2) + { + this->SetDesc(_("Displays your Channel Stats")); + this->SetSyntax(_("[\037channel\037] [\037nick\037]")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + +class CommandCSGStats : public Command +{ + public: + CommandCSGStats(Module *creator) : Command (creator, "chanserv/gstats", 0, 2) + { + this->SetDesc(_("Displays your Global Stats")); + this->SetSyntax(_("\037nick\037")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + + +class CSStats; +static CSStats *me; +class CSStats : public Module +{ + CommandCSStats commandcsstats; + CommandCSGStats commandcsgstats; + ServiceReference<SQL::Provider> sql; + MySQLInterface sqlinterface; + Anope::string prefix; + public: + CSStats(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandcsstats(this), commandcsgstats(this), sql("", ""), sqlinterface(this) + { + me = this; + + } + + void OnReload(Configuration::Conf *conf) anope_override + { + prefix = conf->GetModule("m_chanstats")->Get<const Anope::string>("prefix"); + if (prefix.empty()) + prefix = "anope_"; + this->sql = ServiceReference<SQL::Provider>("SQL::Provider", conf->GetModule("m_chanstats")->Get<const Anope::string>("engine")); + } + + SQL::Result RunQuery(const SQL::Query &query) + { + if (!this->sql) + throw SQL::Exception("Unable to locate SQL reference, is m_mysql loaded and configured correctly?"); + + SQL::Result res = this->sql->RunQuery(query); + if (!res.GetError().empty()) + throw SQL::Exception(res.GetError()); + return res; + } + + void DoStats(CommandSource &source, const bool is_global, const std::vector<Anope::string> ¶ms) + { + Anope::string display, channel; + + /* + * possible parameters are: + * stats [channel] [nick] + * stats [channel] + * stats [nick] + * stats + */ + + switch (params.size()) + { + case 2: + channel = params[0]; + display = params[1]; + break; + case 1: + if (params[0][0] == '#') + channel = params[0]; + else + { + if (NickAlias *na = NickAlias::Find(params[0])) + display = na->nc->display; + else + { + source.Reply(_("%s not found."), params[0].c_str()); + return; + } + } + break; + } + + if (display.empty()) + display = source.nc->display; + + try + { + SQL::Query query; + query = "SELECT letters, words, line, smileys_happy+smileys_sad+smileys_other as smileys," + "actions FROM `" + prefix + "chanstats` " + "WHERE `nick` = @nick@ AND `chan` = @channel@ AND `type` = 'total';"; + if (is_global || channel.empty()) + query.SetValue("channel", ""); + else + query.SetValue("channel", channel); + query.SetValue("nick", display); + SQL::Result res = this->RunQuery(query); + + if (res.Rows() > 0) + { + if (is_global) + source.Reply(_("Network stats for %s:"), display.c_str()); + else + source.Reply(_("Channel stats for %s on %s:"), display.c_str(), channel.c_str()); + + source.Reply(_("letters: %s, words: %s, lines: %s, smileys %s, actions: %s"), + res.Get(0, "letters").c_str(), res.Get(0, "words").c_str(), + res.Get(0, "line").c_str(), res.Get(0, "smileys").c_str(), + res.Get(0, "actions").c_str()); + } + else + source.Reply(_("No stats for %s."), display.c_str()); + } + catch (const SQL::Exception &ex) + { + Log(LOG_DEBUG) << ex.GetReason(); + } + + } +}; + +void CommandCSStats::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoStats(source, false, params); +} + +void CommandCSGStats::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoStats(source, true, params); +} + +MODULE_INIT(CSStats) + diff --git a/modules/extra/stats/cs_fantasy_top.cpp b/modules/extra/stats/cs_fantasy_top.cpp new file mode 100644 index 000000000..f747bebd8 --- /dev/null +++ b/modules/extra/stats/cs_fantasy_top.cpp @@ -0,0 +1,191 @@ +/* Chanstats core functions + * + * (C) 2003-2014 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/sql.h" + +class MySQLInterface : public SQL::Interface +{ + public: + MySQLInterface(Module *o) : SQL::Interface(o) { } + + void OnResult(const SQL::Result &r) anope_override + { + } + + void OnError(const SQL::Result &r) anope_override + { + if (!r.GetQuery().query.empty()) + Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError(); + else + Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError(); + } +}; + +class CommandCSTop : public Command +{ + public: + CommandCSTop(Module *creator) : Command (creator, "chanserv/top", 0, 2) + { + this->SetDesc(_("Displays the top 3 users of a channel")); + this->SetSyntax(_("\037channel\037")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + +class CommandCSTop10 : public Command +{ + public: + CommandCSTop10(Module *creator) : Command (creator, "chanserv/top10", 0, 2) + { + this->SetDesc(_("Displays the top 10 users of a channel")); + this->SetSyntax(_("\037channel\037")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + +class CommandCSGTop : public Command +{ + public: + CommandCSGTop(Module *creator) : Command (creator, "chanserv/gtop", 0, 1) + { + this->SetDesc(_("Displays the top 3 users of the network")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + +class CommandCSGTop10 : public Command +{ + public: + CommandCSGTop10(Module *creator) : Command (creator, "chanserv/gtop10", 0, 1) + { + this->SetDesc(_("Displays the top 10 users of the network")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms); +}; + + +class CSTop; +static CSTop *me; +class CSTop : public Module +{ + CommandCSTop commandcstop; + CommandCSGTop commandcsgtop; + CommandCSTop10 commandcstop10; + CommandCSGTop10 commandcsgtop10; + ServiceReference<SQL::Provider> sql; + MySQLInterface sqlinterface; + Anope::string prefix; + + public: + CSTop(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandcstop(this), commandcsgtop(this), commandcstop10(this), commandcsgtop10(this), sql("", ""), + sqlinterface(this) + { + me = this; + + } + + void OnReload(Configuration::Conf *conf) anope_override + { + prefix = conf->GetModule("m_chanstats")->Get<const Anope::string>("prefix"); + if (prefix.empty()) + prefix = "anope_"; + this->sql = ServiceReference<SQL::Provider>("SQL::Provider", conf->GetModule("m_chanstats")->Get<const Anope::string>("engine")); + } + + SQL::Result RunQuery(const SQL::Query &query) + { + if (!this->sql) + throw SQL::Exception("Unable to locate SQL reference, is m_mysql loaded and configured correctly?"); + + SQL::Result res = sql->RunQuery(query); + if (!res.GetError().empty()) + throw SQL::Exception(res.GetError()); + return res; + } + + void DoTop(CommandSource &source, const std::vector<Anope::string> ¶ms, bool is_global, int limit = 1) + { + + Anope::string channel; + if (!params.empty()) + channel = params[0]; + else if (source.c && source.c->ci) + channel = source.c->ci->name; + + if (!is_global && channel.empty()) + is_global = true; + + try + { + SQL::Query query; + query = "SELECT nick, letters, words, line, actions," + "smileys_happy+smileys_sad+smileys_other as smileys " + "FROM `" + prefix + "chanstats` " + "WHERE `nick` != '' AND `chan` = @channel@ AND `type` = 'total' " + "ORDER BY `letters` DESC LIMIT @limit@;"; + query.SetValue("limit", limit, false); + + if (is_global) + query.SetValue("channel", ""); + else + query.SetValue("channel", channel.c_str()); + + SQL::Result res = this->RunQuery(query); + + if (res.Rows() > 0) + { + source.Reply(_("Top %i of %s"), limit, (is_global ? "Network" : channel.c_str())); + for (int i = 0; i < res.Rows(); ++i) + { + source.Reply(_("%2lu \002%-16s\002 letters: %s, words: %s, lines: %s, smileys %s, actions: %s"), + i+1, res.Get(i, "nick").c_str(), res.Get(i, "letters").c_str(), + res.Get(i, "words").c_str(), res.Get(i, "line").c_str(), + res.Get(0, "smileys").c_str(), res.Get(0, "actions").c_str()); + } + } + else + source.Reply(_("No stats for %s."), is_global ? "Network" : channel.c_str()); + } + catch (const SQL::Exception &ex) + { + Log(LOG_DEBUG) << ex.GetReason(); + } + } +}; + +void CommandCSTop::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoTop(source, params, false, 3); +} + +void CommandCSTop10::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoTop(source, params, false, 10); +} + +void CommandCSGTop::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoTop(source, params, true, 3); +} + +void CommandCSGTop10::Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) +{ + me->DoTop(source, params, true, 10); +} + + +MODULE_INIT(CSTop) diff --git a/modules/extra/stats/irc2sql/CMakeLists.txt b/modules/extra/stats/irc2sql/CMakeLists.txt new file mode 100644 index 000000000..9d36c6e8d --- /dev/null +++ b/modules/extra/stats/irc2sql/CMakeLists.txt @@ -0,0 +1,2 @@ +build_subdir(${CMAKE_CURRENT_SOURCE_DIR}) + diff --git a/modules/extra/stats/irc2sql/irc2sql.cpp b/modules/extra/stats/irc2sql/irc2sql.cpp new file mode 100644 index 000000000..a537f184c --- /dev/null +++ b/modules/extra/stats/irc2sql/irc2sql.cpp @@ -0,0 +1,285 @@ +#include "irc2sql.h" + +void IRC2SQL::OnShutdown() anope_override +{ + // TODO: test if we really have to use blocking query here + // (sometimes m_mysql get unloaded before the other thread executed all queries) + SQL::Result r = this->sql->RunQuery(SQL::Query("CALL " + prefix + "OnShutdown()")); + quitting = true; +} + +void IRC2SQL::OnReload(Configuration::Conf *conf) anope_override +{ + Configuration::Block *block = Config->GetModule(this); + prefix = block->Get<const Anope::string>("prefix", "anope_"); + UseGeoIP = block->Get<bool>("GeoIPLookup", "no"); + GeoIPDB = block->Get<const Anope::string>("GeoIPDatabase", "country"); + ctcpuser = block->Get<bool>("ctcpuser", "no"); + ctcpeob = block->Get<bool>("ctcpeob", "yes"); + Anope::string engine = block->Get<const Anope::string>("engine"); + this->sql = ServiceReference<SQL::Provider>("SQL::Provider", engine); + if (sql) + this->CheckTables(); + else + Log() << "IRC2SQL: no database connection to " << engine; + + const Anope::string &snick = block->Get<const Anope::string>("client"); + if (snick.empty()) + throw ConfigException(Module::name + ": <client> must be defined"); + StatServ = BotInfo::Find(snick, true); + if (!StatServ) + throw ConfigException(Module::name + ": no bot named " + snick); + + if (firstrun) + { + firstrun = false; + + for (Anope::map<Server *>::const_iterator it = Servers::ByName.begin(); it != Servers::ByName.end(); ++it) + { + this->OnNewServer(it->second); + } + + for (channel_map::const_iterator it = ChannelList.begin(), it_end = ChannelList.end(); it != it_end; ++it) + { + this->OnChannelCreate(it->second); + } + + for (user_map::const_iterator it = UserListByNick.begin(); it != UserListByNick.end(); ++it) + { + User *u = it->second; + bool exempt = false; + this->OnUserConnect(u, exempt); + for (User::ChanUserList::const_iterator cit = u->chans.begin(), cit_end = u->chans.end(); cit != cit_end; ++cit) + { + this->OnJoinChannel(u, cit->second->chan); + } + } + } + +} + +void IRC2SQL::OnNewServer(Server *server) anope_override +{ + query = "INSERT DELAYED INTO `" + prefix + "server` (name, hops, comment, link_time, online, ulined) " + "VALUES (@name@, @hops@, @comment@, now(), 'Y', @ulined@) " + "ON DUPLICATE KEY UPDATE name=VALUES(name), hops=VALUES(hops), comment=VALUES(comment), " + "link_time=VALUES(link_time), online=VALUES(online), ulined=(ulined)"; + query.SetValue("name", server->GetName()); + query.SetValue("hops", server->GetHops()); + query.SetValue("comment", server->GetDescription()); + query.SetValue("ulined", server->IsULined() ? "Y" : "N"); + this->RunQuery(query); +} + +void IRC2SQL::OnServerQuit(Server *server) anope_override +{ + if (quitting) + return; + + query = "CALL " + prefix + "ServerQuit(@name@)"; + query.SetValue("name", server->GetName()); + this->RunQuery(query); +} + +void IRC2SQL::OnUserConnect(User *u, bool &exempt) anope_override +{ + if (!introduced_myself) + { + this->OnNewServer(Me); + introduced_myself = true; + } + + query = "CALL " + prefix + "UserConnect(@nick@,@host@,@vhost@,@chost@,@realname@,@ip@,@ident@,@vident@," + "@account@,@secure@,@fingerprint@,@signon@,@server@,@uuid@,@modes@,@oper@)"; + query.SetValue("nick", u->nick); + query.SetValue("host", u->host); + query.SetValue("vhost", u->vhost); + query.SetValue("chost", u->chost); + query.SetValue("realname", u->realname); + query.SetValue("ip", u->ip); + query.SetValue("ident", u->GetIdent()); + query.SetValue("vident", u->GetVIdent()); + query.SetValue("secure", u->HasMode("SSL") || u->HasExt("ssl") ? "Y" : "N"); + query.SetValue("account", u->Account() ? u->Account()->display : ""); + query.SetValue("fingerprint", u->fingerprint); + query.SetValue("signon", u->signon); + query.SetValue("server", u->server->GetName()); + query.SetValue("uuid", u->GetUID()); + query.SetValue("modes", u->GetModes()); + query.SetValue("oper", u->HasMode("OPER") ? "Y" : "N"); + this->RunQuery(query); + + if (ctcpuser && (Me->IsSynced() || ctcpeob) && u->server != Me) + IRCD->SendPrivmsg(StatServ, u->GetUID(), "\1VERSION\1"); + +} + +void IRC2SQL::OnUserQuit(User *u, const Anope::string &msg) anope_override +{ + if (quitting || u->server->IsQuitting()) + return; + + query = "CALL " + prefix + "UserQuit(@nick@)"; + query.SetValue("nick", u->nick); + this->RunQuery(query); +} + +void IRC2SQL::OnUserNickChange(User *u, const Anope::string &oldnick) anope_override +{ + query = "UPDATE `" + prefix + "user` SET nick=@newnick@ WHERE nick=@oldnick@"; + query.SetValue("newnick", u->nick); + query.SetValue("oldnick", oldnick); + this->RunQuery(query); +} + +void IRC2SQL::OnFingerprint(User *u) anope_override +{ + query = "UPDATE `" + prefix + "user` SET secure=@secure@, fingerprint=@fingerprint@ WHERE nick=@nick@"; + query.SetValue("secure", u->HasMode("SSL") || u->HasExt("ssl") ? "Y" : "N"); + query.SetValue("fingerprint", u->fingerprint); + query.SetValue("nick", u->nick); + this->RunQuery(query); +} + +void IRC2SQL::OnUserModeSet(User *u, const Anope::string &mname) anope_override +{ + query = "UPDATE `" + prefix + "user` SET modes=@modes@, oper=@oper@ WHERE nick=@nick@"; + query.SetValue("nick", u->nick); + query.SetValue("modes", u->GetModes()); + query.SetValue("oper", u->HasMode("OPER") ? "Y" : "N"); + this->RunQuery(query); +} + +void IRC2SQL::OnUserModeUnset(User *u, const Anope::string &mname) anope_override +{ + this->OnUserModeSet(u, mname); +} + +void IRC2SQL::OnUserLogin(User *u) +{ + query = "UPDATE `" + prefix + "user` SET account=@account@ WHERE nick=@nick@"; + query.SetValue("nick", u->nick); + query.SetValue("account", u->Account() ? u->Account()->display : ""); + this->RunQuery(query); +} + +void IRC2SQL::OnNickLogout(User *u) anope_override +{ + this->OnUserLogin(u); +} + +void IRC2SQL::OnSetDisplayedHost(User *u) anope_override +{ + query = "UPDATE `" + prefix + "user` " + "SET vhost=@vhost@ " + "WHERE nick=@nick@"; + query.SetValue("vhost", u->GetDisplayedHost()); + query.SetValue("nick", u->nick); + this->RunQuery(query); +} + +void IRC2SQL::OnChannelCreate(Channel *c) anope_override +{ + query = "INSERT INTO `" + prefix + "chan` (channel, topic, topicauthor, topictime, modes) " + "VALUES (@channel@,@topic@,@topicauthor@,@topictime@,@modes@) " + "ON DUPLICATE KEY UPDATE channel=VALUES(channel), topic=VALUES(topic)," + "topicauthor=VALUES(topicauthor), topictime=VALUES(topictime), modes=VALUES(modes)"; + query.SetValue("channel", c->name); + query.SetValue("topic", c->topic); + query.SetValue("topicauthor", c->topic_setter); + query.SetValue("topictime", c->topic_ts); + query.SetValue("modes", c->GetModes(true,true)); + this->RunQuery(query); +} + +void IRC2SQL::OnChannelDelete(Channel *c) anope_override +{ + query = "DELETE FROM `" + prefix + "chan` WHERE channel=@channel@"; + query.SetValue("channel", c->name); + this->RunQuery(query); +} + +void IRC2SQL::OnJoinChannel(User *u, Channel *c) anope_override +{ + Anope::string modes; + ChanUserContainer *cu = u->FindChannel(c); + if (cu) + modes = cu->status.Modes(); + + query = "CALL " + prefix + "JoinUser(@nick@,@channel@,@modes@)"; + query.SetValue("nick", u->nick); + query.SetValue("channel", c->name); + query.SetValue("modes", modes); + this->RunQuery(query); +} + +EventReturn IRC2SQL::OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) anope_override +{ + query = "UPDATE `" + prefix + "chan` SET modes=@modes@ WHERE channel=@channel@"; + query.SetValue("channel", c->name); + query.SetValue("modes", c->GetModes(true,true)); + this->RunQuery(query); + return EVENT_CONTINUE; +} + +EventReturn IRC2SQL::OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) anope_override +{ + this->OnChannelModeSet(c, setter, mode, param); + return EVENT_CONTINUE; +} + +void IRC2SQL::OnLeaveChannel(User *u, Channel *c) anope_override +{ + if (quitting) + return; + /* + * user is quitting, we already received a OnUserQuit() + * at this point the user is already removed from SQL and all channels + */ + if (u->Quitting()) + return; + query = "CALL " + prefix + "PartUser(@nick@,@channel@)"; + query.SetValue("nick", u->nick); + query.SetValue("channel", c->name); + this->RunQuery(query); +} + +void IRC2SQL::OnTopicUpdated(Channel *c, const Anope::string &user, const Anope::string &topic) anope_override +{ + query = "UPDATE `" + prefix + "chan` " + "SET topic=@topic@, topicauthor=@author@, topictime=FROM_UNIXTIME(@time@) " + "WHERE channel=@channel@"; + query.SetValue("topic", c->topic); + query.SetValue("author", c->topic_setter); + query.SetValue("time", c->topic_ts); + query.SetValue("channel", c->name); + this->RunQuery(query); +} + +void IRC2SQL::OnBotNotice(User *u, BotInfo *bi, Anope::string &message) anope_override +{ + Anope::string versionstr; + if (bi != StatServ) + return; + if (message[0] == '\1' && message[message.length() - 1] == '\1') + { + if (message.substr(0, 9).equals_ci("\1VERSION ")) + { + if (u->HasExt("CTCPVERSION")) + return; + u->Extend<bool>("CTCPVERSION"); + + versionstr = Anope::NormalizeBuffer(message.substr(9, message.length() - 10)); + if (versionstr.empty()) + return; + query = "UPDATE `" + prefix + "user` " + "SET version=@version@ " + "WHERE nick=@nick@"; + query.SetValue("version", versionstr); + query.SetValue("nick", u->nick); + this->RunQuery(query); + } + } +} + +MODULE_INIT(IRC2SQL) diff --git a/modules/extra/stats/irc2sql/irc2sql.h b/modules/extra/stats/irc2sql/irc2sql.h new file mode 100644 index 000000000..b5fc8c37f --- /dev/null +++ b/modules/extra/stats/irc2sql/irc2sql.h @@ -0,0 +1,77 @@ +#include "module.h" +#include "modules/sql.h" + +class MySQLInterface : public SQL::Interface +{ + public: + MySQLInterface(Module *o) : SQL::Interface(o) { } + + void OnResult(const SQL::Result &r) anope_override + { + } + + void OnError(const SQL::Result &r) anope_override + { + if (!r.GetQuery().query.empty()) + Log(LOG_DEBUG) << "m_irc2sql: Error executing query " << r.finished_query << ": " << r.GetError(); + else + Log(LOG_DEBUG) << "m_irc2sql: Error executing query: " << r.GetError(); + } +}; + +class IRC2SQL : public Module +{ + ServiceReference<SQL::Provider> sql; + MySQLInterface sqlinterface; + SQL::Query query; + std::vector<Anope::string> TableList, ProcedureList, EventList; + Anope::string prefix, GeoIPDB; + bool quitting, introduced_myself, UseGeoIP, ctcpuser, ctcpeob, firstrun; + BotInfo *StatServ; + PrimitiveExtensibleItem<bool> versionreply; + + void RunQuery(const SQL::Query &q); + void GetTables(); + + bool HasTable(const Anope::string &table); + bool HasProcedure(const Anope::string &table); + bool HasEvent(const Anope::string &table); + + void CheckTables(); + + public: + IRC2SQL(const Anope::string &modname, const Anope::string &creator) : + Module(modname, creator, EXTRA | VENDOR), sql("", ""), sqlinterface(this), versionreply(this, "CTCPVERSION") + { + firstrun = true; + quitting = false; + introduced_myself = false; + } + + void OnShutdown() anope_override; + void OnReload(Configuration::Conf *config) anope_override; + void OnNewServer(Server *server) anope_override; + void OnServerQuit(Server *server) anope_override; + void OnUserConnect(User *u, bool &exempt) anope_override; + void OnUserQuit(User *u, const Anope::string &msg) anope_override; + void OnUserNickChange(User *u, const Anope::string &oldnick) anope_override; + void OnFingerprint(User *u) anope_override; + void OnUserModeSet(User *u, const Anope::string &mname) anope_override; + void OnUserModeUnset(User *u, const Anope::string &mname) anope_override; + void OnUserLogin(User *u) anope_override; + void OnNickLogout(User *u) anope_override; + void OnSetDisplayedHost(User *u) anope_override; + + void OnChannelCreate(Channel *c) anope_override; + void OnChannelDelete(Channel *c) anope_override; + void OnLeaveChannel(User *u, Channel *c) anope_override; + void OnJoinChannel(User *u, Channel *c) anope_override; + EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) anope_override; + EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) anope_override; + + void OnTopicUpdated(Channel *c, const Anope::string &user, const Anope::string &topic) anope_override; + + void OnBotNotice(User *u, BotInfo *bi, Anope::string &message) anope_override; +}; + + diff --git a/modules/extra/stats/irc2sql/tables.cpp b/modules/extra/stats/irc2sql/tables.cpp new file mode 100644 index 000000000..d80f37535 --- /dev/null +++ b/modules/extra/stats/irc2sql/tables.cpp @@ -0,0 +1,382 @@ +#include "irc2sql.h" + +void IRC2SQL::CheckTables() +{ + Anope::string geoquery(""); + + if (firstrun) + { + /* + * reset some tables to make sure they are really empty + */ + this->sql->RunQuery("TRUNCATE TABLE " + prefix + "user"); + this->sql->RunQuery("TRUNCATE TABLE " + prefix + "chan"); + this->sql->RunQuery("TRUNCATE TABLE " + prefix + "ison"); + this->sql->RunQuery("UPDATE `" + prefix + "server` SET currentusers=0, online='N'"); + } + + this->GetTables(); + + if (UseGeoIP && GeoIPDB.equals_ci("country") && !this->HasTable(prefix + "geoip_country")) + { + query = "CREATE TABLE `" + prefix + "geoip_country` (" + "`start` INT UNSIGNED NOT NULL," + "`end` INT UNSIGNED NOT NULL," + "`countrycode` varchar(2)," + "`countryname` varchar(50)," + "PRIMARY KEY `end` (`end`)," + "KEY `start` (`start`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (UseGeoIP && GeoIPDB.equals_ci("city") && !this->HasTable(prefix + "geoip_city_blocks")) + { + query = "CREATE TABLE `" + prefix + "geoip_city_blocks` (" + "`start` INT UNSIGNED NOT NULL," + "`end` INT UNSIGNED NOT NULL," + "`locId` INT UNSIGNED NOT NULL," + "PRIMARY KEY `end` (`end`)," + "KEY `start` (`start`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + + } + if (UseGeoIP && GeoIPDB.equals_ci("city") && !this->HasTable(prefix + "geoip_city_location")) + { + query = "CREATE TABLE `" + prefix + "geoip_city_location` (" + "`locId` INT UNSIGNED NOT NULL," + "`country` CHAR(2) NOT NULL," + "`region` CHAR(2) NOT NULL," + "`city` VARCHAR(50)," + "`latitude` FLOAT," + "`longitude` FLOAT," + "`areaCode` INT," + "PRIMARY KEY (`locId`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (UseGeoIP && GeoIPDB.equals_ci("city") && !this->HasTable(prefix + "geoip_city_region")) + { query = "CREATE TABLE `" + prefix + "geoip_city_region` (" + "`country` CHAR(2) NOT NULL," + "`region` CHAR(2) NOT NULL," + "`regionname` VARCHAR(100) NOT NULL," + "PRIMARY KEY (`country`,`region`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (!this->HasTable(prefix + "server")) + { + query = "CREATE TABLE `" + prefix + "server` (" + "`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT," + "`name` varchar(64) NOT NULL," + "`hops` tinyint(3) NOT NULL," + "`comment` varchar(255) NOT NULL," + "`link_time` datetime DEFAULT NULL," + "`split_time` datetime DEFAULT NULL," + "`version` varchar(127) NOT NULL," + "`currentusers` int(15) NOT NULL," + "`online` enum('Y','N') NOT NULL DEFAULT 'Y'," + "`ulined` enum('Y','N') NOT NULL DEFAULT 'N'," + "PRIMARY KEY (`id`)," + "UNIQUE KEY `name` (`name`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (!this->HasTable(prefix + "chan")) + { + query = "CREATE TABLE `" + prefix + "chan` (" + "`chanid` int(11) UNSIGNED NOT NULL AUTO_INCREMENT," + "`channel` varchar(255) NOT NULL," + "`currentusers` int(15) NOT NULL DEFAULT 0," + "`topic` varchar(255) DEFAULT NULL," + "`topicauthor` varchar(255) DEFAULT NULL," + "`topictime` datetime DEFAULT NULL," + "`modes` varchar(512) DEFAULT NULL," + "PRIMARY KEY (`chanid`)," + "UNIQUE KEY `channel`(`channel`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (!this->HasTable(prefix + "user")) + { + query = "CREATE TABLE `" + prefix + "user` (" + "`nickid` int(11) UNSIGNED NOT NULL AUTO_INCREMENT," + "`nick` varchar(255) NOT NULL DEFAULT ''," + "`host` varchar(255) NOT NULL DEFAULT ''," + "`vhost` varchar(255) NOT NULL DEFAULT ''," + "`chost` varchar(255) NOT NULL DEFAULT ''," + "`realname` varchar(255) NOT NULL DEFAULT ''," + "`ip` varchar(255) NOT NULL DEFAULT ''," + "`ident` varchar(32) NOT NULL DEFAULT ''," + "`vident` varchar(32) NOT NULL DEFAULT ''," + "`modes` varchar(255) NOT NULL DEFAULT ''," + "`account` varchar(255) NOT NULL DEFAULT ''," + "`secure` enum('Y','N') NOT NULL DEFAULT 'N'," + "`fingerprint` varchar(128) NOT NULL DEFAULT ''," + "`signon` datetime DEFAULT NULL," + "`server` varchar(255) NOT NULL DEFAULT ''," + "`servid` int(11) UNSIGNED NOT NULL DEFAULT '0'," + "`uuid` varchar(32) NOT NULL DEFAULT ''," + "`oper` enum('Y','N') NOT NULL DEFAULT 'N'," + "`away` enum('Y','N') NOT NULL DEFAULT 'N'," + "`awaymsg` varchar(255) NOT NULL DEFAULT ''," + "`version` varchar(255) NOT NULL DEFAULT ''," + "`geocode` varchar(16) NOT NULL DEFAULT ''," + "`geocountry` varchar(64) NOT NULL DEFAULT ''," + "`georegion` varchar(100) NOT NULL DEFAULT ''," + "`geocity` varchar(128) NOT NULL DEFAULT ''," + "`locId` INT UNSIGNED," + "PRIMARY KEY (`nickid`)," + "UNIQUE KEY `nick` (`nick`)," + "KEY `servid` (`servid`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (!this->HasTable(prefix + "ison")) + { + query = "CREATE TABLE `" + prefix + "ison` (" + "`nickid` int(11) unsigned NOT NULL default '0'," + "`chanid` int(11) unsigned NOT NULL default '0'," + "`modes` varchar(255) NOT NULL default ''," + "PRIMARY KEY (`nickid`,`chanid`)," + "KEY `modes` (`modes`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (!this->HasTable(prefix + "maxusers")) + { + query = "CREATE TABLE `" + prefix + "maxusers` (" + "`name` VARCHAR(255) NOT NULL," + "`maxusers` INT(15) NOT NULL," + "`maxtime` DATETIME NOT NULL," + "`lastused` DATETIME NOT NULL," + "UNIQUE KEY `name` (`name`)" + ") ENGINE=MyISAM DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + if (this->HasProcedure(prefix + "UserConnect")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "UserConnect")); + + if (UseGeoIP) + { + if (GeoIPDB.equals_ci("country")) + geoquery = "UPDATE `" + prefix + "user` AS u " + "JOIN ( SELECT `countrycode`, `countryname` " + "FROM `" + prefix + "geoip_country` " + "WHERE INET_ATON(ip_) <= `end` " + "AND `start` <= INET_ATON(ip_) " + "ORDER BY `end` ASC LIMIT 1 ) as c " + "SET u.geocode = c.countrycode, u.geocountry = c.countryname " + "WHERE u.nick = nick_; "; + else if (GeoIPDB.equals_ci("city")) + geoquery = "UPDATE `" + prefix + "user` as u " + "JOIN ( SELECT * FROM `" + prefix + "geoip_city_location` " + "WHERE `locID` = ( SELECT `locID` " + "FROM `" + prefix + "geoip_city_blocks` " + "WHERE INET_ATON(ip_) <= `end` " + "AND `start` <= INET_ATON(ip_) " + "ORDER BY `end` ASC LIMIT 1 ) " + ") as l " + "SET u.geocode = l.country, " + "u.geocity = l.city, " + "u.locID = l.locID, " + "u.georegion = ( SELECT `regionname` " + "FROM `" + prefix + "geoip_city_region` " + "WHERE `country` = l.country " + "AND `region` = l.region )" + "WHERE u.nick = nick_;"; + } + query = "CREATE PROCEDURE `" + prefix + "UserConnect`" + "(nick_ varchar(255), host_ varchar(255), vhost_ varchar(255), " + "chost_ varchar(255), realname_ varchar(255), ip_ varchar(255), " + "ident_ varchar(255), vident_ varchar(255), account_ varchar(255), " + "secure_ enum('Y','N'), fingerprint_ varchar(255), signon_ int(15), " + "server_ varchar(255), uuid_ varchar(32), modes_ varchar(255), " + "oper_ enum('Y','N')) " + "BEGIN " + "DECLARE cur int(15);" + "DECLARE max int(15);" + "INSERT INTO `" + prefix + "user` " + "(nick, host, vhost, chost, realname, ip, ident, vident, account, " + "secure, fingerprint, signon, server, uuid, modes, oper) " + "VALUES (nick_, host_, vhost_, chost_, realname_, ip_, ident_, vident_, " + "account_, secure_, fingerprint_, FROM_UNIXTIME(signon_), server_, " + "uuid_, modes_, oper_) " + "ON DUPLICATE KEY UPDATE host=VALUES(host), vhost=VALUES(vhost), " + "chost=VALUES(chost), realname=VALUES(realname), ip=VALUES(ip), " + "ident=VALUES(ident), vident=VALUES(vident), account=VALUES(account), " + "secure=VALUES(secure), fingerprint=VALUES(fingerprint), signon=VALUES(signon), " + "server=VALUES(server), uuid=VALUES(uuid), modes=VALUES(modes), " + "oper=VALUES(oper);" + "UPDATE `" + prefix + "user` as `u`, `" + prefix + "server` as `s`" + "SET u.servid = s.id, " + "s.currentusers = s.currentusers + 1 " + "WHERE s.name = server_ AND u.nick = nick_;" + "SELECT `currentusers` INTO cur FROM `" + prefix + "server` WHERE name=server_;" + "SELECT `maxusers` INTO max FROM `" + prefix + "maxusers` WHERE name=server_;" + "IF found_rows() AND cur <= max THEN " + "UPDATE `" + prefix + "maxusers` SET lastused=now() WHERE name=server_;" + "ELSE " + "INSERT INTO `" + prefix + "maxusers` (name, maxusers, maxtime, lastused) " + "VALUES ( server_, cur, now(), now() ) " + "ON DUPLICATE KEY UPDATE " + "name=VALUES(name), maxusers=VALUES(maxusers)," + "maxtime=VALUES(maxtime), lastused=VALUES(lastused);" + "END IF;" + + geoquery + + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "ServerQuit")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "ServerQuit")); + query = "CREATE PROCEDURE " + prefix + "ServerQuit(sname_ varchar(255)) " + "BEGIN " + /* 1. + * loop through all channels and decrease the user count + * by the number of users that are on this channel AND + * on the splitting server + * + * we dont have to care about channels that get empty, there will be + * an extra OnChannelDelete event triggered from anope. + */ + "DECLARE no_more_rows BOOLEAN DEFAULT FALSE;" + "DECLARE channel_ varchar(255);" + "DECLARE ucount_ int;" + "DECLARE channel_cursor CURSOR FOR " + "SELECT c.channel " + "FROM `" + prefix + "chan` as c, `" + prefix + "ison` as i, " + "`" + prefix + "user` as u, `" + prefix + "server` as s " + "WHERE c.chanid = i.chanid " + "AND i.nickid = u.nickid " + "AND u.servid = s.id " + "AND s.name = sname_;" + "DECLARE CONTINUE HANDLER FOR NOT FOUND " + "SET no_more_rows = TRUE;" + "OPEN channel_cursor;" + "the_loop: LOOP " + "FETCH channel_cursor INTO channel_;" + "IF no_more_rows THEN " + "CLOSE channel_cursor;" + "LEAVE the_loop;" + "END IF;" + "SELECT COUNT(*) INTO ucount_ " + "FROM `" + prefix + "ison` AS i, `" + prefix + "chan` AS c," + "`" + prefix + "user` AS u, `" + prefix + "server` AS s " + "WHERE i.nickid = u.nickid " + "AND u.servid = s.id " + "AND i.chanid = c.chanid " + "AND c.channel = channel_ " + "AND s.name = sname_; " + "UPDATE `" + prefix + "chan` " + "SET currentusers = currentusers - ucount_ " + "WHERE channel = channel_;" + "END LOOP;" + + /* 2. + * remove all users on the splitting server from the ison table + */ + "DELETE i FROM `" + prefix + "ison` AS i " + "INNER JOIN `" + prefix + "server` AS s " + "INNER JOIN `" + prefix + "user` AS u " + "WHERE i.nickid = u.nickid " + "AND u.servid = s.id " + "AND s.name = sname_;" + + /* 3. + * remove all users on the splitting server from the user table + */ + "DELETE u FROM `" + prefix + "user` AS u " + "INNER JOIN `" + prefix + "server` AS s " + "WHERE s.id = u.servid " + "AND s.name = sname_;" + + /* 4. + * on the splitting server, set usercount = 0, split_time = now(), online = 'N' + */ + "UPDATE `" + prefix + "server` SET currentusers = 0, split_time = now(), online = 'N' " + "WHERE name = sname_;" + "END;"; // end of the procedure + this->RunQuery(query); + + + if (this->HasProcedure(prefix + "UserQuit")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "UserQuit")); + query = "CREATE PROCEDURE `" + prefix + "UserQuit`" + "(nick_ varchar(255)) " + "BEGIN " + /* decrease usercount on the server where the user was on */ + "UPDATE `" + prefix + "user` AS `u`, `" + prefix + "server` AS `s` " + "SET s.currentusers = s.currentusers - 1 " + "WHERE u.nick=nick_ AND u.servid = s.id; " + /* decrease the usercount on all channels where the user was on */ + "UPDATE `" + prefix + "user` AS u, `" + prefix + "ison` AS i, " + "`" + prefix + "chan` AS c " + "SET c.currentusers = c.currentusers - 1 " + "WHERE u.nick=nick_ AND u.nickid = i.nickid " + "AND i.chanid = c.chanid; " + /* remove from all channels where the user was on */ + "DELETE i FROM `" + prefix + "ison` AS i " + "INNER JOIN `" + prefix + "user` as u " + "WHERE u.nick = nick_ " + "AND i.nickid = u.nickid;" + /* remove the user from the user table */ + "DELETE FROM `" + prefix + "user` WHERE nick = nick_; " + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "ShutDown")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "ShutDown")); + query = "CREATE PROCEDURE `" + prefix + "ShutDown`()" + "BEGIN " + "UPDATE `" + prefix + "server` " + "SET currentusers=0, online='N', split_time=now();" + "TRUNCATE TABLE `" + prefix + "user`;" + "TRUNCATE TABLE `" + prefix + "chan`;" + "TRUNCATE TABLE `" + prefix + "ison`;" + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "JoinUser")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "JoinUser")); + query = "CREATE PROCEDURE `"+ prefix + "JoinUser`" + "(nick_ varchar(255), channel_ varchar(255), modes_ varchar(255)) " + "BEGIN " + "DECLARE cur int(15);" + "DECLARE max int(15);" + "INSERT INTO `" + prefix + "ison` (nickid, chanid, modes) " + "SELECT u.nickid, c.chanid, modes_ " + "FROM " + prefix + "user AS u, " + prefix + "chan AS c " + "WHERE u.nick=nick_ AND c.channel=channel_;" + "UPDATE `" + prefix + "chan` SET currentusers=currentusers+1 " + "WHERE channel=channel_;" + "SELECT `currentusers` INTO cur FROM `" + prefix + "chan` WHERE channel=channel_;" + "SELECT `maxusers` INTO max FROM `" + prefix + "maxusers` WHERE name=channel_;" + "IF found_rows() AND cur <= max THEN " + "UPDATE `" + prefix + "maxusers` SET lastused=now() WHERE name=channel_;" + "ELSE " + "INSERT INTO `" + prefix + "maxusers` (name, maxusers, maxtime, lastused) " + "VALUES ( channel_, cur, now(), now() ) " + "ON DUPLICATE KEY UPDATE " + "name=VALUES(name), maxusers=VALUES(maxusers)," + "maxtime=VALUES(maxtime), lastused=VALUES(lastused);" + "END IF;" + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "PartUser")) + this->RunQuery(SQL::Query("DROP PROCEDURE " + prefix + "PartUser")); + query = "CREATE PROCEDURE `" + prefix + "PartUser`" + "(nick_ varchar(255), channel_ varchar(255)) " + "BEGIN " + "DELETE i FROM `" + prefix + "ison` AS i " + "INNER JOIN `" + prefix + "user` AS u " + "INNER JOIN `" + prefix + "chan` AS c " + "WHERE i.nickid = u.nickid " + "AND u.nick = nick_ " + "AND i.chanid = c.chanid " + "AND c.channel = channel_;" + "UPDATE `" + prefix + "chan` SET currentusers=currentusers-1 " + "WHERE channel=channel_;" + "END"; + this->RunQuery(query); +} diff --git a/modules/extra/stats/irc2sql/utils.cpp b/modules/extra/stats/irc2sql/utils.cpp new file mode 100644 index 000000000..0c35b13fa --- /dev/null +++ b/modules/extra/stats/irc2sql/utils.cpp @@ -0,0 +1,61 @@ +#include "irc2sql.h" + +void IRC2SQL::RunQuery(const SQL::Query &q) +{ + if (sql) + sql->Run(&sqlinterface, q); +} + +void IRC2SQL::GetTables() +{ + TableList.clear(); + ProcedureList.clear(); + EventList.clear(); + if (!sql) + return; + + SQL::Result r = this->sql->RunQuery(this->sql->GetTables(prefix)); + for (int i = 0; i < r.Rows(); ++i) + { + const std::map<Anope::string, Anope::string> &map = r.Row(i); + for (std::map<Anope::string, Anope::string>::const_iterator it = map.begin(); it != map.end(); ++it) + TableList.push_back(it->second); + } + query = "SHOW PROCEDURE STATUS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + ProcedureList.push_back(r.Get(i, "Name")); + } + query = "SHOW EVENTS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + EventList.push_back(r.Get(i, "Name")); + } +} + +bool IRC2SQL::HasTable(const Anope::string &table) +{ + for (std::vector<Anope::string>::const_iterator it = TableList.begin(); it != TableList.end(); ++it) + if (*it == table) + return true; + return false; +} + +bool IRC2SQL::HasProcedure(const Anope::string &table) +{ + for (std::vector<Anope::string>::const_iterator it = ProcedureList.begin(); it != ProcedureList.end(); ++it) + if (*it == table) + return true; + return false; +} + +bool IRC2SQL::HasEvent(const Anope::string &table) +{ + for (std::vector<Anope::string>::const_iterator it = EventList.begin(); it != EventList.end(); ++it) + if (*it == table) + return true; + return false; +} + diff --git a/modules/extra/stats/m_chanstats.cpp b/modules/extra/stats/m_chanstats.cpp new file mode 100644 index 000000000..13958dcc3 --- /dev/null +++ b/modules/extra/stats/m_chanstats.cpp @@ -0,0 +1,630 @@ +#include "module.h" +#include "modules/sql.h" + +class CommandCSSetChanstats : public Command +{ + public: + CommandCSSetChanstats(Module *creator) : Command(creator, "chanserv/set/chanstats", 2, 2) + { + this->SetDesc(_("Turn chanstats statistics on or off")); + this->SetSyntax(_("\037channel\037 {ON | OFF}")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) anope_override + { + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (!ci) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetChannelOption, MOD_RESULT, (source, this, ci, params[1])); + if (MOD_RESULT == EVENT_STOP) + return; + + if (MOD_RESULT != EVENT_ALLOW && !source.AccessFor(ci).HasPriv("SET") && source.permission.empty() && !source.HasPriv("chanserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (params[1].equals_ci("ON")) + { + ci->Extend<bool>("CS_STATS"); + source.Reply(_("Chanstats statistics are now enabled for this channel.")); + Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to enable chanstats"; + } + else if (params[1].equals_ci("OFF")) + { + Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to disable chanstats"; + ci->Shrink<bool>("CS_STATS"); + source.Reply(_("Chanstats statistics are now disabled for this channel.")); + } + else + this->OnSyntaxError(source, ""); + } + + bool OnHelp(CommandSource &source, const Anope::string &) anope_override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply("Turn Chanstats channel statistics ON or OFF."); + return true; + } +}; + +class CommandNSSetChanstats : public Command +{ + public: + CommandNSSetChanstats(Module *creator, const Anope::string &sname = "nickserv/set/chanstats", size_t min = 1 ) : Command(creator, sname, min, min + 1) + { + this->SetDesc(_("Turn chanstats statistics on or off")); + this->SetSyntax("{ON | OFF}"); + } + void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m, bool saset = false) + { + NickAlias *na = NickAlias::Find(user); + if (!na) + { + source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); + return; + } + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, na->nc, param)); + if (MOD_RESULT == EVENT_STOP) + return; + + if (param.equals_ci("ON")) + { + Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to enable chanstats for " << na->nc->display; + na->nc->Extend<bool>("NS_STATS"); + if (saset) + source.Reply(_("Chanstats statistics are now enabled for %s"), na->nc->display.c_str()); + else + source.Reply(_("Chanstats statistics are now enabled for your nick.")); + } + else if (param.equals_ci("OFF")) + { + Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to disable chanstats for " << na->nc->display; + na->nc->Shrink<bool>("NS_STATS"); + if (saset) + source.Reply(_("Chanstats statistics are now disabled for %s"), na->nc->display.c_str()); + else + source.Reply(_("Chanstats statistics are now disabled for your nick.")); + } + else + this->OnSyntaxError(source, "CHANSTATS"); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) anope_override + { + this->Run(source, source.nc->display, params[0]); + } + + bool OnHelp(CommandSource &source, const Anope::string &) anope_override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Turns Chanstats statistics ON or OFF.")); + return true; + } +}; + +class CommandNSSASetChanstats : public CommandNSSetChanstats +{ + public: + CommandNSSASetChanstats(Module *creator) : CommandNSSetChanstats(creator, "nickserv/saset/chanstats", 2) + { + this->ClearSyntax(); + this->SetSyntax(_("\037nickname\037 {ON | OFF}")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) anope_override + { + this->Run(source, params[0], params[1], true); + } + + bool OnHelp(CommandSource &source, const Anope::string &) anope_override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Turns chanstats channel statistics ON or OFF for this user.")); + return true; + } +}; + +class MySQLInterface : public SQL::Interface +{ + public: + MySQLInterface(Module *o) : SQL::Interface(o) { } + + void OnResult(const SQL::Result &r) anope_override + { + } + + void OnError(const SQL::Result &r) anope_override + { + if (!r.GetQuery().query.empty()) + Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError(); + else + Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError(); + } +}; + +class MChanstats : public Module +{ + SerializableExtensibleItem<bool> cs_stats, ns_stats; + + CommandCSSetChanstats commandcssetchanstats; + + CommandNSSetChanstats commandnssetchanstats; + CommandNSSASetChanstats commandnssasetchanstats; + + ServiceReference<SQL::Provider> sql; + MySQLInterface sqlinterface; + SQL::Query query; + Anope::string SmileysHappy, SmileysSad, SmileysOther, prefix; + std::vector<Anope::string> TableList, ProcedureList, EventList; + + void RunQuery(const SQL::Query &q) + { + if (sql) + sql->Run(&sqlinterface, q); + } + + size_t CountWords(const Anope::string &msg) + { + size_t words = 0; + for (size_t pos = 0; pos != Anope::string::npos; pos = msg.find(" ", pos+1)) + words++; + return words; + } + size_t CountSmileys(const Anope::string &msg, const Anope::string &smileylist) + { + size_t smileys = 0; + spacesepstream sep(smileylist); + Anope::string buf; + + while (sep.GetToken(buf) && !buf.empty()) + { + for (size_t pos = msg.find(buf, 0); pos != Anope::string::npos; pos = msg.find(buf, pos+1)) + smileys++; + } + return smileys; + } + + const Anope::string GetDisplay(User *u) + { + if (u && u->Account() && ns_stats.HasExt(u->Account())) + return u->Account()->display; + else + return ""; + } + + void GetTables() + { + TableList.clear(); + ProcedureList.clear(); + EventList.clear(); + if (!sql) + return; + + SQL::Result r = this->sql->RunQuery(this->sql->GetTables(prefix)); + for (int i = 0; i < r.Rows(); ++i) + { + const std::map<Anope::string, Anope::string> &map = r.Row(i); + for (std::map<Anope::string, Anope::string>::const_iterator it = map.begin(); it != map.end(); ++it) + TableList.push_back(it->second); + } + query = "SHOW PROCEDURE STATUS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + ProcedureList.push_back(r.Get(i, "Name")); + } + query = "SHOW EVENTS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + EventList.push_back(r.Get(i, "Name")); + } + } + + bool HasTable(const Anope::string &table) + { + for (std::vector<Anope::string>::const_iterator it = TableList.begin(); it != TableList.end(); ++it) + if (*it == table) + return true; + return false; + } + + bool HasProcedure(const Anope::string &table) + { + for (std::vector<Anope::string>::const_iterator it = ProcedureList.begin(); it != ProcedureList.end(); ++it) + if (*it == table) + return true; + return false; + } + + bool HasEvent(const Anope::string &table) + { + for (std::vector<Anope::string>::const_iterator it = EventList.begin(); it != EventList.end(); ++it) + if (*it == table) + return true; + return false; + } + + + void CheckTables() + { + this->GetTables(); + if (!this->HasTable(prefix +"chanstats")) + { + query = "CREATE TABLE `" + prefix + "chanstats` (" + "`id` int(11) NOT NULL AUTO_INCREMENT," + "`chan` varchar(64) NOT NULL DEFAULT ''," + "`nick` varchar(64) NOT NULL DEFAULT ''," + "`type` ENUM('total', 'monthly', 'weekly', 'daily') NOT NULL," + "`letters` int(10) unsigned NOT NULL DEFAULT '0'," + "`words` int(10) unsigned NOT NULL DEFAULT '0'," + "`line` int(10) unsigned NOT NULL DEFAULT '0'," + "`actions` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_happy` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_sad` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_other` int(10) unsigned NOT NULL DEFAULT '0'," + "`kicks` int(10) unsigned NOT NULL DEFAULT '0'," + "`kicked` int(10) unsigned NOT NULL DEFAULT '0'," + "`modes` int(10) unsigned NOT NULL DEFAULT '0'," + "`topics` int(10) unsigned NOT NULL DEFAULT '0'," + "`time0` int(10) unsigned NOT NULL default '0'," + "`time1` int(10) unsigned NOT NULL default '0'," + "`time2` int(10) unsigned NOT NULL default '0'," + "`time3` int(10) unsigned NOT NULL default '0'," + "`time4` int(10) unsigned NOT NULL default '0'," + "`time5` int(10) unsigned NOT NULL default '0'," + "`time6` int(10) unsigned NOT NULL default '0'," + "`time7` int(10) unsigned NOT NULL default '0'," + "`time8` int(10) unsigned NOT NULL default '0'," + "`time9` int(10) unsigned NOT NULL default '0'," + "`time10` int(10) unsigned NOT NULL default '0'," + "`time11` int(10) unsigned NOT NULL default '0'," + "`time12` int(10) unsigned NOT NULL default '0'," + "`time13` int(10) unsigned NOT NULL default '0'," + "`time14` int(10) unsigned NOT NULL default '0'," + "`time15` int(10) unsigned NOT NULL default '0'," + "`time16` int(10) unsigned NOT NULL default '0'," + "`time17` int(10) unsigned NOT NULL default '0'," + "`time18` int(10) unsigned NOT NULL default '0'," + "`time19` int(10) unsigned NOT NULL default '0'," + "`time20` int(10) unsigned NOT NULL default '0'," + "`time21` int(10) unsigned NOT NULL default '0'," + "`time22` int(10) unsigned NOT NULL default '0'," + "`time23` int(10) unsigned NOT NULL default '0'," + "PRIMARY KEY (`id`)," + "UNIQUE KEY `chan` (`chan`,`nick`,`type`)," + "KEY `nick` (`nick`)," + "KEY `chan_` (`chan`)," + "KEY `type` (`type`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; + this->RunQuery(query); + } + /* There is no CREATE OR REPLACE PROCEDURE in MySQL */ + if (this->HasProcedure(prefix + "chanstats_proc_update")) + { + query = "DROP PROCEDURE " + prefix + "chanstats_proc_update"; + this->RunQuery(query); + } + query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_update`" + "(chan_ VARCHAR(255), nick_ VARCHAR(255), line_ INT(10), letters_ INT(10)," + "words_ INT(10), actions_ INT(10), sm_h_ INT(10), sm_s_ INT(10), sm_o_ INT(10)," + "kicks_ INT(10), kicked_ INT(10), modes_ INT(10), topics_ INT(10))" + "BEGIN " + "DECLARE time_ VARCHAR(20);" + "SET time_ = CONCAT('time', hour(now()));" + "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " + "('', chan_, 'total'), ('', chan_, 'monthly')," + "('', chan_, 'weekly'), ('', chan_, 'daily');" + "IF nick_ != '' THEN " + "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " + "(nick_, chan_, 'total'), (nick_, chan_, 'monthly')," + "(nick_, chan_, 'weekly'),(nick_, chan_, 'daily')," + "(nick_, '', 'total'), (nick_, '', 'monthly')," + "(nick_, '', 'weekly'), (nick_, '', 'daily');" + "END IF;" + "SET @update_query = CONCAT('UPDATE `" + prefix + "chanstats` SET line=line+', line_, '," + "letters=letters+', letters_, ' , words=words+', words_, ', actions=actions+', actions_, ', " + "smileys_happy=smileys_happy+', sm_h_, ', smileys_sad=smileys_sad+', sm_s_, ', " + "smileys_other=smileys_other+', sm_o_, ', kicks=kicks+', kicks_, ', kicked=kicked+', kicked_, ', " + "modes=modes+', modes_, ', topics=topics+', topics_, ', ', time_ , '=', time_, '+', line_ ,' " + "WHERE (nick='''' OR nick=''', nick_, ''') AND (chan='''' OR chan=''', chan_, ''')');" + "PREPARE update_query FROM @update_query;" + "EXECUTE update_query;" + "DEALLOCATE PREPARE update_query;" + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "chanstats_proc_chgdisplay")) + { + query = "DROP PROCEDURE " + prefix + "chanstats_proc_chgdisplay;"; + this->RunQuery(query); + } + query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_chgdisplay`" + "(old_nick varchar(255), new_nick varchar(255))" + "BEGIN " + "DECLARE res_count int(10) unsigned;" + "SELECT COUNT(nick) INTO res_count FROM `" + prefix + "chanstats` WHERE nick = new_nick;" + "IF res_count = 0 THEN " + "UPDATE `" + prefix + "chanstats` SET `nick` = new_nick WHERE `nick` = old_nick;" + "ELSE " + "my_cursor: BEGIN " + "DECLARE no_more_rows BOOLEAN DEFAULT FALSE;" + "DECLARE chan_ VARCHAR(255);" + "DECLARE type_ ENUM('total', 'monthly', 'weekly', 'daily');" + "DECLARE letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, time9_," + "time10_, time11_, time12_, time13_, time14_, time15_, time16_, time17_, time18_," + "time19_, time20_, time21_, time22_, time23_ INT(10) unsigned;" + "DECLARE stats_cursor CURSOR FOR " + "SELECT chan, type, letters, words, line, actions, smileys_happy," + "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1," + "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," + "time12, time13, time14, time15, time16, time17, time18, time19, time20," + "time21, time22, time23 " + "FROM `" + prefix + "chanstats` " + "WHERE `nick` = old_nick;" + "DECLARE CONTINUE HANDLER FOR NOT FOUND " + "SET no_more_rows = TRUE;" + "OPEN stats_cursor;" + "the_loop: LOOP " + "FETCH stats_cursor " + "INTO chan_, type_, letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_," + "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_," + "time17_, time18_, time19_, time20_, time21_, time22_, time23_;" + "IF no_more_rows THEN " + "CLOSE stats_cursor;" + "LEAVE the_loop;" + "END IF;" + "INSERT INTO `" + prefix + "chanstats` " + "(chan, nick, type, letters, words, line, actions, smileys_happy, " + "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1, " + "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," + "time12, time13, time14, time15, time16, time17, time18, time19, time20," + "time21, time22, time23)" + "VALUES (chan_, new_nick, type_, letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, " + "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_, " + "time17_, time18_, time19_, time20_, time21_, time22_, time23_)" + "ON DUPLICATE KEY UPDATE letters=letters+VALUES(letters), words=words+VALUES(words)," + "line=line+VALUES(line), actions=actions+VALUES(actions)," + "smileys_happy=smileys_happy+VALUES(smileys_happy)," + "smileys_sad=smileys_sad+VALUES(smileys_sad)," + "smileys_other=smileys_other+VALUES(smileys_other)," + "kicks=kicks+VALUES(kicks), kicked=kicked+VALUES(kicked)," + "modes=modes+VALUES(modes), topics=topics+VALUES(topics)," + "time1=time1+VALUES(time1), time2=time2+VALUES(time2), time3=time3+VALUES(time3)," + "time4=time4+VALUES(time4), time5=time5+VALUES(time5), time6=time6+VALUES(time6)," + "time7=time7+VALUES(time7), time8=time8+VALUES(time8), time9=time9+VALUES(time9)," + "time10=time10+VALUES(time10), time11=time11+VALUES(time11), time12=time12+VALUES(time12)," + "time13=time13+VALUES(time13), time14=time14+VALUES(time14), time15=time15+VALUES(time15)," + "time16=time16+VALUES(time16), time17=time17+VALUES(time17), time18=time18+VALUES(time18)," + "time19=time19+VALUES(time19), time20=time20+VALUES(time20), time21=time21+VALUES(time21)," + "time22=time22+VALUES(time22), time23=time23+VALUES(time23);" + "END LOOP;" + "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = old_nick;" + "END my_cursor;" + "END IF;" + "END;"; + this->RunQuery(query); + + /* dont prepend any database prefix to events so we can always delete/change old events */ + if (this->HasEvent("chanstats_event_cleanup_daily")) + { + query = "DROP EVENT chanstats_event_cleanup_daily"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_daily` " + "ON SCHEDULE EVERY 1 DAY STARTS CURRENT_DATE " + "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='daily';"; + this->RunQuery(query); + + if (this->HasEvent("chanstats_event_cleanup_weekly")) + { + query = "DROP EVENT `chanstats_event_cleanup_weekly`"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_weekly` " + "ON SCHEDULE EVERY 1 WEEK STARTS ADDDATE(CURDATE(), INTERVAL 1-DAYOFWEEK(CURDATE()) DAY) " + "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='weekly';"; + this->RunQuery(query); + + if (this->HasEvent("chanstats_event_cleanup_monthly")) + { + query = "DROP EVENT `chanstats_event_cleanup_monthly`;"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_monthly` " + "ON SCHEDULE EVERY 1 MONTH STARTS LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY " + "DO BEGIN " + "UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0, " + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='monthly';" + "OPTIMIZE TABLE `" + prefix + "chanstats`;" + "END;"; + this->RunQuery(query); + } + + + public: + MChanstats(const Anope::string &modname, const Anope::string &creator) : + Module(modname, creator, EXTRA | VENDOR), + cs_stats(this, "CS_STATS"), ns_stats(this, "NS_STATS"), + commandcssetchanstats(this), commandnssetchanstats(this), commandnssasetchanstats(this), + sqlinterface(this) + { + } + + void OnReload(Configuration::Conf *conf) anope_override + { + Configuration::Block *block = conf->GetModule(this); + prefix = block->Get<const Anope::string>("prefix", "anope_"); + SmileysHappy = block->Get<const Anope::string>("SmileysHappy"); + SmileysSad = block->Get<const Anope::string>("SmileysSad"); + SmileysOther = block->Get<const Anope::string>("SmileysOther"); + + Anope::string engine = block->Get<const Anope::string>("engine"); + this->sql = ServiceReference<SQL::Provider>("SQL::Provider", engine); + if (sql) + this->CheckTables(); + else + Log(this) << "no database connection to " << engine; + } + + void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_all) anope_override + { + if (!show_all) + return; + if (cs_stats.HasExt(ci)) + info.AddOption(_("Chanstats")); + } + + void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) anope_override + { + if (!show_hidden) + return; + if (ns_stats.HasExt(na->nc)) + info.AddOption(_("Chanstats")); + } + + void OnTopicUpdated(Channel *c, const Anope::string &user, const Anope::string &topic) anope_override + { + User *u = User::Find(user); + if (!u || !u->Account() || !c->ci || !cs_stats.HasExt(c->ci)) + return; + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(u)); + this->RunQuery(query); + } + + EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) anope_override + { + this->OnModeChange(c, setter.GetUser()); + return EVENT_CONTINUE; + } + + EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *, const Anope::string ¶m) anope_override + { + this->OnModeChange(c, setter.GetUser()); + return EVENT_CONTINUE; + } + + private: + void OnModeChange(Channel *c, User *u) + { + if (!u || !u->Account() || !c->ci || !cs_stats.HasExt(c->ci)) + return; + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(u)); + this->RunQuery(query); + } + + public: + void OnPreUserKicked(const MessageSource &source, ChanUserContainer *cu, const Anope::string &kickmsg) anope_override + { + if (!cu->chan->ci || !cs_stats.HasExt(cu->chan->ci)) + return; + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0);"; + query.SetValue("channel", cu->chan->name); + query.SetValue("nick", GetDisplay(cu->user)); + this->RunQuery(query); + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0);"; + query.SetValue("channel", cu->chan->name); + query.SetValue("nick", GetDisplay(source.GetUser())); + this->RunQuery(query); + } + + void OnPrivmsg(User *u, Channel *c, Anope::string &msg) anope_override + { + if (!c->ci || !cs_stats.HasExt(c->ci)) + return; + + size_t letters = msg.length(); + size_t words = this->CountWords(msg); + + size_t action = 0; + if (msg.find("\01ACTION")!=Anope::string::npos) + { + action = 1; + letters = letters - 7; + words--; + } + + // count smileys + size_t smileys_happy = CountSmileys(msg, SmileysHappy); + size_t smileys_sad = CountSmileys(msg, SmileysSad); + size_t smileys_other = CountSmileys(msg, SmileysOther); + + // do not count smileys as words + words = words - smileys_happy - smileys_sad - smileys_other; + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 1, @letters@, @words@, @action@, " + "@smileys_happy@, @smileys_sad@, @smileys_other@, '0', '0', '0', '0');"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(u)); + query.SetValue("letters", letters); + query.SetValue("words", words); + query.SetValue("action", action); + query.SetValue("smileys_happy", smileys_happy); + query.SetValue("smileys_sad", smileys_sad); + query.SetValue("smileys_other", smileys_other); + this->RunQuery(query); + } + + void OnDelCore(NickCore *nc) anope_override + { + query = "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = @nick@;"; + query.SetValue("nick", nc->display); + this->RunQuery(query); + } + + void OnChangeCoreDisplay(NickCore *nc, const Anope::string &newdisplay) anope_override + { + query = "CALL " + prefix + "chanstats_proc_chgdisplay(@old_display@, @new_display@);"; + query.SetValue("old_display", nc->display); + query.SetValue("new_display", newdisplay); + this->RunQuery(query); + } + + void OnDelChan(ChannelInfo *ci) anope_override + { + query = "DELETE FROM `" + prefix + "chanstats` WHERE `chan` = @channel@;"; + query.SetValue("channel", ci->name); + this->RunQuery(query); + } +}; + +MODULE_INIT(MChanstats) + |
