summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDukePyrolator <DukePyrolator@anope.org>2013-10-26 18:31:50 +0200
committerDukePyrolator <DukePyrolator@anope.org>2013-10-26 18:42:58 +0200
commitb5af310f8aacd168ba4b9bee97374558e11c57fd (patch)
tree10be038b2b6fd717ae59b8cdb5106d76b896ef72
parent2cfc97053faba7582cd57701900de78ebacb12ce (diff)
added the irc2sql gateway module
-rw-r--r--data/example.conf13
-rw-r--r--data/irc2sql.example.conf106
-rw-r--r--data/stats.standalone.example.conf522
-rw-r--r--modules/stats/irc2sql/CMakeLists.txt2
-rw-r--r--modules/stats/irc2sql/irc2sql.cpp239
-rw-r--r--modules/stats/irc2sql/irc2sql.h74
-rw-r--r--modules/stats/irc2sql/tables.cpp311
-rw-r--r--modules/stats/irc2sql/utils.cpp61
-rw-r--r--src/tools/CMakeLists.txt3
-rw-r--r--src/tools/geoipupdate.sh78
10 files changed, 1409 insertions, 0 deletions
diff --git a/data/example.conf b/data/example.conf
index 68a3a6583..6d73eb021 100644
--- a/data/example.conf
+++ b/data/example.conf
@@ -1275,3 +1275,16 @@ include
type = "file"
name = "chanstats.example.conf"
}
+
+/*
+ * IRC2SQL Gateway
+ * This module collects data about users, channels and servers. It doesn't build stats
+ * itself, however, it gives you the database, it's up to you how you use it.
+ *
+ * Requires a MySQL Database and MySQL version 5.5 or higher
+ */
+#include
+{
+ type = "file"
+ name = "irc2sql.example.conf"
+}
diff --git a/data/irc2sql.example.conf b/data/irc2sql.example.conf
new file mode 100644
index 000000000..8872b8817
--- /dev/null
+++ b/data/irc2sql.example.conf
@@ -0,0 +1,106 @@
+/*
+ * Example configuration file for the irc2sql gateway
+ *
+ */
+
+module
+{
+ name = "irc2sql"
+
+ /*
+ * The name of this engine.
+ * This must match with the name in the mysql{ } block
+ */
+ engine ="mysql/main"
+
+ /*
+ * An optional prefix to prepended to the name of each created table.
+ * Do not use the same prefix for other programs.
+ */
+
+ prefix = "anope_"
+
+ /*
+ * GeoIP - Automagically add a users geoip to the user table.
+ * Tables are created by irc2sql, you have to run the
+ * geoipupdate script after you started anope to download
+ * and import the actual geoip database
+ *
+ */
+
+ /*
+ * Enable GeoIP Lookup
+ */
+ GeoIPLookup = yes
+
+ /*
+ * Chose between the smaller 'country' or the bigger 'city' database.
+ *
+ */
+ GeoIPDatabase = "country"
+
+ /*
+ * The name of the client that should send the CTCP VERSION requests.
+ * It must already exist or must be defined in the following service block.
+ */
+ client = "StatServ"
+
+ /*
+ * Get the CTCP version from users
+ * The users connecting to the network will receive a CTCP VERSION
+ * request from the above configured services client
+ */
+ ctcpuser = "yes"
+
+ /*
+ * Send out CTCP VERSION requests to users during burst.
+ * Disable this if you restart Anope often and don't want to
+ * annoy your users.
+ */
+ ctcpeob = "yes"
+}
+
+service
+{
+ /*
+ * The name of the StatServ client.
+ */
+ nick = "StatServ"
+
+ /*
+ * The username of the StatServ client.
+ */
+ user = "StatServ"
+
+ /*
+ * The hostname of the StatServ client.
+ */
+ host = "services.host"
+
+ /*
+ * The realname of the StatServ client.
+ */
+ gecos = "Statistical Service"
+
+ /*
+ * The modes this client should use.
+ * Do not modify this unless you know what you are doing.
+ *
+ * These modes are very IRCd specific. If left commented, sane defaults
+ * are used based on what protocol module you have loaded.
+ *
+ * Note that setting this option incorrectly could potentially BREAK some, if
+ * not all, usefulness of the client. We will not support you if this client is
+ * unable to do certain things if this option is enabled.
+ */
+ #modes = "+o"
+
+ /*
+ * An optional comma separated list of channels this service should join. Outside
+ * of log channels this is not very useful, as the service will just idle in the
+ * specified channels, and will not accept any types of commands.
+ *
+ * Prefixes may be given to the channels in the form of mode characters or prefix symbols.
+ */
+ #channels = "@#services,#mychan"
+}
diff --git a/data/stats.standalone.example.conf b/data/stats.standalone.example.conf
new file mode 100644
index 000000000..45526f389
--- /dev/null
+++ b/data/stats.standalone.example.conf
@@ -0,0 +1,522 @@
+/*
+ * Example configuration file for Services. After making the appropriate
+ * changes to this file, place it in the Services conf directory (as
+ * specified in the "configure" script, default /home/username/services/conf)
+ * under the name "services.conf".
+ *
+ * The format of this file is fairly simple: three types of comments are supported:
+ * - All text after a '#' on a line is ignored, as in shell scripting
+ * - All text after '//' on a line is ignored, as in C++
+ * - A block of text like this one is ignored, as in C
+ *
+ * Outside of comments, there are three structures: blocks, keys, and values.
+ *
+ * A block is a named container, which contains a number of key to value pairs
+ * - you may think of this as an array.
+ *
+ * A block is created like so:
+ * foobar
+ * {
+ * moo = "cow"
+ * foo = bar
+ * }
+ *
+ * Note that nameless blocks are allowed and are often used with comments to allow
+ * easily commenting an entire block, for example:
+ * #foobar
+ * {
+ * moo = "cow"
+ * foo = bar
+ * }
+ * is an entirely commented block.
+ *
+ * Keys are case insensitive. Values depend on what key - generally, information is
+ * given in the key comment. The quoting of values (and most other syntax) is quite
+ * flexible, however, please do not forget to quote your strings:
+ *
+ * "This is a parameter string with spaces in it"
+ *
+ * If you need to include a double quote inside a quoted string, precede it
+ * by a backslash:
+ *
+ * "This string has \"double quotes\" in it"
+ *
+ * Time parameters can be specified either as an integer representing a
+ * number of seconds (e.g. "3600" = 1 hour), or as an integer with a unit
+ * specifier: "s" = seconds, "m" = minutes, "h" = hours, "d" = days.
+ * Combinations (such as "1h30m") are not permitted. Examples (all of which
+ * represent the same length of time, one day):
+ *
+ * "86400", "86400s", "1440m", "24h", "1d"
+ *
+ * In the documentation for each directive, one of the following will be
+ * included to indicate whether an option is required:
+ *
+ * [REQUIRED]
+ * Indicates a directive which must be given. Without it, Services will
+ * not start.
+ *
+ * [RECOMMENDED]
+ * Indicates a directive which may be omitted, but omitting it may cause
+ * undesirable side effects.
+ *
+ * [OPTIONAL]
+ * Indicates a directive which is optional. If not given, the feature
+ * will typically be disabled. If this is not the case, more
+ * information will be given in the documentation.
+ *
+ * [DISCOURAGED]
+ * Indicates a directive which may cause undesirable side effects if
+ * specified.
+ *
+ * [DEPRECATED]
+ * Indicates a directive which will disappear in a future version of
+ * Services, usually because its functionality has been either
+ * superseded by that of other directives or incorporated into the main
+ * program.
+ */
+
+/*
+ * [OPTIONAL] Defines
+ *
+ * You can define values to other values, which can be used to easily change
+ * every value in the configuration. For example, use:
+ *
+ * define
+ * {
+ * name = "ChanServ"
+ * value = "ChannelServ"
+ * }
+ *
+ * To replace every occurrence of ChanServ with ChannelServ in the configuration file,
+ * and in every included configuration file (such as chanserv.example.conf).
+ */
+
+/*
+ * The services.host define is used in multiple different locations throughout the
+ * configuration for services clients hostnames.
+ */
+define
+{
+ name = "services.host"
+ value = "services.localhost.net"
+}
+
+/*
+ * [OPTIONAL] Additional Includes
+ *
+ * You can include additional configuration files here.
+ * You may also include executable files, which will be executed and
+ * the output from it will be included into your configuration.
+ */
+
+#include
+{
+ type = "file"
+ name = "some.conf"
+}
+
+#include
+{
+ type = "executable"
+ name = "/usr/bin/wget -q -O - http://some.misconfigured.network.com/services.conf"
+}
+
+/*
+ * [REQUIRED] IRCd Config
+ *
+ * This section is used to set up Anope to connect to your IRC network.
+ * This section can be included multiple times, and Anope will attempt to
+ * connect to each server until it finally connects.
+ *
+ * Each uplink IRCd should have a corresponding configuration to allow Services
+ * to link to it.
+ *
+ * An example configuration for InspIRCd that is compatible with the below uplink
+ * and serverinfo configuration would look like:
+ *
+ * <link name="services.localhost.net"
+ * ipaddr="127.0.0.1"
+ * port="7000"
+ * sendpass="mypassword"
+ * recvpass="mypassword">
+ * <uline server="services.localhost.net" silent="yes">
+ * <bind address="127.0.0.1" port="7000" type="servers">
+ *
+ * An example configuration for UnrealIRCd that is compatible with the below uplink
+ * and serverinfo configuration would look like:
+ *
+ * link services.localhost.net
+ * {
+ * username *;
+ * hostname *;
+ * bind-ip "127.0.0.1";
+ * port 7000;
+ * hub *;
+ * password-connect "mypassword";
+ * password-receive "mypassword";
+ * class servers;
+ * };
+ * ulines { services.localhost.net; };
+ * listen 127.0.0.1:7000;
+ */
+uplink
+{
+ /*
+ * The IP or hostname of the IRC server you wish to connect Services to.
+ * Usually, you will want to connect Services over 127.0.0.1 (aka localhost).
+ *
+ * NOTE: On some shell providers, this will not be an option.
+ */
+ host = "127.0.0.1"
+
+ /*
+ * Enable if Services should connect using IPv6.
+ */
+ ipv6 = no
+
+ /*
+ * Enable if Services should connect using SSL.
+ * You must have m_ssl loaded for this to work.
+ */
+ ssl = no
+
+ /*
+ * The port to connect to.
+ * The IRCd *MUST* be configured to listen on this port, and to accept
+ * server connections.
+ *
+ * Refer to your IRCd documentation for how this is to be done.
+ */
+ port = 7000
+
+ /*
+ * The password to send to the IRC server for authentication.
+ * This must match the link block on your IRCd.
+ *
+ * Refer to your IRCd documentation for more information on link blocks.
+ */
+ password = "mypassword"
+}
+
+/*
+ * [REQUIRED] Server Information
+ *
+ * This section contains information about the Services server.
+ */
+serverinfo
+{
+ /*
+ * The hostname that Services will be seen as, it must have no conflicts with any
+ * other server names on the rest of your IRC network. Note that it does not have
+ * to be an existing hostname, just one that isn't on your network already.
+ */
+ name = "stats.localhost.net"
+
+ /*
+ * The text which should appear as the server's information in /whois and similar
+ * queries.
+ */
+ description = "Stats for IRC Networks"
+
+ /*
+ * The local address that Services will bind to before connecting to the remote
+ * server. This may be useful for multihomed hosts. If omitted, Services will let
+ * the Operating System choose the local address. This directive is optional.
+ *
+ * If you don't know what this means or don't need to use it, just leave this
+ * directive commented out.
+ */
+ #localhost = "nowhere."
+
+ /*
+ * What Server ID to use for this connection?
+ * Note: This should *ONLY* be used for TS6/P10 IRCds. Refer to your IRCd documentation
+ * to see if this is needed.
+ */
+ #id = "00A"
+
+ /*
+ * The filename containing the Services process ID. The path is relative to the
+ * services root directory.
+ */
+ pid = "data/services.pid"
+
+ /*
+ * The filename containing the Message of the Day. The path is relative to the
+ * services root directory.
+ */
+ motd = "conf/services.motd"
+}
+
+/*
+ * [REQUIRED] Protocol module
+ *
+ * This directive tells Anope which IRCd Protocol to speak when connecting.
+ * You MUST modify this to match the IRCd you run.
+ *
+ * Supported:
+ * - bahamut
+ * - charybdis
+ * - hybrid
+ * - inspircd12
+ * - inspircd20
+ * - ngircd
+ * - plexus
+ * - ratbox
+ * - unreal
+ */
+module
+{
+ name = "inspircd20"
+}
+
+/*
+ * [REQUIRED] Network Information
+ *
+ * This section contains information about the IRC network that Services will be
+ * connecting to.
+ */
+networkinfo
+{
+ /*
+ * This is the name of the network that Services will be running on.
+ */
+ networkname = "LocalNet"
+
+ /*
+ * Set this to the maximum allowed nick length on your network.
+ * Be sure to set this correctly, as setting this wrong can result in
+ * Services being disconnected from the network.
+ */
+ nicklen = 31
+
+ /* Set this to the maximum allowed ident length on your network.
+ * Be sure to set this correctly, as setting this wrong can result in
+ * Services being disconnected from the network.
+ */
+ userlen = 10
+
+ /* Set this to the maximum allowed hostname length on your network.
+ * Be sure to set this correctly, as setting this wrong can result in
+ * Services being disconnected from the network.
+ */
+ hostlen = 64
+
+ /* Set this to the maximum allowed channel length on your network.
+ */
+ chanlen = 32
+
+ /* The maximum number of list modes settable on a channel (such as b, e, I).
+ * Comment out or set to 0 to disable.
+ */
+ modelistsize = 100
+
+ /*
+ * The characters allowed in hostnames. This is used for validating hostnames given
+ * to services, such as BotServ bot hostnames and user vhosts. Changing this is not
+ * recommended unless you know for sure your IRCd supports whatever characters you are
+ * wanting to use. Telling services to set a vHost containing characters your IRCd
+ * disallows could potentially break the IRCd and/or Services.
+ *
+ * It is recommended you DON'T change this.
+ */
+ vhost_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"
+
+ /*
+ * If set to true, allows vHosts to not contain dots (.).
+ * Newer IRCds generally do not have a problem with this, but the same warning as
+ * vhost_chars applies.
+ *
+ * It is recommended you DON'T change this.
+ */
+ allow_undotted_vhosts = false
+
+ /*
+ * The characters that are not allowed to be at the very beginning or very ending
+ * of a vHost. The same warning as vhost_chars applies.
+ *
+ * It is recommended you DON'T change this.
+ */
+ disallow_start_or_end = ".-"
+}
+
+/*
+ * [REQUIRED] Services Options
+ *
+ * This section contains various options which determine how Services will operate.
+ */
+options
+{
+ /*
+ * On Linux/UNIX systems Anope can setuid and setgid to this user and group
+ * after starting up. This is useful if Anope has to bind to privileged ports
+ */
+ #user = "anope"
+ #group = "anope"
+
+ /*
+ * The case mapping used by services. This must be set to a valid locale name
+ * installed on your machine. Services use this case map to compare, with
+ * case insensitivity, things such as nick names, channel names, etc.
+ *
+ * We provide two special casemaps shipped with Anope, ascii and rfc1459.
+ *
+ * This value should be set to what your IRCd uses, which is probably rfc1459,
+ * however Anope has always used ascii for comparison, so the default is ascii.
+ *
+ * Changing this value once set is not recommended.
+ */
+ casemap = "ascii"
+
+ /*
+ * Sets the timeout period for reading from the uplink.
+ */
+ readtimeout = 5s
+
+ /*
+ * Sets the interval between sending warning messages for program errors via
+ * WALLOPS/GLOBOPS.
+ */
+ warningtimeout = 4h
+
+ /*
+ * If set, Services will only show /stats o to IRC Operators. This directive
+ * is optional.
+ */
+ #hidestatso = yes
+
+ /*
+ * A space-separated list of ulined servers on your network, it is assumed that
+ * the servers in this list are allowed to set channel modes and Services will
+ * not attempt to reverse their mode changes.
+ *
+ * WARNING: Do NOT put your normal IRC user servers in this directive.
+ *
+ * This directive is optional.
+ */
+ #ulineservers = "services.your.network"
+
+ /*
+ * How long to wait between connection retries with the uplink(s).
+ */
+ retrywait = 60s
+}
+
+/*
+ * [RECOMMENDED] Logging Configuration
+ *
+ * This section is used for configuring what is logged and where it is logged to.
+ * You may have multiple log blocks if you wish. Remember to properly secure any
+ * channels you choose to have Anope log to!
+ */
+log
+{
+ /*
+ * Target(s) to log to, which may be one of the following:
+ * - a channel name
+ * - a filename
+ * - globops
+ */
+ target = "stats.log"
+
+ /* Log to both services.log and the channel #services
+ *
+ * Note that some older IRCds, such as Ratbox, require services to be in the
+ * log channel to be able to message it. To do this, configure service:channels to
+ * join your logging channel.
+ */
+ #target = "stats.log #services"
+
+ /*
+ * The source(s) to only accept log messages from. Leave commented to allow all sources.
+ * This can be a users name, a channel name, one of our clients (eg, OperServ), or a server name.
+ */
+ #source = ""
+
+ /*
+ * The bot used to log generic messages which have no predefined sender if there
+ * is a channel in the target directive.
+ */
+ bot = "Global"
+
+ /*
+ * The number of days to keep logfiles, only useful if you are logging to a file.
+ * Set to 0 to never delete old logfiles.
+ *
+ * Note that Anope must run 24 hours a day for this feature to work correctly.
+ */
+ logage = 7
+
+ /*
+ * What types of log messages should be logged by this block. There are nine general categories:
+ *
+ * servers - Server actions, linking, squitting, etc.
+ * channels - Actions in channels such as joins, parts, kicks, etc.
+ * users - User actions such as connecting, disconnecting, changing name, etc.
+ * other - All other messages without a category.
+ * rawio - Logs raw input and output from services
+ * debug - Debug messages (log files can become VERY large from this).
+ *
+ * These options determine what messages from the categories should be logged. Wildcards are accepted, and
+ * you can also negate values with a ~. For example, "~operserv/akill operserv/*" would log all operserv
+ * messages except for operserv/akill. Note that processing stops at the first matching option, which
+ * means "* ~operserv/*" would log everything because * matches everything.
+ *
+ * Valid server options are:
+ * connect, quit, sync, squit
+ *
+ * Valid channel options are:
+ * create, destroy, join, part, kick, leave, mode
+ *
+ * Valid user options are:
+ * connect, disconnect, quit, nick, ident, host, mode, maxusers, oper
+ *
+ * Rawio and debug are simple yes/no answers, there are no types for them.
+ *
+ * Note that modules may add their own values to these options.
+ */
+ servers = "*"
+ #channels = "~mode *"
+ users = "connect disconnect nick"
+ other = "*"
+ rawio = no
+ debug = no
+}
+
+/*
+ * [REQUIRED] MySQL Database configuration.
+ *
+ * m_mysql
+ *
+ * This module allows other modules to use MySQL.
+ */
+module
+{
+ name = "m_mysql"
+
+ mysql
+ {
+ /* The name of this service. */
+ name = "mysql/main"
+ database = "anope"
+ server = "127.0.0.1"
+ username = "anope"
+ password = "mypassword"
+ port = 3306
+ }
+}
+
+/*
+ * IRC2SQL Gateway
+ * This module collects data about users, channels and servers. It doesn't build stats
+ * itself, however, it gives you the database, it's up to you how you use it.
+ *
+ * Requires a MySQL Database and MySQL version 5.5 or higher
+ */
+include
+{
+ type = "file"
+ name = "irc2sql.example.conf"
+}
+
diff --git a/modules/stats/irc2sql/CMakeLists.txt b/modules/stats/irc2sql/CMakeLists.txt
new file mode 100644
index 000000000..e68a5b63e
--- /dev/null
+++ b/modules/stats/irc2sql/CMakeLists.txt
@@ -0,0 +1,2 @@
+#build_subdir(${CMAKE_CURRENT_SOURCE_DIR})
+
diff --git a/modules/stats/irc2sql/irc2sql.cpp b/modules/stats/irc2sql/irc2sql.cpp
new file mode 100644
index 000000000..e47abf34a
--- /dev/null
+++ b/modules/stats/irc2sql/irc2sql.cpp
@@ -0,0 +1,239 @@
+#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);
+}
+
+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 = "UPDATE `" + prefix + "server` "
+ "SET currentusers = 0, online = 'N', split_time = now()";
+ 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))
+ IRCD->SendPrivmsg(StatServ, u->GetUID(), "\1VERSION\1");
+
+}
+
+void IRC2SQL::OnUserQuit(User *u, const Anope::string &msg) anope_override
+{
+ if (quitting)
+ 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);
+}
+
+void IRC2SQL::OnLeaveChannel(User *u, Channel *c) anope_override
+{
+ if (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/stats/irc2sql/irc2sql.h b/modules/stats/irc2sql/irc2sql.h
new file mode 100644
index 000000000..d2fcd7aca
--- /dev/null
+++ b/modules/stats/irc2sql/irc2sql.h
@@ -0,0 +1,74 @@
+#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;
+ 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 | THIRD), sql("", ""), sqlinterface(this), versionreply(this, "CTCPVERSION")
+ {
+ 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;
+
+ 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/stats/irc2sql/tables.cpp b/modules/stats/irc2sql/tables.cpp
new file mode 100644
index 000000000..8b339f406
--- /dev/null
+++ b/modules/stats/irc2sql/tables.cpp
@@ -0,0 +1,311 @@
+#include "irc2sql.h"
+
+void IRC2SQL::CheckTables()
+{
+ Anope::string geoquery("");
+
+ /* TODO: remove the DropTable commands when the table layout is done
+ * perhaps we should drop/recreate some tables by default in case anope crashed
+ * and was unable to clear the content (ison)
+ * TRUNCATE could perform better for this?
+ */
+ SQL::Result r;
+ r = this->sql->RunQuery(SQL::Query("DROP TABLE " + prefix + "user"));
+ r = this->sql->RunQuery(SQL::Query("DROP TABLE " + prefix + "chan"));
+ r = this->sql->RunQuery(SQL::Query("DROP TABLE " + prefix + "ison"));
+
+ 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 + "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 "
+ "INNER JOIN `" + prefix + "chan` as c "
+ "WHERE u.nick = nick_ "
+ "AND i.nickid = u.nickid "
+ "AND i.chanid = c.chanid;"
+ /* 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 FROM `" + prefix + "ison` "
+ "USING `" + prefix + "ison`, `" + prefix + "user` , `"
+ + prefix + "chan` "
+ "WHERE " + prefix + "ison.nickid = " + prefix + "user.nickid "
+ "AND " + prefix + "user.nick = nick_ "
+ "AND " + prefix + "ison.chanid = " + prefix + "chan.chanid "
+ "AND " + prefix + "chan.channel = channel_; "
+ "UPDATE `" + prefix + "chan` SET currentusers=currentusers-1 "
+ "WHERE channel=channel_;"
+ "END";
+ this->RunQuery(query);
+}
diff --git a/modules/stats/irc2sql/utils.cpp b/modules/stats/irc2sql/utils.cpp
new file mode 100644
index 000000000..0c35b13fa
--- /dev/null
+++ b/modules/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/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt
index 8d66e577f..200aa2bf3 100644
--- a/src/tools/CMakeLists.txt
+++ b/src/tools/CMakeLists.txt
@@ -41,6 +41,9 @@ if(NOT WIN32)
install (PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/anoperc
DESTINATION ${BIN_DIR}
)
+ install (PROGRAMS geoipupdate.sh
+ DESTINATION ${BIN_DIR}
+ )
endif(NOT WIN32)
# On non-Windows platforms, if RUNGROUP is set, change the permissions of the tools directory
diff --git a/src/tools/geoipupdate.sh b/src/tools/geoipupdate.sh
new file mode 100644
index 000000000..827860a16
--- /dev/null
+++ b/src/tools/geoipupdate.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+
+# This script is a helper script for the irc2sql module.
+# It downloads the configured geoip databases and inserts
+# them into existing mysql tables. The tables are created
+# by the irc2sql module on the first load.
+
+############################
+# Config
+############################
+
+
+geoip_database="country" # available options: "country" and "city"
+mysql_host="localhost"
+mysql_user="anope"
+mysql_password="anope"
+mysql_database="anope"
+prefix="anope_"
+die="no"
+
+###########################
+
+# The GeoIP data is created by MaxMind, available from www.maxmind.com.
+geoip_country_source="http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip"
+geoip_city_source="http://geolite.maxmind.com/download/geoip/database/GeoLiteCity_CSV/GeoLiteCity-latest.zip"
+geoip_region_source="http://dev.maxmind.com/static/csv/codes/maxmind/region.csv"
+
+###########################
+LOGIN="--host=$mysql_host --user=$mysql_user --password=$mysql_password $mysql_database"
+PARAMS="--delete --local --fields-terminated-by=, --fields-enclosed-by=\" --lines-terminated-by=\n $LOGIN"
+
+download() {
+ local url=$1
+ local desc=$2
+ echo -n " $desc "
+ wget --progress=dot $url 2>&1 | grep --line-buffered "%" | sed -u -e "s,\.,,g" | awk '{printf("\b\b\b\b%4s", $2)}'
+ echo -ne " Done\n"
+}
+
+
+if test $die = "yes"; then
+ echo "You have to edit and configure this script first."
+ exit
+fi
+
+
+if test $geoip_database = "country"; then
+ echo "Downloading..."
+ download "$geoip_country_source" "Country Database:"
+ echo "Unpacking..."
+ unzip -jo GeoIPCountryCSV.zip
+ rm GeoIPCountryCSV.zip
+ echo "Converting to UFT-8..."
+ iconv -f ISO-8859-1 -t UTF-8 GeoIPCountryWhois.csv -o $prefix"geoip_country.csv"
+ rm GeoIPCountryWhois.csv
+ echo "Importing..."
+ mysqlimport --columns=@x,@x,start,end,countrycode,countryname $PARAMS $prefix"geoip_country.csv"
+ rm $prefix"geoip_country.csv" $prefix"geoip_country6.csv"
+ echo "Done..."
+elif test $geoip_database = "city"; then
+ echo "Downloading..."
+ download "$geoip_city_source" "City Database:"
+ download "$geoip_region_source" "Region Database:"
+ echo "Unpacking..."
+ unzip -jo GeoLiteCity-latest.zip
+ rm GeoLiteCity-latest.zip
+ echo "Converting to utf-8..."
+ iconv -f ISO-8859-1 -t UTF-8 GeoLiteCity-Blocks.csv -o $prefix"geoip_city_blocks.csv"
+ iconv -f ISO-8859-1 -t UTF-8 GeoLiteCity-Location.csv -o $prefix"geoip_city_location.csv"
+ iconv -f ISO-8859-1 -t UTF-8 region.csv -o $prefix"geoip_city_region.csv"
+ rm GeoLiteCity-Blocks.csv GeoLiteCity-Location.csv region.csv
+ echo "Importing..."
+ mysqlimport --columns=start,end,locID --ignore-lines=2 $PARAMS $prefix"geoip_city_blocks.csv"
+ mysqlimport --columns=locID,country,region,city,@x,latitude,longitude,@x,areaCode --ignore-lines=2 $PARAMS $prefix"geoip_city_location.csv"
+ mysqlimport --columns=country,region,regionname $PARAMS $prefix"geoip_city_region.csv"
+ rm $prefix"geoip_city_blocks.csv" $prefix"geoip_city_location.csv" $prefix"geoip_city_region.csv" $prefix"geoip_country6.csv"
+ echo "Done..."
+fi