summaryrefslogtreecommitdiff
path: root/modules/extra
diff options
context:
space:
mode:
Diffstat (limited to 'modules/extra')
-rw-r--r--modules/extra/stats/cs_fantasy_stats.cpp179
-rw-r--r--modules/extra/stats/cs_fantasy_top.cpp191
-rw-r--r--modules/extra/stats/irc2sql/CMakeLists.txt2
-rw-r--r--modules/extra/stats/irc2sql/irc2sql.cpp285
-rw-r--r--modules/extra/stats/irc2sql/irc2sql.h77
-rw-r--r--modules/extra/stats/irc2sql/tables.cpp382
-rw-r--r--modules/extra/stats/irc2sql/utils.cpp61
-rw-r--r--modules/extra/stats/m_chanstats.cpp630
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> &params);
+};
+
+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> &params);
+};
+
+
+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> &params)
+ {
+ 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> &params)
+{
+ me->DoStats(source, false, params);
+}
+
+void CommandCSGStats::Execute(CommandSource &source, const std::vector<Anope::string> &params)
+{
+ 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> &params);
+};
+
+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> &params);
+};
+
+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> &params);
+};
+
+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> &params);
+};
+
+
+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> &params, 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> &params)
+{
+ me->DoTop(source, params, false, 3);
+}
+
+void CommandCSTop10::Execute(CommandSource &source, const std::vector<Anope::string> &params)
+{
+ me->DoTop(source, params, false, 10);
+}
+
+void CommandCSGTop::Execute(CommandSource &source, const std::vector<Anope::string> &params)
+{
+ me->DoTop(source, params, true, 3);
+}
+
+void CommandCSGTop10::Execute(CommandSource &source, const std::vector<Anope::string> &params)
+{
+ 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 &param) 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 &param) 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 &param) anope_override;
+ EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string &param) 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
+ *
+