anope

- supernets anope source code & configuration
git clone git://git.acid.vegas/anope.git
Log | Files | Refs | Archive | README

m_chanstats.cpp (24063B)

      1 /*
      2  *
      3  * (C) 2012-2022 Anope Team
      4  * Contact us at team@anope.org
      5  *
      6  * Please read COPYING and README for further details.
      7  */
      8 
      9 #include "module.h"
     10 #include "modules/sql.h"
     11 
     12 class CommandCSSetChanstats : public Command
     13 {
     14  public:
     15 	CommandCSSetChanstats(Module *creator) : Command(creator, "chanserv/set/chanstats", 2, 2)
     16 	{
     17 		this->SetDesc(_("Turn chanstats statistics on or off"));
     18 		this->SetSyntax(_("\037channel\037 {ON | OFF}"));
     19 	}
     20 
     21 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
     22 	{
     23 		ChannelInfo *ci = ChannelInfo::Find(params[0]);
     24 		if (!ci)
     25 		{
     26 			source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str());
     27 			return;
     28 		}
     29 
     30 		EventReturn MOD_RESULT;
     31 		FOREACH_RESULT(OnSetChannelOption, MOD_RESULT, (source, this, ci, params[1]));
     32 		if (MOD_RESULT == EVENT_STOP)
     33 			return;
     34 
     35 		if (MOD_RESULT != EVENT_ALLOW && !source.AccessFor(ci).HasPriv("SET") && source.permission.empty() && !source.HasPriv("chanserv/administration"))
     36 		{
     37 			source.Reply(ACCESS_DENIED);
     38 			return;
     39 		}
     40 
     41 		if (params[1].equals_ci("ON"))
     42 		{
     43 			ci->Extend<bool>("CS_STATS");
     44 			source.Reply(_("Chanstats statistics are now enabled for this channel."));
     45 			Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to enable chanstats";
     46 		}
     47 		else if (params[1].equals_ci("OFF"))
     48 		{
     49 			Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to disable chanstats";
     50 			ci->Shrink<bool>("CS_STATS");
     51 			source.Reply(_("Chanstats statistics are now disabled for this channel."));
     52 		}
     53 		else
     54 			this->OnSyntaxError(source, "");
     55 	}
     56 
     57 	bool OnHelp(CommandSource &source, const Anope::string &) anope_override
     58 	{
     59 		this->SendSyntax(source);
     60 		source.Reply(" ");
     61 		source.Reply(_("Turns chanstats statistics ON or OFF."));
     62 		return true;
     63 	}
     64 };
     65 
     66 class CommandNSSetChanstats : public Command
     67 {
     68  public:
     69 	CommandNSSetChanstats(Module *creator, const Anope::string &sname = "nickserv/set/chanstats", size_t min = 1 ) : Command(creator, sname, min, min + 1)
     70 	{
     71 		this->SetDesc(_("Turn chanstats statistics on or off"));
     72 		this->SetSyntax("{ON | OFF}");
     73 	}
     74 	void Run(CommandSource &source, const Anope::string &user, const Anope::string &param, bool saset = false)
     75 	{
     76 		NickAlias *na = NickAlias::Find(user);
     77 		if (!na)
     78 		{
     79 			source.Reply(NICK_X_NOT_REGISTERED, user.c_str());
     80 			return;
     81 		}
     82 
     83 		EventReturn MOD_RESULT;
     84 		FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, na->nc, param));
     85 		if (MOD_RESULT == EVENT_STOP)
     86 			return;
     87 
     88 		if (param.equals_ci("ON"))
     89 		{
     90 			Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to enable chanstats for " << na->nc->display;
     91 			na->nc->Extend<bool>("NS_STATS");
     92 			if (saset)
     93 				source.Reply(_("Chanstats statistics are now enabled for %s"), na->nc->display.c_str());
     94 			else
     95 				source.Reply(_("Chanstats statistics are now enabled for your nick."));
     96 		}
     97 		else if (param.equals_ci("OFF"))
     98 		{
     99 			Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to disable chanstats for " << na->nc->display;
    100 			na->nc->Shrink<bool>("NS_STATS");
    101 			if (saset)
    102 				source.Reply(_("Chanstats statistics are now disabled for %s"), na->nc->display.c_str());
    103 			else
    104 				source.Reply(_("Chanstats statistics are now disabled for your nick."));
    105 		}
    106 		else
    107 			this->OnSyntaxError(source, "CHANSTATS");
    108 	}
    109 
    110 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
    111 	{
    112 		this->Run(source, source.nc->display, params[0]);
    113 	}
    114 
    115 	bool OnHelp(CommandSource &source, const Anope::string &) anope_override
    116 	{
    117 		this->SendSyntax(source);
    118 		source.Reply(" ");
    119 		source.Reply(_("Turns chanstats statistics ON or OFF."));
    120 		return true;
    121 	}
    122 };
    123 
    124 class CommandNSSASetChanstats : public CommandNSSetChanstats
    125 {
    126  public:
    127 	CommandNSSASetChanstats(Module *creator) : CommandNSSetChanstats(creator, "nickserv/saset/chanstats", 2)
    128 	{
    129 		this->ClearSyntax();
    130 		this->SetSyntax(_("\037nickname\037 {ON | OFF}"));
    131 	}
    132 
    133 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
    134 	{
    135 		this->Run(source, params[0], params[1], true);
    136 	}
    137 
    138 	bool OnHelp(CommandSource &source, const Anope::string &) anope_override
    139 	{
    140 		this->SendSyntax(source);
    141 		source.Reply(" ");
    142 		source.Reply(_("Turns chanstats channel statistics ON or OFF for this user."));
    143 		return true;
    144 	}
    145 };
    146 
    147 class MySQLInterface : public SQL::Interface
    148 {
    149  public:
    150 	MySQLInterface(Module *o) : SQL::Interface(o) { }
    151 
    152 	void OnResult(const SQL::Result &r) anope_override
    153 	{
    154 	}
    155 
    156 	void OnError(const SQL::Result &r) anope_override
    157 	{
    158 		if (!r.GetQuery().query.empty())
    159 			Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError();
    160 		else
    161 			Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError();
    162 	}
    163 };
    164 
    165 class MChanstats : public Module
    166 {
    167 	SerializableExtensibleItem<bool> cs_stats, ns_stats;
    168 
    169 	CommandCSSetChanstats commandcssetchanstats;
    170 
    171 	CommandNSSetChanstats commandnssetchanstats;
    172 	CommandNSSASetChanstats commandnssasetchanstats;
    173 
    174 	ServiceReference<SQL::Provider> sql;
    175 	MySQLInterface sqlinterface;
    176 	SQL::Query query;
    177 	Anope::string SmileysHappy, SmileysSad, SmileysOther, prefix;
    178 	std::vector<Anope::string> TableList, ProcedureList, EventList;
    179 	bool NSDefChanstats, CSDefChanstats;
    180 
    181 	void RunQuery(const SQL::Query &q)
    182 	{
    183 		if (sql)
    184 			sql->Run(&sqlinterface, q);
    185 	}
    186 
    187 	size_t CountWords(const Anope::string &msg)
    188 	{
    189 		size_t words = 0;
    190 		for (size_t pos = 0; pos != Anope::string::npos; pos = msg.find(" ", pos+1))
    191 			words++;
    192 		return words;
    193 	}
    194 	size_t CountSmileys(const Anope::string &msg, const Anope::string &smileylist)
    195 	{
    196 		size_t smileys = 0;
    197 		spacesepstream sep(smileylist);
    198 		Anope::string buf;
    199 
    200 		while (sep.GetToken(buf) && !buf.empty())
    201 		{
    202 			for (size_t pos = msg.find(buf, 0); pos != Anope::string::npos; pos = msg.find(buf, pos+1))
    203 				smileys++;
    204 		}
    205 		return smileys;
    206 	}
    207 
    208 	const Anope::string GetDisplay(User *u)
    209 	{
    210 		if (u && u->Account() && ns_stats.HasExt(u->Account()))
    211 			return u->Account()->display;
    212 		else
    213 			return "";
    214 	}
    215 
    216 	void GetTables()
    217 	{
    218 		TableList.clear();
    219 		ProcedureList.clear();
    220 		EventList.clear();
    221 		if (!sql)
    222 			return;
    223 
    224 		SQL::Result r = this->sql->RunQuery(this->sql->GetTables(prefix));
    225 		for (int i = 0; i < r.Rows(); ++i)
    226 		{
    227 			const std::map<Anope::string, Anope::string> &map = r.Row(i);
    228 			for (std::map<Anope::string, Anope::string>::const_iterator it = map.begin(); it != map.end(); ++it)
    229 				TableList.push_back(it->second);
    230 		}
    231 		query = "SHOW PROCEDURE STATUS WHERE `Db` = Database();";
    232 		r = this->sql->RunQuery(query);
    233 		for (int i = 0; i < r.Rows(); ++i)
    234 		{
    235 			ProcedureList.push_back(r.Get(i, "Name"));
    236 		}
    237 		query = "SHOW EVENTS WHERE `Db` = Database();";
    238 		r = this->sql->RunQuery(query);
    239 		for (int i = 0; i < r.Rows(); ++i)
    240 		{
    241 			EventList.push_back(r.Get(i, "Name"));
    242 		}
    243 	}
    244 
    245 	bool HasTable(const Anope::string &table)
    246 	{
    247 		for (std::vector<Anope::string>::const_iterator it = TableList.begin(); it != TableList.end(); ++it)
    248 			if (*it == table)
    249 				return true;
    250 		return false;
    251 	}
    252 
    253 	bool HasProcedure(const Anope::string &table)
    254 	{
    255 		for (std::vector<Anope::string>::const_iterator it = ProcedureList.begin(); it != ProcedureList.end(); ++it)
    256 			if (*it == table)
    257 				return true;
    258 		return false;
    259 	}
    260 
    261 	bool HasEvent(const Anope::string &table)
    262 	{
    263 		for (std::vector<Anope::string>::const_iterator it = EventList.begin(); it != EventList.end(); ++it)
    264 			if (*it == table)
    265 				return true;
    266 		return false;
    267 	}
    268 
    269 
    270 	void CheckTables()
    271 	{
    272 		this->GetTables();
    273 		if (!this->HasTable(prefix +"chanstats"))
    274 		{
    275 			query = "CREATE TABLE `" + prefix + "chanstats` ("
    276 				"`id` int(11) NOT NULL AUTO_INCREMENT,"
    277 				"`chan` varchar(64) NOT NULL DEFAULT '',"
    278 				"`nick` varchar(64) NOT NULL DEFAULT '',"
    279 				"`type` ENUM('total', 'monthly', 'weekly', 'daily') NOT NULL,"
    280 				"`letters` int(10) unsigned NOT NULL DEFAULT '0',"
    281 				"`words` int(10) unsigned NOT NULL DEFAULT '0',"
    282 				"`line` int(10) unsigned NOT NULL DEFAULT '0',"
    283 				"`actions` int(10) unsigned NOT NULL DEFAULT '0',"
    284 				"`smileys_happy` int(10) unsigned NOT NULL DEFAULT '0',"
    285 				"`smileys_sad` int(10) unsigned NOT NULL DEFAULT '0',"
    286 				"`smileys_other` int(10) unsigned NOT NULL DEFAULT '0',"
    287 				"`kicks` int(10) unsigned NOT NULL DEFAULT '0',"
    288 				"`kicked` int(10) unsigned NOT NULL DEFAULT '0',"
    289 				"`modes` int(10) unsigned NOT NULL DEFAULT '0',"
    290 				"`topics` int(10) unsigned NOT NULL DEFAULT '0',"
    291 				"`time0` int(10) unsigned NOT NULL default '0',"
    292 				"`time1` int(10) unsigned NOT NULL default '0',"
    293 				"`time2` int(10) unsigned NOT NULL default '0',"
    294 				"`time3` int(10) unsigned NOT NULL default '0',"
    295 				"`time4` int(10) unsigned NOT NULL default '0',"
    296 				"`time5` int(10) unsigned NOT NULL default '0',"
    297 				"`time6` int(10) unsigned NOT NULL default '0',"
    298 				"`time7` int(10) unsigned NOT NULL default '0',"
    299 				"`time8` int(10) unsigned NOT NULL default '0',"
    300 				"`time9` int(10) unsigned NOT NULL default '0',"
    301 				"`time10` int(10) unsigned NOT NULL default '0',"
    302 				"`time11` int(10) unsigned NOT NULL default '0',"
    303 				"`time12` int(10) unsigned NOT NULL default '0',"
    304 				"`time13` int(10) unsigned NOT NULL default '0',"
    305 				"`time14` int(10) unsigned NOT NULL default '0',"
    306 				"`time15` int(10) unsigned NOT NULL default '0',"
    307 				"`time16` int(10) unsigned NOT NULL default '0',"
    308 				"`time17` int(10) unsigned NOT NULL default '0',"
    309 				"`time18` int(10) unsigned NOT NULL default '0',"
    310 				"`time19` int(10) unsigned NOT NULL default '0',"
    311 				"`time20` int(10) unsigned NOT NULL default '0',"
    312 				"`time21` int(10) unsigned NOT NULL default '0',"
    313 				"`time22` int(10) unsigned NOT NULL default '0',"
    314 				"`time23` int(10) unsigned NOT NULL default '0',"
    315 				"PRIMARY KEY (`id`),"
    316 				"UNIQUE KEY `chan` (`chan`,`nick`,`type`),"
    317 				"KEY `nick` (`nick`),"
    318 				"KEY `chan_` (`chan`),"
    319 				"KEY `type` (`type`)"
    320 				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
    321 			this->RunQuery(query);
    322 		}
    323 		/* There is no CREATE OR REPLACE PROCEDURE in MySQL */
    324 		if (this->HasProcedure(prefix + "chanstats_proc_update"))
    325 		{
    326 			query = "DROP PROCEDURE " + prefix + "chanstats_proc_update";
    327 			this->RunQuery(query);
    328 		}
    329 		query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_update`"
    330 			"(chan_ VARCHAR(255), nick_ VARCHAR(255), line_ INT(10), letters_ INT(10),"
    331 			"words_ INT(10), actions_ INT(10), sm_h_ INT(10), sm_s_ INT(10), sm_o_ INT(10),"
    332 			"kicks_ INT(10), kicked_ INT(10), modes_ INT(10), topics_ INT(10))"
    333 			"BEGIN "
    334 				"DECLARE time_ VARCHAR(20);"
    335 				"SET time_ = CONCAT('time', hour(now()));"
    336 				"INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES "
    337 					"('', chan_, 'total'), ('', chan_, 'monthly'),"
    338 					"('', chan_, 'weekly'), ('', chan_, 'daily');"
    339 				"IF nick_ != '' THEN "
    340 					"INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES "
    341 						"(nick_, chan_, 'total'), (nick_, chan_, 'monthly'),"
    342 						"(nick_, chan_, 'weekly'),(nick_, chan_, 'daily'),"
    343 						"(nick_, '', 'total'), (nick_, '', 'monthly'),"
    344 						"(nick_, '', 'weekly'), (nick_, '', 'daily');"
    345 				"END IF;"
    346 				"SET @update_query = CONCAT('UPDATE `" + prefix + "chanstats` SET line=line+', line_, ',"
    347 				"letters=letters+', letters_, ' , words=words+', words_, ', actions=actions+', actions_, ', "
    348 				"smileys_happy=smileys_happy+', sm_h_, ', smileys_sad=smileys_sad+', sm_s_, ', "
    349 				"smileys_other=smileys_other+', sm_o_, ', kicks=kicks+', kicks_, ', kicked=kicked+', kicked_, ', "
    350 				"modes=modes+', modes_, ', topics=topics+', topics_, ', ', time_ , '=', time_, '+', line_ ,' "
    351 				"WHERE (nick='''' OR nick=''', nick_, ''') AND (chan='''' OR chan=''', chan_, ''')');"
    352 				"PREPARE update_query FROM @update_query;"
    353 				"EXECUTE update_query;"
    354 				"DEALLOCATE PREPARE update_query;"
    355 			"END";
    356 		this->RunQuery(query);
    357 
    358 		if (this->HasProcedure(prefix + "chanstats_proc_chgdisplay"))
    359 		{
    360 			query = "DROP PROCEDURE " + prefix + "chanstats_proc_chgdisplay;";
    361 			this->RunQuery(query);
    362 		}
    363 		query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_chgdisplay`"
    364 			"(old_nick varchar(255), new_nick varchar(255))"
    365 			"BEGIN "
    366 			"DECLARE res_count int(10) unsigned;"
    367 			"SELECT COUNT(nick) INTO res_count FROM `" + prefix + "chanstats` WHERE nick = new_nick;"
    368 			"IF res_count = 0 THEN "
    369 				"UPDATE `" + prefix + "chanstats` SET `nick` = new_nick WHERE `nick` = old_nick;"
    370 			"ELSE "
    371 			"my_cursor: BEGIN "
    372 				"DECLARE no_more_rows BOOLEAN DEFAULT FALSE;"
    373 				"DECLARE chan_ VARCHAR(255);"
    374 				"DECLARE type_ ENUM('total', 'monthly', 'weekly', 'daily');"
    375 				"DECLARE letters_, words_, line_, actions_, smileys_happy_,"
    376 					"smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_,"
    377 					"time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, time9_,"
    378 					"time10_, time11_, time12_, time13_, time14_, time15_, time16_, time17_, time18_,"
    379 					"time19_, time20_, time21_, time22_, time23_ INT(10) unsigned;"
    380 				"DECLARE stats_cursor CURSOR FOR "
    381 					"SELECT chan, type, letters, words, line, actions, smileys_happy,"
    382 						"smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1,"
    383 						"time2, time3, time4, time5, time6, time7, time8, time9, time10, time11,"
    384 						"time12, time13, time14, time15, time16, time17, time18, time19, time20,"
    385 						"time21, time22, time23 "
    386 					"FROM `" + prefix + "chanstats` "
    387 					"WHERE `nick` = old_nick;"
    388 				"DECLARE CONTINUE HANDLER FOR NOT FOUND "
    389 					"SET no_more_rows = TRUE;"
    390 				"OPEN stats_cursor;"
    391 				"the_loop: LOOP "
    392 					"FETCH stats_cursor "
    393 					"INTO chan_, type_, letters_, words_, line_, actions_, smileys_happy_,"
    394 						"smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_,"
    395 						"time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_,"
    396 						"time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_,"
    397 						"time17_, time18_, time19_, time20_, time21_, time22_, time23_;"
    398 					"IF no_more_rows THEN "
    399 						"CLOSE stats_cursor;"
    400 						"LEAVE the_loop;"
    401 					"END IF;"
    402 					"INSERT INTO `" + prefix + "chanstats` "
    403 						"(chan, nick, type, letters, words, line, actions, smileys_happy, "
    404 						"smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1, "
    405 						"time2, time3, time4, time5, time6, time7, time8, time9, time10, time11,"
    406 						"time12, time13, time14, time15, time16, time17, time18, time19, time20,"
    407 						"time21, time22, time23)"
    408 					"VALUES (chan_, new_nick, type_, letters_, words_, line_, actions_, smileys_happy_,"
    409 						"smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_,"
    410 						"time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, "
    411 						"time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_, "
    412 						"time17_, time18_, time19_, time20_, time21_, time22_, time23_)"
    413 					"ON DUPLICATE KEY UPDATE letters=letters+VALUES(letters), words=words+VALUES(words),"
    414 						"line=line+VALUES(line), actions=actions+VALUES(actions),"
    415 						"smileys_happy=smileys_happy+VALUES(smileys_happy),"
    416 						"smileys_sad=smileys_sad+VALUES(smileys_sad),"
    417 						"smileys_other=smileys_other+VALUES(smileys_other),"
    418 						"kicks=kicks+VALUES(kicks), kicked=kicked+VALUES(kicked),"
    419 						"modes=modes+VALUES(modes), topics=topics+VALUES(topics),"
    420 						"time1=time1+VALUES(time1), time2=time2+VALUES(time2), time3=time3+VALUES(time3),"
    421 						"time4=time4+VALUES(time4), time5=time5+VALUES(time5), time6=time6+VALUES(time6),"
    422 						"time7=time7+VALUES(time7), time8=time8+VALUES(time8), time9=time9+VALUES(time9),"
    423 						"time10=time10+VALUES(time10), time11=time11+VALUES(time11), time12=time12+VALUES(time12),"
    424 						"time13=time13+VALUES(time13), time14=time14+VALUES(time14), time15=time15+VALUES(time15),"
    425 						"time16=time16+VALUES(time16), time17=time17+VALUES(time17), time18=time18+VALUES(time18),"
    426 						"time19=time19+VALUES(time19), time20=time20+VALUES(time20), time21=time21+VALUES(time21),"
    427 						"time22=time22+VALUES(time22), time23=time23+VALUES(time23);"
    428 				"END LOOP;"
    429 				"DELETE FROM `" + prefix + "chanstats` WHERE `nick` = old_nick;"
    430 			"END my_cursor;"
    431 			"END IF;"
    432 			"END;";
    433 		this->RunQuery(query);
    434 
    435 		/* don't prepend any database prefix to events so we can always delete/change old events */
    436 		if (this->HasEvent("chanstats_event_cleanup_daily"))
    437 		{
    438 			query = "DROP EVENT chanstats_event_cleanup_daily";
    439 			this->RunQuery(query);
    440 		}
    441 		query = "CREATE EVENT `chanstats_event_cleanup_daily` "
    442 			"ON SCHEDULE EVERY 1 DAY STARTS CURRENT_DATE "
    443 			"DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0,"
    444 				"smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0,"
    445 				"time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0,"
    446 				"time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0,"
    447 				"time20=0, time21=0, time22=0, time23=0 "
    448 			"WHERE type='daily';";
    449 		this->RunQuery(query);
    450 
    451 		if (this->HasEvent("chanstats_event_cleanup_weekly"))
    452 		{
    453 			query = "DROP EVENT `chanstats_event_cleanup_weekly`";
    454 			this->RunQuery(query);
    455 		}
    456 		query = "CREATE EVENT `chanstats_event_cleanup_weekly` "
    457 			"ON SCHEDULE EVERY 1 WEEK STARTS ADDDATE(CURDATE(), INTERVAL 1-DAYOFWEEK(CURDATE()) DAY) "
    458 			"DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0,"
    459 				"smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0,"
    460 				"time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0,"
    461 				"time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0,"
    462 				"time20=0, time21=0, time22=0, time23=0 "
    463 			"WHERE type='weekly';";
    464 		this->RunQuery(query);
    465 
    466 		if (this->HasEvent("chanstats_event_cleanup_monthly"))
    467 		{
    468 			query = "DROP EVENT `chanstats_event_cleanup_monthly`;";
    469 			this->RunQuery(query);
    470 		}
    471 		query = "CREATE EVENT `chanstats_event_cleanup_monthly` "
    472 			"ON SCHEDULE EVERY 1 MONTH STARTS LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY "
    473 			"DO BEGIN "
    474 			"UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0,"
    475 				"smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0,"
    476 				"time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0,"
    477 				"time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0, "
    478 				"time20=0, time21=0, time22=0, time23=0 "
    479 			"WHERE type='monthly';"
    480 			"OPTIMIZE TABLE `" + prefix + "chanstats`;"
    481 			"END;";
    482 		this->RunQuery(query);
    483 	}
    484 
    485 
    486  public:
    487 	MChanstats(const Anope::string &modname, const Anope::string &creator) :
    488 		Module(modname, creator, EXTRA | VENDOR),
    489 		cs_stats(this, "CS_STATS"), ns_stats(this, "NS_STATS"),
    490 		commandcssetchanstats(this), commandnssetchanstats(this), commandnssasetchanstats(this),
    491 		sqlinterface(this)
    492 	{
    493 	}
    494 
    495 	void OnReload(Configuration::Conf *conf) anope_override
    496 	{
    497 		Configuration::Block *block = conf->GetModule(this);
    498 		prefix = block->Get<const Anope::string>("prefix", "anope_");
    499 		SmileysHappy = block->Get<const Anope::string>("SmileysHappy");
    500 		SmileysSad = block->Get<const Anope::string>("SmileysSad");
    501 		SmileysOther = block->Get<const Anope::string>("SmileysOther");
    502 		NSDefChanstats = block->Get<bool>("ns_def_chanstats");
    503 		CSDefChanstats = block->Get<bool>("cs_def_chanstats");
    504 		Anope::string engine = block->Get<const Anope::string>("engine");
    505 		this->sql = ServiceReference<SQL::Provider>("SQL::Provider", engine);
    506 		if (sql)
    507 			this->CheckTables();
    508 		else
    509 			Log(this) << "no database connection to " << engine;
    510 	}
    511 
    512 	void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_all) anope_override
    513 	{
    514 		if (!show_all)
    515 			return;
    516 		if (cs_stats.HasExt(ci))
    517 			info.AddOption(_("Chanstats"));
    518 	}
    519 
    520 	void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) anope_override
    521 	{
    522 		if (!show_hidden)
    523 			return;
    524 		if (ns_stats.HasExt(na->nc))
    525 			info.AddOption(_("Chanstats"));
    526 	}
    527 
    528 	void OnTopicUpdated(User *source, Channel *c, const Anope::string &user, const Anope::string &topic) anope_override
    529 	{
    530 		if (!source || !source->Account() || !c->ci || !cs_stats.HasExt(c->ci))
    531 			return;
    532 		query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);";
    533 		query.SetValue("channel", c->name);
    534 		query.SetValue("nick", GetDisplay(source));
    535 		this->RunQuery(query);
    536 	}
    537 
    538 	EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string &param) anope_override
    539 	{
    540 		this->OnModeChange(c, setter.GetUser());
    541 		return EVENT_CONTINUE;
    542 	}
    543 
    544 	EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *, const Anope::string &param) anope_override
    545 	{
    546 		this->OnModeChange(c, setter.GetUser());
    547 		return EVENT_CONTINUE;
    548 	}
    549 
    550  private:
    551 	void OnModeChange(Channel *c, User *u)
    552 	{
    553 		if (!u || !u->Account() || !c->ci || !cs_stats.HasExt(c->ci))
    554 			return;
    555 
    556 		query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);";
    557 		query.SetValue("channel", c->name);
    558 		query.SetValue("nick", GetDisplay(u));
    559 		this->RunQuery(query);
    560 	}
    561 
    562  public:
    563 	void OnPreUserKicked(const MessageSource &source, ChanUserContainer *cu, const Anope::string &kickmsg) anope_override
    564 	{
    565 		if (!cu->chan->ci || !cs_stats.HasExt(cu->chan->ci))
    566 			return;
    567 
    568 		query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0);";
    569 		query.SetValue("channel", cu->chan->name);
    570 		query.SetValue("nick", GetDisplay(cu->user));
    571 		this->RunQuery(query);
    572 
    573 		query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0);";
    574 		query.SetValue("channel", cu->chan->name);
    575 		query.SetValue("nick", GetDisplay(source.GetUser()));
    576 		this->RunQuery(query);
    577 	}
    578 
    579 	void OnPrivmsg(User *u, Channel *c, Anope::string &msg) anope_override
    580 	{
    581 		if (!c->ci || !cs_stats.HasExt(c->ci))
    582 			return;
    583 
    584 		size_t letters = msg.length();
    585 		size_t words = this->CountWords(msg);
    586 
    587 		size_t action = 0;
    588 		if (msg.find("\01ACTION")!=Anope::string::npos)
    589 		{
    590 			action = 1;
    591 			letters = letters - 7;
    592 			words--;
    593 		}
    594 
    595 		// count smileys
    596 		size_t smileys_happy = CountSmileys(msg, SmileysHappy);
    597 		size_t smileys_sad = CountSmileys(msg, SmileysSad);
    598 		size_t smileys_other = CountSmileys(msg, SmileysOther);
    599 
    600 		// do not count smileys as words
    601 		size_t smileys = smileys_happy + smileys_sad + smileys_other;
    602 		if (smileys > words)
    603 			words = 0;
    604 		else
    605 			words = words - smileys;
    606 
    607 		query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 1, @letters@, @words@, @action@, "
    608 		"@smileys_happy@, @smileys_sad@, @smileys_other@, '0', '0', '0', '0');";
    609 		query.SetValue("channel", c->name);
    610 		query.SetValue("nick", GetDisplay(u));
    611 		query.SetValue("letters", letters);
    612 		query.SetValue("words", words);
    613 		query.SetValue("action", action);
    614 		query.SetValue("smileys_happy", smileys_happy);
    615 		query.SetValue("smileys_sad", smileys_sad);
    616 		query.SetValue("smileys_other", smileys_other);
    617 		this->RunQuery(query);
    618 	}
    619 
    620 	void OnDelCore(NickCore *nc) anope_override
    621 	{
    622 		query = "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = @nick@;";
    623 		query.SetValue("nick", nc->display);
    624 		this->RunQuery(query);
    625 	}
    626 
    627 	void OnChangeCoreDisplay(NickCore *nc, const Anope::string &newdisplay) anope_override
    628 	{
    629 		query = "CALL " + prefix + "chanstats_proc_chgdisplay(@old_display@, @new_display@);";
    630 		query.SetValue("old_display", nc->display);
    631 		query.SetValue("new_display", newdisplay);
    632 		this->RunQuery(query);
    633 	}
    634 
    635 	void OnDelChan(ChannelInfo *ci) anope_override
    636 	{
    637 		query = "DELETE FROM `" + prefix + "chanstats` WHERE `chan` = @channel@;";
    638 		query.SetValue("channel", ci->name);
    639 		this->RunQuery(query);
    640 	}
    641 
    642 	void OnChanRegistered(ChannelInfo *ci)
    643 	{
    644 		if (CSDefChanstats)
    645 			ci->Extend<bool>("CS_STATS");
    646 	}
    647 
    648 	void OnNickRegister(User *user, NickAlias *na, const Anope::string &)
    649 	{
    650 		if (NSDefChanstats)
    651 			na->nc->Extend<bool>("NS_STATS");
    652 	}
    653 };
    654 
    655 MODULE_INIT(MChanstats)