소스 검색

Init: stats.mod 1.3.3dev3

Florian Sander 13 년 전
커밋
aa38426f5a
24개의 변경된 파일10382개의 추가작업 그리고 0개의 파일을 삭제
  1. 27 0
      Makefile
  2. 243 0
      README
  3. 175 0
      UPDATES
  4. BIN
      bar.gif
  5. 1457 0
      datahandling.c
  6. 515 0
      dcccmds.c
  7. 147 0
      help/set/stats.help
  8. 95 0
      help/stats.help
  9. 1558 0
      livestats.c
  10. 407 0
      misc.c
  11. 290 0
      msgcmds.c
  12. 784 0
      pubcmds.c
  13. 499 0
      sensors.c
  14. 842 0
      slang.c
  15. 110 0
      slang.h
  16. 766 0
      stats.c
  17. 244 0
      stats.conf
  18. 234 0
      stats.ger.lang
  19. 396 0
      stats.h
  20. 243 0
      stats.lang
  21. 187 0
      tclstats.c
  22. 302 0
      user.c
  23. 627 0
      userrec.c
  24. 234 0
      webfiles.c

+ 27 - 0
Makefile

@@ -0,0 +1,27 @@
+# Makefile for src/mod/stats.mod/
+
+doofus:
+	@echo ""
+	@echo "Let's try this from the right directory..."
+	@echo ""
+	@cd ../../../; make
+
+clean:
+	@rm -f *.o *.so *~
+
+static: ../stats.o
+
+modules: ../../../stats.so
+
+../stats.o: ../module.h ../modvals.h ../../eggdrop.h datahandling.c \
+ stats.c sensors.c dcccmds.c misc.c pubcmds.c msgcmds.c webfiles.c \
+ user.c livestats.c userrec.c tclstats.c slang.c slang.h stats.h
+	$(CC) $(CFLAGS) $(CPPFLAGS) -DMAKING_MODS -c stats.c
+	rm -f ../stats.o
+	mv stats.o ../
+
+../../../stats.so: ../stats.o
+	$(LD) -o ../../../stats.so ../stats.o
+	$(STRIP) ../../../stats.so
+
+#safety hash

+ 243 - 0
README

@@ -0,0 +1,243 @@
+Description:
+------------
+
+Stats.mod generates statistics about the users in a channel. It counts
+the words that the users have spoken, the lines, actions, smileys,
+joins, quits, mode changes, topic changes, nick changes, kicks and how
+long the user has been on the channel. Depending on the configuration,
+it even logs the user's favourite words and the most used words on
+the channel.
+
+The statistics are accessable via public commands (!top10, !stat...)
+and via http.
+
+
+Installation:
+-------------
+
+Stats.mod will work with eggdrop1.6.x and eggdrop1.4.x.
+
+The following instructions assume, ~/eggdrop1.6/ is the directory
+where you installed your eggdrop from. (of course, other source dirs
+will work as well)
+Unfortunately, you need to compile stats.mod within your eggdrop source,
+so if you removed your original compile directory, you'll have to
+compile the whole bot again... sorry.
+
+Put stats.mod.1.3.2.tar.gz in ~/eggdrop1.6/src/mod/,
+and unpack it (tar xfz stats.mod.1.3.2.tar.gz). Change directory
+back to ~/eggdrop1.6/. Type 'make config' (on eggdrop 1.4, you can skip
+that part) Type 'make', wait until compiling
+is done and use 'make install' to install the bot and stats.mod.
+
+Don't forget to copy the language files from ~/eggdrop1.6/src/mod/stats.mod/ to
+~/eggdrop/language/ !
+
+All settings can be found in ~/eggdrop1.6/src/mod/stats.mod/stats.conf
+I suggest to copy it to your eggdrop directory (probably ~/eggdrop/),
+edit it to fit your needs and put a 'source stats.conf' at the end of
+your eggdrop config file. This will execute the config file with every
+restart or rehash.
+
+
+
+Public commands:
+----------------
+
+!top10 [ordering]
+  lists the top10 users in the channel
+!top20 [ordering]
+  same as !top10, but lists the top20 (surprise!)
+!stat [user]
+  shows the statistics for the user
+!place [user/ordering]
+  shows on which place the user is
+!ttop10/!ttop20/!tstat/!tplace
+  same as above, but only uses the statistics of today
+
+Only available if log-wordstats is turned on:
+!wordstats [user]
+  lists the most used words of the user
+!topwords
+  lists the most used words in the channel
+!top10 word <word>
+  lists the top10 people who used <word>
+
+Msg commands:
+-------------
+
+All public commands are also accessible via /msg.
+("/msg <bot> top10 #chan", for example)
+
+
+Additionally, there are two new commands:
+
+/msg <bot> setemail <email-address>
+  sets your email address
+/msg <bot> sethomepage <homepage-address>
+  sets the URL of your homepage
+
+DCC commands:
+-------------
+
+.+suser <user> [host]
+  adds a new user to the database
+.-suser <user>
+  removes a user from the database
+.+shost <user> <hostmask>
+  adds a new hostmask to <user>
+.-shost <user> <hostmask>
+  removes a hostmask from <user>
+.smatch [mask] [+/-list] [+/-addhosts]
+  lists all matching users
+.schannel #chan
+  displays the stats-users in a channel
+.schattr <user> <flags>
+  changes the flags for a stats user
+.chsusername <oldname> <newname>
+  changes the name of a user in the stats database
+.purgestats
+  deletes stats for non-existant users, empty stats and deletes expired users.
+  (m|-)
+.sumuser <user1> <user2>
+  adds all statistics of user2 to user1 and deletes user2
+  (n|-)
+.resetuser <user> <channel>
+  sets all statistics of user in channel to 0
+  (m|-)
+
+.writewebstats
+  writes the old, static webfiles
+  (m|-)
+
+TCL commands:
+-------------
+
+incrstats <user> <chan> <type> <value> [set]
+  increases the statistics of type <type> in <chan> for <user> by <value>.
+  If <set> is 1, the stats are not increased but set to the value.
+
+getstats <user> <chan> <type> [today]
+  returns the stats of <user> in <chan> of type <type>. If <today> is 1,
+  the statistics of today are returned.
+
+livestats <port>
+  starts listening for livestats connections on port <port>.
+  If <port> is "off" or "0", the bot will stop listening.
+
+resetuser <user> <channel>
+  sets all statistics of user in channel to 0
+
+resetslang
+  removes every language from the memory, so you can cleanly load a new
+  set of languages.
+
+loadslang [lang] <langfile>
+  loads a language
+
+setchanslang <channel> <language>
+  sets the language in <channel> to <language>
+
+nick2suser <nick> <channel>
+  returns the username of <nick> in <chan>
+
+
+Livestats:
+----------
+
+If you activate livestats, your bot will listen on a specified port for
+http connections. (accessable via http://your.shell.com:8033/, for example)
+The webfiles will then be generated on the fly. There will also be a little
+channel and user list which makes it easier for your users to browse through
+the several channel statistics of your bot.
+  This method also uses less cpu power than the frequently written webfiles,
+because only the file that is really needed is generated. 
+  Use .console +1 to see access, or use the setting stats-loglevel to
+change the loglevel.
+  Stats.mod is now also able to log the access to a CLF logfile, so you can
+use your favourite loganalyzer to generate stats of the stats access. :)
+
+Hint: If you don't want a channel to be listed on the index,
+----  just set it +secret.
+
+Static Webfiles:
+---------------
+
+If there's really no way to use livestats, stats.mod can also generate static
+web files.
+In this case, it simply mirrors the whole livestats server into a directory
+on your shell. Of course, this takes some time. You bot won't be able to
+do anything while it's generating the files, so only use it if there's no
+other way. (expect an occasional ping timeout on large channels)
+Be sure that you chose an empty output directory.
+
+User management:
+----------------
+
+Since v1.3.0, stats.mod has an internal userdatabase. It can automatically
+add users and hostmasks to it. Users which are in the eggdrop userfile have a
+higher priority, so if someone is known as "bla" by the bot and as "blub"
+by stats.mod, his stats will be logged to "bla".
+  Hostmasks will be removed from a user if they haven't been used for a
+specified time. If all hosts got removed from a user, the user will be erased
+completely.
+  Note that stats.mod can't know when someone used IDENT or if someone added
+new hosts to a user manually. The affected user should cycle the chan to make
+sure that he's recognized correctly by stats.mod.
+
+There are two flags for users: +/-list and +/-addhosts:
+- If a user has the flag -list, he/she/it won't be listed in the top10. (you
+  probably want to set all your bot -list)
+- If a user is -addhosts, no new hostmasks will be added to him/her. You'll
+  have to add all hosts manually with .+shost. (useful if someone if trying
+  to fake you)
+
+Channel settings:
+-----------------
+
+If you're using eggdrop 1.5 or later, you have some additional config options
+available via .chanset:
+
++nopubstats
+	Disables all channel commands (!top10 etc...)
++quietstats
+	Bot will reply with a notice directly to the user instead of
+	sending the reply to the channel
++nostats
+	Don't log any stats in this chan at all.
+
+Other:
+------
+
+There is absolutely NO WARRANTY on this module. I do my best to make it
+work properly, but if anything gets screwed up, I'm not responsible. Use
+this module at your own risk.
+
+Known problems:
+---------------
+
+There can be a problem if you use wordstats on a bot with enabled memory
+debugging. The reset of the daily stats might take a long time on slow shells.
+If your bot is on big channels, your memory table might be too small.
+Edit src/mem.c and change the #define that sets the size of the memtable
+(#define MEMTBLSIZE 25000      /* yikes! */) to a higher value.
+
+Feedback:
+---------
+
+Feel free to send feedback and bugreports(I hope there won't be any<g>) to
+stats.mod@visions-of-fantasy.de
+
+Homepage:
+---------
+
+The latest version of stats.mod(and a few addons) can always be found at
+http://www.visions-of-fantasy.de/stats.mod/
+
+Thanks to:
+----------
+
+- Fabian for teaching me plenty of things
+- Johoho and Fox_Muld for various bug reports and suggestions
+- dw for the idea of livestats and for many bug reports
+- the eggdev team for developing eggdrop

+ 175 - 0
UPDATES

@@ -0,0 +1,175 @@
+Changes in Stats.mod: (since v1.0.1)
+--------------------
+
+1.3.3
+- fixed bug which could crash the bot in certain cases
+- links for weekly/monthly graphs were switched
+- possible length of CSS was a little bit too limited (fix by Vetinari`)
+
+1.3.2
+- optimized sorting functions, livestats should be faster now
+- wordstats now use a binary searchtree => faster
+- loading should be faster now
+- added config file option: $quote-frequency
+- fixed rather unimportant bug (module didn't unload correctly
+  if it detected an incompatible eggdrop version)
+- new langfile entries: 
+         "place" (#502)
+         T idle idle-factor
+- new stat-type: idle (factor of minutes and lines) (idea by Zev)
+- !stat and !place had some problems with users from the stats-userbase
+- fixed problem with strict-host 0 (I hate this setting! It's useless and only
+  causes confusion and bugs! Maybe someone should remove it from eggdrop*sigh*)
+- if a user is online with 2 (or more) clients, he/she will not receive
+  online-time for each client anymore. (fix by Zev Toledano)
+- !stat wasn't using langfiles for parts of the output
+- !top10 and !place didn't recognize "slanged" sorting-types
+- fixed crash when writing static (*cough*) webfiles for channels, which the
+  bot has already left.
+- misc stats crashed if a IPv6 host was logged on the same day (*doh*)
+- updated helpfiles
+
+1.3.1
+- stats.mod now also works with eggdrop 1.6
+- you can bind livestats to one ip now
+- .chsusername can now also change the case of usernames
+
+1.3.0
+- stats.mod is now no longer dependant on the userfile (hurray!)
+- added multi-language support
+- added background-color-fading for the tables
+- added URL-logger
+- added kick-logger
+- static webfiles are now identical to livestats (which makes them
+  more detailed and way slower)
+- added livestats-flood-protection
+- added new setting list-secret-chans
+- users can now set their email- and homepage-addresses (which are displayed
+  on their personal stats pages)
+- "average users"-table has now two rows
+- splitted /chan/onchan from /chan/users
+- livestats now listen on every vhost
+- added +/-nostats
+- monthly/weekly peak wasn't resetted
+- !place couldn't handle long nicks
+
+1.2.3
+- livestats access can now be .+ignore'ed
+- user's which match $anti-autoadd-flags won't be expired anymore
+- replaced a few nmalloc's by user_malloc's to solve problems with
+  memory debugging
+- added $min-word-length
+- added +/-quietstats
+
+1.2.2
+- added some checks to prevent division by 0
+
+1.2.1
+- filt2() was buggy
+- livestats access to !chans, &chans, etc was unnecessary
+  complicated (it's /!chan/ now, as it should have been
+  from the beginning)
+- purging stats for an empty channel caused memleak and
+  non-nmalloc'ed pointers which could lead to a crash
+  on some bots
+
+1.2.0
+- added wordstats
+- added random quote
+- added new stat type: questions
+- added misc stats section to livestats
+- added meta refresh tag
+- inactive channels aren't listed in server root anymore
+- the column by which the livestats are sorted is now bold
+- added weekly and monthly stats
+- improved navigation
+- user peak is now also shown in the webfiles
+- added words per line
+- !place <type> didn't work
+- pubcmds did not work in !channels
+- improved sorting of /chan/users/
+- +secret chans are no longer listed in /
+- added livestats support for !chans +chans and &chans
+
+
+1.1.9
+- fixed yet another evil bug. I'm verry sorry... :(
+
+1.1.8
+- fixed evil bug that allowed anyone to crash the bot :(
+- added GPL licence stuff
+
+1.1.7
+- .sumuser now also transfers the starting time
+- commands were not logged
+- added cmdchar setting
+
+1.1.6
+- added webfile-suffix
+- bot won't add users with wildcards in their host anymore
+
+1.1.5
+- access to logfiles is now logged in apache format
+- IPs were turned upside down
+- changed detection of old bots, so even people who use weird patches can
+  compile this mod without problems
+- some msg commands were damaged
+- stats.mod was incompatible to the new lagcheck feature in egg1.5
+
+1.1.4
+- duration in the userlist wasn't calculated correctly
+- socket is now killed when the buffer is empty on egg1.5 (patch by fabian)
+- http-equiv refresh could be set to 0 which made browsers reload them every
+  minute :)
+- added a few '\n's
+- livestats create now less debug output
+- removed a space from !stat reply :)
+- module didn't compile if DEBUG_CONTEXT was undefined
+- sumuser now also transfers hostmasks
+- added .resetuser and [resetuser]
+- IP is now also shown on egg1.4
+- livestats access is now logged to a definable log level (1 by default)
+
+1.1.3
+- some bots crashed when they tried to delete expired users <blush>
+
+1.1.2
+- fixed problem with un-allocated sockets
+- auto-expireing of old users did not work
+- minutes on livestats-user-pages are now in duration-format
+- updates to readme and config file
+- removed the '?' from the comment of auto-added users (don't ask me why
+  it was there <g>)
+
+1.1.1
+- removed incompatibility to eggdrop1.4.1 (<sigh>)
+- added setting "stats-file-mode" to make the mode of the stats file
+  configurable
+- added meta tags in the html files
+- fixed minor bug that caused a bit too much debug output when using !place
+- added readme :))
+- one back-link was broken
+- module didn't send the correct http headers
+- tcl commands didn't get unloaded with the modul. This could cause a crash.
+
+1.1.0
+- added msg commands
+- added livestats ^_^
+- stats of unknown users are now also saved (makes debugging easier)
+- top20 reached from 10 to 20, not from 11 to 20
+- you can now define yourself which users should be excluded from the stats
+- added user peak
+- bot won't autoadd itself anymore
+- added simple flood protection
+- fixed bug in !place
+- grmpf... it did not work on eggdrop1.4 (why didn't anyone tell me that???<g>)
+
+1.0.2
+- added compatibility to 1.4.x
+- started this updates history <g>
+- added help files
+- added smileys to !stat
+- changed "chan->name" to "chan->dname"
+- users with zero stats were still listed
+- link to today's stats showed up even if write-today was disabled
+- timers for webfile generation and saving triggered one minute too late

BIN
bar.gif


+ 1457 - 0
datahandling.c

@@ -0,0 +1,1457 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static void incrstats(char *user, char *chan, int type, int value, int set)
+{
+  globstats *gs, *gs2;
+  locstats *ls, *ls2;
+  int i, ii;
+
+  if (type >= TOTAL_TYPES)
+    return;
+  if (!user) {
+    debug0("Stats.mod: incrstats(..) Ups, user is NULL!");
+    return;
+  }
+  if (!chan) {
+    debug0("Stats.mod: incrstats(..) Ups, chan is NULL!");
+    return;
+  }
+  for (gs = sdata; gs; gs = gs->next) {
+    if (!strcasecmp(chan, gs->chan))
+      break;
+  }
+  if (!gs) {
+    gs2 = sdata;
+    while (gs2 && gs2->next)
+      gs2 = gs2->next;
+    gs = nmalloc(sizeof(globstats));
+    gs->started = now;
+    gs->peak[S_TOTAL] = gs->peak[S_DAILY] = gs->peak[S_WEEKLY] = gs->peak[S_MONTHLY] = 0;
+    for (i = 0; i < 24; i++) {
+      gs->users[S_USERSUM][i] = 0;
+      gs->users[S_USERCOUNTS][i] = -1;
+    }
+    for (i = 0; i < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); i++)
+      gs->slocal[S_TOTAL][i] = gs->slocal[S_DAILY][i] = gs->slocal[S_WEEKLY][i] = gs->slocal[S_MONTHLY][i] = NULL;
+    gs->next = NULL;
+    gs->local = NULL;
+    gs->topics = NULL;
+    gs->hosts = NULL;
+    gs->urls = NULL;
+    gs->log = gs->lastlog = NULL;
+    gs->log_length = 0;
+    gs->kicks = NULL;
+    gs->words = NULL;
+    gs->chan = nmalloc(strlen(chan) + 1);
+    strcpy(gs->chan, chan);
+    if (gs2)
+      gs2->next = gs;
+    else
+      sdata = gs;
+  }
+  for (ls = gs->local; ls; ls = ls->next) {
+    if (!strcasecmp(ls->user, user))
+      break;
+  }
+  if (type == T_GSTARTED) {
+    gs->started = value;
+    return;
+  }
+  if (type == T_PEAK) {
+    gs->peak[set] = value;
+    return;
+  }
+  if (!ls) {
+    ls2 = gs->local;
+    while (ls2 && ls2->next)
+      ls2 = ls2->next;
+    ls = nmalloc(sizeof(locstats));
+    ls->started = now;
+    ls->next = NULL;
+    ls->words = NULL;
+    ls->tree = NULL;
+    ls->quotes = NULL;
+    ls->quotefr = 0;
+    ls->flag = 0;
+    for (i = 0; i < TOTAL_TYPES; i++) {
+      ls->values[S_TOTAL][i] = 0;
+      ls->values[S_TODAY][i] = 0;
+      ls->values[S_WEEKLY][i] = 0;
+      ls->values[S_MONTHLY][i] = 0;
+    }
+    ls->user = nmalloc(strlen(user) + 1);
+    strcpy(ls->user, user);
+    // we'll initialize this later, if it's needed
+    ls->u = NULL;
+    if (ls2)
+      ls2->next = ls;
+    else
+      gs->local = ls;
+    for (i = 0; i < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); i++)
+      ls->snext[S_TOTAL][i] = ls->snext[S_DAILY][i] = ls->snext[S_WEEKLY][i] = ls->snext[S_MONTHLY][i] = NULL;
+    for (i = 0; i < 4; i++) {
+      for (ii = 0; ii < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); ii++) {
+        ls2 = gs->slocal[i][ii];
+        while (ls2 && ls2->snext[i][ii])
+          ls2 = ls2->snext[i][ii];
+        if (ls2)
+          ls2->snext[i][ii] = ls;
+        else
+          gs->slocal[i][ii] = ls;
+      }
+    }
+  }
+  if (type == T_LSTARTED)
+    ls->started = value;
+  else {
+    if (set > 0)
+      ls->values[set - 1][type] = value;
+    else if (set < 0)
+      ls->values[(set * (-1)) - 1][type] += value;
+    else {
+      ls->values[S_TOTAL][type] += value;
+      ls->values[S_TODAY][type] += value;
+      ls->values[S_WEEKLY][type] += value;
+      ls->values[S_MONTHLY][type] += value;
+    }
+  }
+}
+
+static void nincrstats(locstats *ls, int type, int value)
+{
+  ls->values[S_TOTAL][type] += value;
+  ls->values[S_TODAY][type] += value;
+  ls->values[S_WEEKLY][type] += value;
+  ls->values[S_MONTHLY][type] += value;
+}
+
+static locstats *initstats(char *chan, char *user)
+{
+  globstats *gs, *gs2;
+  locstats *ls, *ls2;
+  int i, ii;
+
+  gs = sdata;
+  while (gs) {
+    if (!rfc_casecmp(gs->chan, chan))
+      break;
+    gs = gs->next;
+  }
+  if (!gs) {
+    gs2 = sdata;
+    while (gs2 && gs2->next)
+      gs2 = gs2->next;
+    gs = nmalloc(sizeof(globstats));
+    gs->started = now;
+    gs->peak[S_TOTAL] = gs->peak[S_DAILY] = gs->peak[S_WEEKLY] = gs->peak[S_MONTHLY] = 0;
+    for (i = 0; i < 24; i++) {
+      gs->users[S_USERSUM][i] = 0;
+      gs->users[S_USERCOUNTS][i] = -1;
+    }
+    gs->next = NULL;
+    gs->local = NULL;
+    gs->words = NULL;
+    gs->topics = NULL;
+    gs->hosts = NULL;
+    gs->urls = NULL;
+    gs->log = gs->lastlog = NULL;
+    gs->log_length = 0;
+    gs->kicks = NULL;
+    gs->chan = nmalloc(strlen(chan) + 1);
+    strcpy(gs->chan, chan);
+    for (i = 0; i < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); i++)
+      gs->slocal[S_TOTAL][i] = gs->slocal[S_DAILY][i] = gs->slocal[S_WEEKLY][i] = gs->slocal[S_MONTHLY][i] = NULL;
+    if (gs2)
+      gs2->next = gs;
+    else
+      sdata = gs;
+  }
+  for (ls = gs->local; ls; ls = ls->next) {
+    if (!rfc_casecmp(ls->user, user))
+      return ls;
+  }
+  if (!ls) {
+    ls2 = gs->local;
+    while (ls2 && ls2->next)
+      ls2 = ls2->next;
+    ls = nmalloc(sizeof(locstats));
+    ls->started = now;
+    ls->next = NULL;
+    ls->words = NULL;
+    ls->tree = NULL;
+    ls->quotes = NULL;
+    ls->quotefr = 0;
+    ls->flag = 0;
+    for (i = 0; i < TOTAL_TYPES; i++) {
+      ls->values[S_TOTAL][i] = 0;
+      ls->values[S_TODAY][i] = 0;
+      ls->values[S_WEEKLY][i] = 0;
+      ls->values[S_MONTHLY][i] = 0;
+    }
+    ls->user = nmalloc(strlen(user) + 1);
+    strcpy(ls->user, user);
+    // we'll initialize this later, if it's needed
+    ls->u = NULL;
+    if (ls2)
+      ls2->next = ls;
+    else
+      gs->local = ls;
+    for (i = 0; i < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); i++)
+      ls->snext[S_TOTAL][i] = ls->snext[S_DAILY][i] = ls->snext[S_WEEKLY][i] = ls->snext[S_MONTHLY][i] = NULL;
+    for (i = 0; i < 4; i++) {
+      for (ii = 0; ii < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); ii++) {
+        ls2 = gs->slocal[i][ii];
+        while (ls2 && ls2->snext[i][ii])
+          ls2 = ls2->snext[i][ii];
+        if (ls2)
+          ls2->snext[i][ii] = ls;
+        else
+          gs->slocal[i][ii] = ls;
+      }
+    }
+  }
+  return ls;
+}
+
+static int getstats(char *user, char *chan, char *type, int today)
+{
+  struct stats_global *gs = sdata;
+  struct stats_local *ls;
+  int itype;
+
+  while (gs) {
+    ls = gs->local;
+    if (!strcasecmp(gs->chan, chan)) {
+      while (ls) {
+        if (!strcasecmp(ls->user, user)) {
+          itype = typetoi(type);
+          if (itype >= 0)
+            return ls->values[today][itype];
+        }
+        ls = ls->next;
+      }
+    }
+    gs = gs->next;
+  }
+  return 0;
+}
+
+static void sortstats(struct stats_global *gs, int itype, int today)
+{
+  int again = 1;
+  struct stats_local *last, *p, *c, *n;
+  int a, b, pitype;
+
+  Context;
+  again = 1;
+  last = NULL;
+  if (itype < 0) {
+    // switch to the special sorting function
+    switch (itype) {
+      case T_WPL:
+        sortstats_wpl(gs, today);
+        break;
+      case T_VOCABLES:
+        sortstats_vocables(gs, today);
+        break;
+      case T_WORD:
+        sortstats_word(gs, today);
+        break;
+      case T_IDLE:
+        sortstats_idle(gs, today);
+        break;
+      default:
+        debug1("Missing sorting algorithm for \"%d\"!!!", itype);
+    }
+    return;
+  }
+
+  // if (itype < 0) pitype = (TOTAL_TYPES - 1) + (itype * -1);
+  // not needed here
+  pitype = itype;
+  while ((gs->slocal[today][pitype] != last) && (again)) {
+    p = NULL;
+    c = gs->slocal[today][pitype];
+    n = c->snext[today][pitype];
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        a = c->values[today][itype];
+        b = n->values[today][itype];
+      }
+      if (a < b) {
+        again = 1;
+        c->snext[today][pitype] = n->snext[today][pitype];
+        n->snext[today][pitype] = c;
+        if (p == NULL)
+          gs->slocal[today][pitype] = n;
+        else
+          p->snext[today][pitype] = n;
+      }
+      p = c;
+      c = n;
+      n = n->snext[today][pitype];
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+static void sortstats_wpl(struct stats_global *gs, int today)
+{
+  int again = 1;
+  struct stats_local *last, *p, *c, *n;
+  int a, b, pitype;
+
+  Context;
+  again = 1;
+  last = NULL;
+  pitype = (T_WPL * (-1)) + TOTAL_TYPES - 1;
+  while ((gs->slocal[today][pitype] != last) && (again)) {
+    p = NULL;
+    c = gs->slocal[today][pitype];
+    n = c->snext[today][pitype];
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        if (c->values[today][T_LINES])
+          a = (int) (((float) c->values[today][T_WORDS] / (float) c->values[today][T_LINES]) * 100.0);
+        else
+          a = 0;
+        if (n->values[today][T_LINES])
+          b = (int) (((float) n->values[today][T_WORDS] / (float) n->values[today][T_LINES]) * 100.0);
+        else
+          b = 0;
+      }
+      if (a < b) {
+        again = 1;
+        c->snext[today][pitype] = n->snext[today][pitype];
+        n->snext[today][pitype] = c;
+        if (p == NULL)
+          gs->slocal[today][pitype] = n;
+        else
+          p->snext[today][pitype] = n;
+      }
+      p = c;
+      c = n;
+      n = n->snext[today][pitype];
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+static void sortstats_vocables(struct stats_global *gs, int today)
+{
+  int again = 1;
+  struct stats_local *last, *p, *c, *n;
+  int a, b, pitype;
+
+  Context;
+  again = 1;
+  last = NULL;
+  countvocables(gs);
+  pitype = (T_VOCABLES * (-1)) + TOTAL_TYPES - 1;
+  while ((gs->slocal[today][pitype] != last) && (again)) {
+    p = NULL;
+    c = gs->slocal[today][pitype];
+    n = c->snext[today][pitype];
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        a = c->vocables;
+        b = n->vocables;
+      }
+      if (a < b) {
+        again = 1;
+        c->snext[today][pitype] = n->snext[today][pitype];
+        n->snext[today][pitype] = c;
+        if (p == NULL)
+          gs->slocal[today][pitype] = n;
+        else
+          p->snext[today][pitype] = n;
+      }
+      p = c;
+      c = n;
+      n = n->snext[today][pitype];
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+static void sortstats_word(struct stats_global *gs, int today)
+{
+  int again = 1;
+  struct stats_local *last, *p, *c, *n;
+  int a, b, pitype;
+
+  Context;
+  again = 1;
+  last = NULL;
+  pitype = (T_WORD * (-1)) + TOTAL_TYPES - 1;
+  while ((gs->slocal[today][pitype] != last) && (again)) {
+    p = NULL;
+    c = gs->slocal[today][pitype];
+    n = c->snext[today][pitype];
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        if (c->word)
+          a = c->word->nr;
+        else
+          a = 0;
+        if (n->word)
+          b = n->word->nr;
+        else
+          b = 0;
+      }
+      if (a < b) {
+        again = 1;
+        c->snext[today][pitype] = n->snext[today][pitype];
+        n->snext[today][pitype] = c;
+        if (p == NULL)
+          gs->slocal[today][pitype] = n;
+        else
+          p->snext[today][pitype] = n;
+      }
+      p = c;
+      c = n;
+      n = n->snext[today][pitype];
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+// sort stats by idle-factor (minutes/lines)
+static void sortstats_idle(struct stats_global *gs, int today)
+{
+  int again = 1;
+  struct stats_local *last, *p, *c, *n;
+  int a, b, pitype;
+
+  Context;
+  again = 1;
+  last = NULL;
+  pitype = (T_IDLE * (-1)) + TOTAL_TYPES - 1;
+  while ((gs->slocal[today][pitype] != last) && (again)) {
+    p = NULL;
+    c = gs->slocal[today][pitype];
+    n = c->snext[today][pitype];
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        if (c->values[today][T_LINES])
+          a = (int) (((float) c->values[today][T_MINUTES] / (float) c->values[today][T_LINES]) * 100.0);
+        else
+          a = 0;
+        if (n->values[today][T_LINES])
+          b = (int) (((float) n->values[today][T_MINUTES] / (float) n->values[today][T_LINES]) * 100.0);
+        else
+          b = 0;
+      }
+      if (a < b) {
+        again = 1;
+        c->snext[today][pitype] = n->snext[today][pitype];
+        n->snext[today][pitype] = c;
+        if (p == NULL)
+          gs->slocal[today][pitype] = n;
+        else
+          p->snext[today][pitype] = n;
+      }
+      p = c;
+      c = n;
+      n = n->snext[today][pitype];
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+static void countvocables(globstats *gs)
+{
+  locstats *ls;
+  wordstats *ws;
+
+  for (ls = gs->local; ls; ls = ls->next) {
+    ls->vocables = 0;
+    for (ws = ls->words; ws; ws = ws->next)
+      ls->vocables++;
+  }
+}
+
+static void sortwordstats(locstats *ls, globstats *gs)
+{
+  int again = 1;
+  wordstats *last, *p, *c, *n, *tmp;
+  int a, b;
+
+  Context;
+  again = 1;
+  last = NULL;
+  if (ls)
+    tmp = ls->words;
+  else
+    tmp = gs->words;
+  while ((tmp != last) && (again)) {
+    p = NULL;
+    if (ls)
+      c = ls->words;
+    else
+      c = gs->words;
+    n = c->next;
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        a = c->nr;
+        b = n->nr;
+      }
+      if (a < b) {
+  again = 1;
+  c->next = n->next;
+  n->next = c;
+  if (p == NULL) {
+    if (ls)
+      ls->words = n;
+    else
+      gs->words = n;
+    tmp = n;
+  } else
+    p->next = n;
+      }
+      p = c;
+      c = n;
+      n = n->next;
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+static void sorthosts(struct stats_global *gs)
+{
+  int again = 1;
+  hoststr *last, *p, *c, *n;
+  int a, b;
+
+  Context;
+  again = 1;
+  last = NULL;
+  while ((gs->hosts != last) && (again)) {
+    p = NULL;
+    c = gs->hosts;
+    n = c->next;
+    again = 0;
+    while (n != last) {
+      if (!c || !n)
+        a = b = 0;
+      else {
+        a = c->nr;
+        b = n->nr;
+      }
+      if (a < b) {
+        again = 1;
+        c->next = n->next;
+        n->next = c;
+        if (p == NULL)
+          gs->hosts = n;
+        else
+          p->next = n;
+      }
+      p = c;
+      c = n;
+      n = n->next;
+    }
+    last = c;
+  }
+  Context;
+  return;
+}
+
+// typetoi(): returns the index of a stat-type
+static int typetoi(char *type)
+{
+  if (!strcasecmp(type, "lstarted"))
+    return T_LSTARTED;
+  else if (!strcasecmp(type, "gstarted"))
+    return T_GSTARTED;
+  else if (!strcasecmp(type, "words"))
+    return T_WORDS;
+  else if (!strcasecmp(type, "letters"))
+    return T_LETTERS;
+  else if (!strcasecmp(type, "minutes"))
+    return T_MINUTES;
+  else if (!strcasecmp(type, "topics"))
+    return T_TOPICS;
+  else if (!strcasecmp(type, "lines"))
+    return T_LINES;
+  else if (!strcasecmp(type, "actions"))
+    return T_ACTIONS;
+  else if (!strcasecmp(type, "kicks"))
+    return T_KICKS;
+  else if (!strcasecmp(type, "modes"))
+    return T_MODES;
+  else if (!strcasecmp(type, "bans"))
+    return T_BANS;
+  else if (!strcasecmp(type, "nicks"))
+    return T_NICKS;
+  else if (!strcasecmp(type, "joins"))
+    return T_JOINS;
+  else if (!strcasecmp(type, "smileys"))
+    return T_SMILEYS;
+  else if (!strcasecmp(type, "questions"))
+    return T_QUESTIONS;
+  else if (!strcasecmp(type, "wpl"))
+    return T_WPL;
+  else if (!strcasecmp(type, "w/l"))
+    return T_WPL;
+  else if (!strcasecmp(type, "word"))
+    return T_WORD;
+  else if (!strcasecmp(type, "vocables"))
+    return T_VOCABLES;
+  else if (!strcasecmp(type, "started"))
+    return T_LSTARTED;
+  else if (!strcasecmp(type, "quote"))
+    return T_QUOTE;
+  else if (!strcasecmp(type, "idle"))
+    return T_IDLE;
+  else {
+    debug1("Stats.mod: Unknown stat type: %s", type);
+    return T_ERROR;
+  }
+}
+
+static locstats *findlocstats(char *chan, char *user)
+{
+  globstats *gl;
+  locstats *ll;
+
+  for (gl = sdata; gl; gl = gl->next) {
+    if (!rfc_casecmp(gl->chan, chan))
+      break;
+  }
+  if (!gl)
+    return NULL;
+  for (ll = gl->local; ll; ll = ll->next) {
+    if (!rfc_casecmp(ll->user, user))
+      return ll;
+  }
+  return NULL;
+}
+
+static globstats *findglobstats(char *chan)
+{
+  globstats *gl;
+
+  for (gl = sdata; gl; gl = gl->next) {
+    if (!rfc_casecmp(gl->chan, chan))
+      break;
+  }
+  return gl;
+}
+
+static void write_stats()
+{
+  char s[125];
+  FILE *f;
+  struct stats_global *gs;
+  struct stats_local *ls;
+  struct stats_userlist *u;
+  struct stats_hostlist *h;
+  int i;
+
+  Context;
+  if (!statsfile[0])
+    return;
+  sprintf(s, "%s~new", statsfile);
+  f = fopen(s, "w");
+  chmod(s, statsfilemode);
+  if (f == NULL) {
+    putlog(LOG_MISC, "*", "ERROR writing stats file.");
+    return;
+  }
+  fprintf(f, "@ # Statistics from %s.\n", botnetnick);
+  fprintf(f, "@ filever 1\n");
+  fprintf(f, "@ month %d\n", getmonth());
+  for (gs = sdata; gs; gs = gs->next) {
+    fprintf(f, "%s ! %d\n", gs->chan, (int) gs->started);
+    fprintf(f, "%s @ %d\n", gs->chan, gs->peak[S_TOTAL]);
+    fprintf(f, "@ peaks %s %d %d %d %d\n", gs->chan, gs->peak[S_TOTAL],
+            gs->peak[S_DAILY], gs->peak[S_WEEKLY], gs->peak[S_MONTHLY]);
+    for (ls = gs->local; ls; ls = ls->next) {
+      fprintf(f, "%s %s %d", gs->chan, ls->user, (int) ls->started);
+      for (i = 0; i < TOTAL_TYPES; i++)
+        fprintf(f, " %ld", ls->values[S_TOTAL][i]);
+      fprintf(f, "\n");
+      fprintf(f, "@ daily %s %s", gs->chan, ls->user);
+      for (i = 0; i < TOTAL_TYPES; i++)
+        fprintf(f, " %ld", ls->values[S_DAILY][i]);
+      fprintf(f, "\n");
+      fprintf(f, "@ weekly %s %s", gs->chan, ls->user);
+      for (i = 0; i < TOTAL_TYPES; i++)
+        fprintf(f, " %ld", ls->values[S_WEEKLY][i]);
+      fprintf(f, "\n");
+      fprintf(f, "@ monthly %s %s", gs->chan, ls->user);
+      for (i = 0; i < TOTAL_TYPES; i++)
+        fprintf(f, " %ld", ls->values[S_MONTHLY][i]);
+      fprintf(f, "\n");
+      i = 0;
+    }
+  }
+  for (u = suserlist; u; u = u->next) {
+    fprintf(f, "@ user %s %d %d", u->user, u->list, u->addhosts);
+    for (h = u->hosts; h; h = h->next) {
+      fprintf(f, " %s %lu", h->mask, h->lastused);
+    }
+    fprintf(f, "\n");
+    if (u->email || u->homepage) {
+      fprintf(f, "@ uxtra %s", u->user);
+      if (u->email)
+        fprintf(f, " e %s", u->email);
+      if (u->homepage)
+        fprintf(f, " h %s", u->homepage);
+      fprintf(f, "\n");
+    }
+  }
+  fclose(f);
+  unlink(statsfile);
+  movefile(s, statsfile);
+  Context;
+  return;
+}
+
+static void read_stats()
+{
+  FILE *f;
+  char buf[SAVESTATSLENGTH + 1];
+  char *s, *chan, *user, *cmd, *host, *tmp;
+  int i, version, range, month, list, addhosts;
+  struct stats_userlist *u;
+  time_t lastused;
+  locstats *ls;
+  globstats *gs;
+
+  Context;
+  ls = NULL;
+  gs = NULL;
+  version = 0;
+  month = 0;
+  f = fopen(statsfile, "r");
+  if (f == NULL) {
+    putlog(LOG_MISC, "*", "ERROR reading stats file");
+    return;
+  }
+  free_stats();
+  while (!feof(f)) {
+    buf[0] = 0;
+    s = buf;
+    fgets(s, SAVESTATSLENGTH - 1, f);
+    s[SAVESTATSLENGTH - 1] = 0;
+    if (buf[0] == 0)
+      continue;
+    if (s[strlen(s) - 1] == '\n')
+      s[strlen(s) - 1] = 0;
+    chan = newsplit(&s);
+    if (!strcmp(chan, "@")) {
+      cmd = newsplit(&s);
+      if (!strcmp(cmd, "filever"))
+	version = atoi(newsplit(&s));
+      else if (!strcmp(cmd, "month"))
+        month = atoi(newsplit(&s));
+      else if (!strcmp(cmd, "peaks")) {
+        chan = newsplit(&s);
+        incrstats("*", chan, T_PEAK, atoi(newsplit(&s)), S_TOTAL);
+        incrstats("*", chan, T_PEAK, atoi(newsplit(&s)), S_DAILY);
+        incrstats("*", chan, T_PEAK, atoi(newsplit(&s)), S_WEEKLY);
+        incrstats("*", chan, T_PEAK, atoi(newsplit(&s)), S_MONTHLY);
+      } else if (!strcmp(cmd, "daily") || !strcmp(cmd, "weekly")
+                 || !strcmp(cmd, "monthly")) {
+        if (!strcmp(cmd, "daily"))
+	  range = S_DAILY;
+        else if (!strcmp(cmd, "weekly"))
+	  range = S_WEEKLY;
+        else if (!strcmp(cmd, "monthly"))
+          range = S_MONTHLY;
+        else {
+          debug2("Error while reading statsfile: range uninitialized! (%s %s)", cmd, s);
+          continue;
+        }
+        if ((range == S_MONTHLY) && (month != lastmonth))
+          continue;
+        chan = newsplit(&s);
+        user = newsplit(&s);
+        // Check if pointers still point to the correct data and
+        // update them, if not.
+        if ((gs && strcmp(gs->chan, chan)) || !gs) {
+          gs = findglobstats(chan);
+          ls = findlocstats(chan, user);
+        } else {
+          if ((ls && strcmp(ls->user, user)) || !ls)
+            ls = findlocstats(chan, user);
+        }
+        if (!ls)
+          ls = initstats(chan, user);
+        for (i = 0; i < TOTAL_TYPES; i++)
+          ls->values[range][i] = atoi(newsplit(&s));
+      } else if (!strcmp(cmd, "user")) {
+	user = newsplit(&s);
+	list = atoi(newsplit(&s));
+	addhosts = atoi(newsplit(&s));
+	u = addsuser(user, list, addhosts);
+	while (s[0]) {
+	  host = newsplit(&s);
+	  lastused = (time_t) atoi(newsplit(&s));
+	  saddhost(u, host, lastused);
+	}
+      } else if (!strcmp(cmd, "uxtra")) {
+        user = newsplit(&s);
+        u = findsuser_by_name(user);
+        while (u && s[0]) {
+          tmp = newsplit(&s);
+          if (!strcmp(tmp, "e"))
+            setemail(u, newsplit(&s));
+          else
+            sethomepage(u, newsplit(&s));
+        }
+      }
+    } else {
+      // old style data
+      // left-over from v1.0. I should change it, but I don't want
+      // to break compatibility
+      user = newsplit(&s);
+      if (!strcmp(user, "!"))
+        incrstats(user, chan, T_GSTARTED, atoi(newsplit(&s)), 1);
+      else if (!strcmp(user, "@"))
+        incrstats(user, chan, T_PEAK, atoi(newsplit(&s)), S_TOTAL);
+      else {
+        incrstats(user, chan, T_LSTARTED, atoi(newsplit(&s)), 1);
+        // initstats also returns the current 'ls' if it also exists,
+        // so better don't even use findlocstats() before to save
+        // some CPU-time
+        ls = initstats(chan, user);
+        for (i = 0; i < TOTAL_TYPES; i++)
+          ls->values[S_TOTAL][i] = atoi(newsplit(&s));
+      }
+    }
+  }
+  fclose(f);
+  Context;
+  return;
+}
+
+static void reset_tstats()
+{
+  globstats *gs;
+  locstats *ls;
+  int i;
+
+  Context;
+  putlog(LOG_MISC, "*", "Stats.mod: Resetting today's statistics...");
+  for (gs = sdata; gs; gs = gs->next) {
+    gs->peak[S_TODAY] = 0;
+    free_wordstats(gs->words);
+    gs->words = NULL;
+    free_topics(gs->topics);
+    gs->topics = NULL;
+    free_urls(gs->urls);
+    gs->urls = NULL;
+    free_hosts(gs->hosts);
+    gs->hosts = NULL;
+    free_kicks(gs->kicks);
+    gs->kicks = NULL;
+    for (ls = gs->local; ls; ls = ls->next) {
+      free_wordstats(ls->words);
+      ls->words = NULL;
+      ls->tree = NULL;
+      free_quotes(ls->quotes);
+      ls->quotes = NULL;
+      for (i = 0; i < TOTAL_TYPES; i++)
+        ls->values[S_TODAY][i] = 0;
+    }
+  }
+  Context;
+}
+
+static void reset_mwstats(int range)
+{
+  globstats *gs;
+  locstats *ls;
+  int i;
+
+  Context;
+  putlog(LOG_MISC, "*", "Stats.mod: Resetting %s statistics...", (range == S_WEEKLY) ? "weekly" : "monthly");
+  for (gs = sdata; gs; gs = gs->next) {
+    gs->peak[range] = 0;
+    for (ls = gs->local; ls; ls = ls->next) {
+      for (i = 0; i < TOTAL_TYPES; i++)
+        ls->values[range][i] = 0;
+    }
+  }
+  Context;
+}
+
+static void sort_stats_alphabetically(globstats *gs)
+{
+  locstats *as, *bs, *l, *last;
+  int a, b, again = 1;
+  char *astr, *bstr, n[2];
+
+  Context;
+  n[0] = n[1] = 0;
+  last = NULL;
+  while ((gs->local != last) && again) {
+    again = 0;
+    l = NULL;
+    as = gs->local;
+    bs = gs->local->next;
+    while(bs) {
+      if (!as)
+        astr = n;
+      else
+        astr = as->user;
+      if (!bs)
+        bstr = n;
+      else
+        bstr = bs->user;
+      a = (int) tolower(astr[0]);
+      b = (int) tolower(bstr[0]);
+      while ((a == b) && a && b) {
+	astr++;
+	bstr++;
+        a = (int) tolower(astr[0]);
+        b = (int) tolower(bstr[0]);
+      }
+      if (a > b) {
+        if (!l)
+          gs->local = bs;
+        else
+          l->next = bs;
+        as->next = bs->next;
+        bs->next = as;
+        again = 1;
+        if (l == NULL)
+          gs->local = bs;
+        else
+          l = bs;
+      }
+      l = as;
+      as = bs;
+      bs = bs->next;
+    }
+    last = as;
+  }
+  Context;
+}
+
+static void resetlocstats(locstats *ls)
+{
+  int i;
+
+  if (!ls) {
+    debug0("ERROR! resetlocstats called with NULL pointer!");
+    return;
+  }
+  for (i = 0; i < TOTAL_TYPES; i++) {
+    ls->values[S_TOTAL][i] = 0;
+    ls->values[S_TODAY][i] = 0;
+    ls->values[S_WEEKLY][i] = 0;
+    ls->values[S_MONTHLY][i] = 0;
+  }
+  return;
+}
+
+static void calcwordstats(char *hand, globstats *gs, char *rest, locstats *stats)
+{
+  locstats *ls;
+  char *word;
+  int i;
+
+  Context;
+  if (!log_wordstats)
+    return;
+  if (!gs) {
+    debug1("Can't calculate wordstats for %s, no globstats.", hand);
+    return;
+  }
+  if (stats)
+    ls = stats;
+  else {
+    for (ls = gs->local; ls; ls = ls->next)
+      if (!rfc_casecmp(hand, ls->user))
+        break;
+  }
+  if (!ls) {
+    debug2("Can't calculate wordstats for %s in %s, no locstats.", hand, gs->chan);
+    return;
+  }
+  for (i = 0; i < strlen(rest); i++)
+    if (strchr("!?.,\"<>&\\", rest[i]))
+      rest[i] = ' ';
+  while (rest[0]) {
+    word = newsplit(&rest);
+    strlower(word);
+    incrwordstats(ls, word, 1, 0);
+  }
+}
+
+// add another entry to the tree
+static void incrwordstats(locstats *ls, char *word, int value, int set)
+{
+  wordstats *ne, *te, *le;
+  wordstats *ll;
+  int cmp;
+
+  Context;
+  if ((word[0] == ' ') || !word[0])
+    return;
+  if (min_word_length && (strlen(word) < min_word_length))
+    return;     /* only log words that are longer than min_word_length chars */
+  if (!ls) {
+    return;
+  }
+  // at first, check if it already exists and only needs to be updated
+  te = ls->tree;
+  le = NULL;
+  while (te) {
+    if (!(cmp = strcasecmp(te->word, word)))
+      break;
+    le = te;
+    if (cmp < 0)
+      te = te->left;
+    else
+      te = te->right;
+  }
+  if (!te) { // nothing to update, so let's append a new node
+    ne = nmalloc(sizeof(struct stats_words));
+    ne->word = nmalloc(strlen(word) + 1);
+    strcpy(ne->word, word);
+    ne->nr = 0;
+    ne->left = ne->right = ne->next = NULL;
+    if (!le)  // no last entry -> new entry is going to be the crown
+      ls->tree = ne;
+    else {
+      if (strcasecmp(le->word, word) < 0)  // -1 -> left child
+        le->left = ne;
+      else                                  // 1 -> right child
+        le->right = ne;
+    }
+    // now let's add it also to the linked list (needed for sorting)
+    ll = ls->words;
+    while (ll && ll->next)
+      ll = ll->next;
+    if (ll)
+      ll->next = ne;
+    else
+      ls->words = ne;
+    te = ne;
+  }
+  // now let's set the value
+  if (set)
+    te->nr = value;
+  else
+    te->nr += value;
+}
+
+static void nincrwordstats(globstats *gs, char *word, int value)
+{
+  wordstats *l, *ll;
+
+  for (l = gs->words; l; l = l->next)
+    if (!strcmp(word, l->word))
+      break;
+  if (!l) {
+    l = gs->words;
+    while (l && l->next)
+      l = l->next;
+    ll = nmalloc(sizeof(wordstats));
+    ll->word = nmalloc(strlen(word) + 1);
+    strcpy(ll->word, word);
+    ll->nr = 0;
+    ll->next = NULL;
+    if (l)
+      l->next = ll;
+    else
+      gs->words = ll;
+    l = ll;
+  }
+  l->nr += value;
+}
+
+static void do_globwordstats(globstats *gs)
+{
+  wordstats *l;
+  locstats *ls;
+
+  for (l = gs->words; l; l = l->next)
+    l->nr = 0;
+  for (ls = gs->local; ls; ls = ls->next)
+    for (l = ls->words; l; l = l->next)
+      nincrwordstats(gs, l->word, l->nr);
+  sortwordstats(NULL, gs);
+}
+
+static void addquote(char *user, globstats *gs, char *quote, locstats *stats)
+{
+  quotestr *l, *nl;
+  locstats *ls;
+
+  if (!quote_freq)
+    return;
+  if (!gs) {
+    debug1("Can't add quote to %s, no globstats.", user);
+    return;
+  }
+  if (stats)
+    ls = stats;
+  else {
+    for (ls = gs->local; ls; ls = ls->next)
+      if (!rfc_casecmp(user, ls->user))
+        break;
+  }
+  if (!ls) {
+    debug2("Can't add quote to %s in %s, no locstats.", user, gs->chan);
+    return;
+  }
+  ls->quotefr--;
+  if (ls->quotefr > 0)
+    return;
+  ls->quotefr = quote_freq;
+  l = ls->quotes;
+  while (l && l->next)
+    l = l->next;
+  nl = nmalloc(sizeof(quotestr));
+  nl->next = NULL;
+  nl->quote = nmalloc(strlen(quote) + 1);
+  strcpy(nl->quote, quote);
+  if (l)
+    l->next = nl;
+  else
+    ls->quotes = nl;
+}
+
+static void addtopic(char *channel, char *topic, char *by)
+{
+  topicstr *e, *ne;
+  globstats *gs;
+
+  Context;
+  gs = findglobstats(channel);
+  if (!gs)
+    return;
+  for (e = gs->topics; e; e = e->next)
+    if (!strcasecmp(topic, e->topic))
+      return;
+  e = gs->topics;
+  while (e && e->next)
+    e = e->next;
+  ne = nmalloc(sizeof(topicstr));
+  ne->topic = nmalloc(strlen(topic) + 1);
+  strcpy(ne->topic, topic);
+  ne->by = nmalloc(strlen(by) + 1);
+  strcpy(ne->by, by);
+  ne->when = now;
+  ne->next = NULL;
+  if (e)
+    e->next = ne;
+  else
+    gs->topics = ne;
+}
+
+static void addhost(char *host, globstats *gs)
+{
+  hoststr *e, *ne;
+  char *s;
+
+  if (!gs || !host)
+    return;
+  s = strchr(host, '@');
+  if (s)
+    host = s + 1;
+  if (strcmp(host, "[IP]"))
+    strlower(host);
+  for (e = gs->hosts; e; e = e->next) {
+    if (!strcmp(host, e->host)) {
+      e->nr++;
+      return;
+    }
+  }
+  e = gs->hosts;
+  while (e && e->next)
+    e = e->next;
+  ne = nmalloc(sizeof(hoststr));
+  ne->host = nmalloc(strlen(host) + 1);
+  strcpy(ne->host, host);
+  ne->nr = 1;
+  ne->next = NULL;
+  if (e)
+    e->next = ne;
+  else
+    gs->hosts = ne;
+  return;
+}
+
+static void setword(globstats *gs, char *word)
+{
+  locstats *l;
+  wordstats *w;
+
+  for (l = gs->local; l; l = l->next) {
+    l->word = NULL;
+    for (w = l->words; w; w = w->next) {
+      if (!strcmp(w->word, word)) {
+        l->word = w;
+        break;
+      }
+    }
+  }
+}
+
+static int track_stat_user(char *oldnick, char *newnick)
+{
+  globstats *gs;
+  locstats *ls;
+  struct stats_userlist *u;
+  int found = 0;
+
+  Context;
+  for (gs = sdata; gs; gs = gs->next) {
+    for (ls = gs->local; ls; ls = ls->next) {
+      if (!rfc_casecmp(oldnick, ls->user) && strcmp(newnick, ls->user)) {
+        nfree(ls->user);
+        ls->user = nmalloc(strlen(newnick) + 1);
+        strcpy(ls->user, newnick);
+        // ls->u should still be valid...
+        found = 1;
+        debug3("Transferred stats from %s to %s in %s", oldnick, newnick, gs->chan);
+      }
+    }
+  }
+  for (u = suserlist; u; u = u->next) {
+    if (!rfc_casecmp(oldnick, u->user) && strcmp(newnick, u->user)) {
+      nfree(u->user);
+      u->user = nmalloc(strlen(newnick) + 1);
+      strcpy(u->user, newnick);
+      found = 1;
+      debug2("Changed user name from %s to %s in my local database.", oldnick, newnick);
+    }
+  }
+  if (found)
+    return 1;
+  return 0;
+}
+
+static void check_for_url(char *user, char *chan, char *text)
+{
+  char *p, *url;
+  char *tmp, *tmp2, *t;
+  struct stats_url *e, *ne;
+  globstats *gs;
+  int weiter;
+
+  if (log_urls < 1)
+    return;
+  gs = findglobstats(chan);
+  if (!gs)
+    return;
+  url = p = tmp = tmp2 = t = NULL;
+  if ((p = strstr(text, "http://")))
+    url = newsplit(&p);
+  else if ((p = strstr(text, "ftp://")))
+    url = newsplit(&p);
+  else if (strstr(text, "www.") || strstr(text, ".com") || strstr(text, "ftp.")) {
+    tmp = nmalloc(strlen(text) + 1);
+    strcpy(tmp, text);
+    t = tmp;
+    while (t[0]) {
+      p = newsplit(&t);
+      if (strstr(p, "www.") || strstr(p, ".com") || strstr(text, "ftp.")) {
+        url = p;
+        break;
+      }
+    }
+  }
+  if (!url)
+    return;
+  if (strchr(url, '@')) { /* probably an email address or something similar */
+    if (tmp)
+      nfree(tmp);
+    return;
+  }
+  if (strncasecmp(url, "http://", 7) && strncasecmp(url, "ftp://", 6)) {
+    if (!strncasecmp(url, "ftp.", 4)) {
+      tmp2 = nmalloc(strlen(url) + 6 + 1);
+      strcpy(tmp2, "ftp://");
+      strcpy(tmp2 + 6, url);
+    } else {
+      tmp2 = nmalloc(strlen(url) + 7 + 1);
+      strcpy(tmp2, "http://");
+      strcpy(tmp2 + 7, url);
+    }
+    url = tmp2;
+  }
+  for (e = gs->urls; e; e = e->next) {
+    if (!strcmp(e->url, url)) {
+      nfree(e->by);
+      e->by = nmalloc(strlen(user) + 1);
+      strcpy(e->by, user);
+      e->when = now;
+      if (tmp)
+        nfree(tmp);
+      if (tmp2)
+        nfree(tmp2);
+      return;
+    }
+  }
+  weiter = 1;
+  for (p = url; (p != url) && weiter; p--) {
+    if (strchr(".!?,#", p[0]))
+      p[0] = 0;
+    else
+      weiter = 0;
+  }
+  for (e = gs->urls; e && e->next; e = e->next);
+  ne = nmalloc(sizeof(struct stats_url));
+  ne->url = nmalloc(strlen(url) + 1);
+  strcpy(ne->url, url);
+  ne->by = nmalloc(strlen(user) + 1);
+  strcpy(ne->by, user);
+  ne->when = now;
+  ne->next = NULL;
+  if (e)
+    e->next = ne;
+  else
+    gs->urls = ne;
+  if (tmp)
+    nfree(tmp);
+  if (tmp2)
+    nfree(tmp2);
+  debug2("Logged URL: \"%s\" mentioned by %s.", ne->url, ne->by);
+}
+
+static void add_chanlog(globstats *gs, char *nick, char *text, int type)
+{
+  char ts[20];
+  time_t tt, ttbuf;
+  quotestr *newlog, *l;
+
+  if (!gs || (kick_context < 1))
+    return;
+  ttbuf = tt = now;
+  strftime(ts, 19, "[%H:%M:%S]", localtime(&tt));
+  newlog = nmalloc(sizeof(quotestr));
+  newlog->next = NULL;
+  if (type == SL_PRIVMSG) {
+    newlog->quote = nmalloc(strlen(nick) + strlen(ts) + strlen(text) + 11);
+    sprintf(newlog->quote, "%s &lt;%s&gt; %s", ts, nick, text);
+  } else if (type == SL_KICK) {
+    newlog->quote = nmalloc(strlen(text) + strlen(ts) + 2);
+    sprintf(newlog->quote, "%s %s", ts, text);
+  } else if (type == SL_MODE) {
+    newlog->quote = nmalloc(strlen(ts) + strlen(text) + 2);
+    sprintf(newlog->quote, "%s %s", ts, text);
+  } else if (type == SL_NICK) {
+    newlog->quote = nmalloc(strlen(ts) + strlen(nick) + strlen(text) + 19);
+    sprintf(newlog->quote, "%s %s changed nick to %s", ts, nick, text);
+  } else if (type == SL_PART) {
+    newlog->quote = nmalloc(strlen(ts) + strlen(nick) + strlen(gs->chan) + 12);
+    sprintf(newlog->quote, "%s %s has left %s", ts, nick, gs->chan);
+  } else if (type == SL_JOIN) {
+    newlog->quote = nmalloc(strlen(ts) + strlen(nick) + strlen(gs->chan) + 14);
+    sprintf(newlog->quote, "%s %s has joined %s", ts, nick, gs->chan);
+  } else if (type == SL_QUIT) {
+    newlog->quote = nmalloc(strlen(ts) + strlen(nick) + strlen(text) + 18);
+    sprintf(newlog->quote, "%s %s has quit IRC (%s)", ts, nick, text);
+  } else {
+    debug1("Unknown log-type: %d !!!", type);
+    newlog->quote = NULL;
+  }
+  if (gs->lastlog)
+    gs->lastlog->next = newlog;
+  else
+    gs->log = newlog;
+  gs->lastlog = newlog;
+  gs->log_length++;
+  while ((gs->log_length > kick_context) && (gs->log_length > 0)) {
+    l = gs->log->next;
+    nfree(gs->log->quote);
+    if (gs->lastlog == gs->log)
+      gs->lastlog = NULL;
+    nfree(gs->log);
+    gs->log = l;
+    gs->log_length--;
+  }
+  ctime(&ttbuf); /* workaround for a bug in older eggdrops */
+}
+
+static void save_kick(globstats *gs, char *kick)
+{
+  struct stats_kick *k, *nk;
+  quotestr *log, *l, *nl;
+
+  Context;
+  if (!gs)
+    return;
+  for (k = gs->kicks; k && k->next; k = k->next);
+  nk = nmalloc(sizeof(struct stats_kick));
+  nk->next = NULL;
+  nk->log = NULL;
+  if (!gs->log || (kick_context < 1)) {
+    nl = nmalloc(sizeof(quotestr));
+    nl->quote = nmalloc(strlen(kick) + 1);
+    strcpy(nl->quote, kick);
+    nl->next = NULL;
+    nk->log = nl;
+  } else {
+    for (log = gs->log; log; log = log->next) {
+      nl = nmalloc(sizeof(quotestr));
+      nl->quote = nmalloc(strlen(log->quote) + 1);
+      strcpy(nl->quote, log->quote);
+      nl->next = NULL;
+      for (l = nk->log; l && l->next; l = l->next);
+      if (l)
+        l->next = nl;
+      else
+        nk->log = nl;
+    }
+  }
+  if (k)
+    k->next = nk;
+  else
+    gs->kicks = nk;
+  debug1("Logged kick in %s.", gs->chan);
+}

+ 515 - 0
dcccmds.c

@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int cmd_savestats(struct userrec *u, int idx, char *par)
+{
+  write_stats();
+  putlog(LOG_CMDS, "*", "#%s# savestats", dcc[idx].nick);
+  return 0;
+}
+
+static int cmd_loadstats(struct userrec *u, int idx, char *par)
+{
+  read_stats();
+  putlog(LOG_CMDS, "*", "#%s# loadstats", dcc[idx].nick);
+  return 0;
+}
+
+static int cmd_writewebstats(struct userrec *u, int idx, char *par)
+{
+  Context;
+#if EGG_IS_MIN_VER(10500)
+  write_new_webstats();
+#else
+  dprintf(idx, "Sorry, this function isn't available with eggdrop1.4.\n");
+  dprintf(idx, "Either update your bot to eggdrop1.5+ or use livestats instead of static webfiles.\n");
+  dprintf(idx, "(or even better: do both! <g>)\n");
+#endif
+  putlog(LOG_CMDS, "*", "#%s# writewebstats %s", dcc[idx].nick, par);
+  Context;
+  return 0;
+}
+
+static int cmd_purgestats(struct userrec *u, int idx, char *par)
+{
+  Context;
+  deloldstatusers();
+  Context;
+  purgestats();
+  Context;
+  update_schannel_members();
+  Context;
+  putlog(LOG_CMDS, "*", "#%s# purgestats", dcc[idx].nick);
+  Context;
+  return 0;
+}
+
+static int cmd_sumuser(struct userrec *user, int idx, char *par)
+{
+  struct userrec *u1, *u2;
+  struct stats_userlist *uu1, *uu2;
+  struct stats_hostlist *h;
+  char *user1, *user2, *hand1, *hand2;
+  globstats *gs;
+  locstats *ls;
+  int i;
+  struct list_type *q;
+
+  Context;
+  user1 = newsplit(&par);
+  user2 = par;
+  u1 = get_user_by_handle(userlist, user1);
+  u2 = get_user_by_handle(userlist, user2);
+  uu1 = findsuser_by_name(user1);
+  uu2 = findsuser_by_name(user2);
+  if (!user1[0] || !user2[0]) {
+    dprintf(idx, "Usage: .sumuser <user1> <user2>\n");
+    return 0;
+  }
+  if (use_userfile && !u1) {
+    dprintf(idx, "%s isn't a valid user!\n", user1);
+    return 0;
+  }
+  if (use_userfile && !u2) {
+    dprintf(idx, "%s isn't a valid user!\n", user2);
+    return 0;
+  }
+  if (!use_userfile && !uu1) {
+    dprintf(idx, "%s isn't a valid user!\n", user1);
+    return 0;
+  }
+  if (!use_userfile && !uu2) {
+    dprintf(idx, "%s isn't a valid user!\n", user2);
+    return 0;
+  }
+  if (use_userfile) {
+    hand1 = u1->handle;
+    hand2 = u2->handle;
+  } else {
+    hand1 = uu1->user;
+    hand2 = uu2->user;
+  }
+  for (gs = sdata; gs; gs = gs->next) {
+    for (ls = gs->local; ls; ls = ls->next) {
+      if (!strcasecmp(ls->user, hand2)) {
+        for (i = 0; i < TOTAL_TYPES; i++) {
+          incrstats(hand1, gs->chan, i, ls->values[S_TOTAL][i], (S_TOTAL + 1) * (-1));
+          incrstats(hand1, gs->chan, i, ls->values[S_DAILY][i], (S_DAILY + 1) * (-1));
+          incrstats(hand1, gs->chan, i, ls->values[S_WEEKLY][i], (S_WEEKLY + 1) * (-1));
+          incrstats(hand1, gs->chan, i, ls->values[S_MONTHLY][i], (S_MONTHLY + 1) * (-1));
+        }
+        if (getstats(hand1, "", "gstarted", 0)
+            < getstats(hand2, "", "gstarted", 0))
+          incrstats(hand1, "", T_LSTARTED, getstats(hand2, "", "gstarted", 0), 1);
+      }
+    }
+  }
+  if (use_userfile) {
+    for (q = get_user(&USERENTRY_HOSTS, u2); q; q = q->next)
+      addhost_by_handle(u1->handle, q->extra);
+    deluser(u2->handle);
+  } else {
+    for (h = uu2->hosts; h; h = h->next)
+      saddhost(uu1, h->mask, h->lastused);
+    delsuser(uu2->user);
+  }
+  dprintf(idx, "Transferred stats from %s to %s and deleted %s\n", user2,
+  	  hand1, user2);
+  putlog(LOG_CMDS, "*", "#%s# sumuser %s %s", dcc[idx].nick, user1, user2);
+  update_schannel_members();
+  Context;
+  return 0;
+}
+
+static int cmd_resetuser(struct userrec *u, int idx, char *par)
+{
+  char *user, *chan;
+  locstats *ls;
+
+  Context;
+  user = newsplit(&par);
+  chan = newsplit(&par);
+  ls = findlocstats(chan, user);
+  if (!ls) {
+    dprintf(idx, "Couldn't find stats for %s in %s.\n", user, chan);
+    return 0;
+  }
+  resetlocstats(ls);
+  dprintf(idx, "%s's stats set to 0 on %s.\n", ls->user, chan);
+  putlog(LOG_CMDS, "*", "#%s# resetuser %s %s", dcc[idx].nick, user, chan);
+  Context;
+  return 0;
+}
+
+static int cmd_schannel(struct userrec *u, int idx, char *par)
+{
+  char *chname, spaces[50], *user, ubuf[2];
+  struct stats_chanset *chan;
+  struct stats_memberlist *m;
+  int len = 0;
+
+  Context;
+  ubuf[0] = '*';
+  ubuf[1] = 0;
+#if EGG_IS_MIN_VER(10500)
+  len = nick_len;
+#else
+  len = NICKLEN;
+#endif
+  chname = newsplit(&par);
+  putlog(LOG_CMDS, "*", "#%s# (%s) schannel %s", dcc[idx].nick,
+	 dcc[idx].u.chat->con_chan, chname);
+  if (!chname[0])
+    chname = dcc[idx].u.chat->con_chan;
+  chan = findschan(chname);
+  if (!chan) {
+    if (!findchan_by_dname(chname))
+      dprintf(idx, "Invalid channel: %s\n", chname);
+    else
+      dprintf(idx, "Channel %s is inactive\n", chname);
+    return 0;
+  }
+  dprintf(idx, "%d users on channel:\n", countmembers(chan->members));
+  sprintf(spaces, "                                                ");
+  spaces[len - 4] = 0;
+  dprintf(idx, "NICK%s", spaces);
+  spaces[len - 4] = ' ';
+  spaces[HANDLEN - 4] = 0;
+  dprintf(idx, "  USER%s", spaces);
+  dprintf(idx, "  UHOST\n");
+  spaces[HANDLEN - 4] = ' ';
+  for (m = chan->members; m; m = m->next) {
+    if (m->user)
+      user = m->user->user;
+    else
+      user = ubuf;
+    spaces[len - strlen(m->nick)] = 0;
+    dprintf(idx, "%s%s", m->nick, spaces);
+    spaces[len - strlen(m->nick)] = ' ';
+    spaces[HANDLEN - strlen(user)] = 0;
+    dprintf(idx, "  %s%s", user, spaces);
+    spaces[HANDLEN - strlen(user)] = ' ';
+    dprintf(idx, "  %s\n", m->uhost);
+  }
+  Context;
+  return 0;
+}
+
+static int cmd_swhois(struct userrec *user, int idx, char *par)
+{
+  struct stats_userlist *u;
+
+  Context;
+  if (!par[0]) {
+    dprintf(idx, "Usage: .swhois <stats-user>\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# swhois %s", dcc[idx].nick, par);
+  u = findsuser_by_name(par);
+  if (!u) {
+    dprintf(idx, "No such user %s.\n", par);
+    return 0;
+  }
+  dump_suser(idx, u);
+  return 0;
+}
+
+static void dump_suser(int idx, struct stats_userlist *u)
+{
+  struct stats_hostlist *h;
+  struct userrec *uu;
+
+  dprintf(idx, "------ %s ------\n", u->user);
+  dprintf(idx, "flags: %clist %caddhosts\n", u->list ? '+' : '-', u->addhosts ? '+' : '-');
+  if (u->hosts) {
+    dprintf(idx, "Hosts: %s\n", u->hosts->mask);
+    for (h = u->hosts->next; h; h = h->next)
+      dprintf(idx, "       %s\n", h->mask);
+  }
+  uu = get_user_by_handle(userlist, u->user);
+  if (uu)
+    dprintf(idx, "This user also exists in the eggdrop-userfile. There might be additional hosts.\n");
+}
+
+static int cmd_pls_shost(struct userrec *user, int idx, char *par)
+{
+  char *suser, *host;
+  struct stats_userlist *u;
+
+  Context;
+  suser = newsplit(&par);
+  host = newsplit(&par);
+  if (!suser[0] || !host[0]) {
+    dprintf(idx, "Usage: .+shost <statuser> <host>\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# +shost %s %s", dcc[idx].nick, suser, host);
+  u = findsuser_by_name(suser);
+  if (!u) {
+    dprintf(idx, "No such user: %s\n", suser);
+    return 0;
+  }
+  saddhost(u, host, now);
+  dprintf(idx, "Added hostmask %s to %s.\n", host, u->user);
+  update_schannel_members();
+  return 0;
+}
+
+static int cmd_mns_shost(struct userrec *user, int idx, char *par)
+{
+  char *suser, *host;
+  struct stats_userlist *u;
+
+  Context;
+  suser = newsplit(&par);
+  host = newsplit(&par);
+  if (!suser[0] || !host[0]) {
+    dprintf(idx, "Usage: .-shost <statuser> <host>\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# -shost %s %s", dcc[idx].nick, suser, host);
+  u = findsuser_by_name(suser);
+  if (!u) {
+    dprintf(idx, "No such user: %s\n", suser);
+    return 0;
+  }
+  if (sdelhost(u, host)) {
+    dprintf(idx, "Removed hostmask %s from %s.\n", host, u->user);
+    update_schannel_members();
+  } else
+    dprintf(idx, "Couldn't remove hostmask %s from %s: no such mask!\n", host, u->user);
+  update_schannel_members();
+  return 0;
+}
+
+static int cmd_pls_suser(struct userrec *user, int idx, char *par)
+{
+  struct stats_userlist *u;
+  char *suser, *host;
+
+  Context;
+  suser = newsplit(&par);
+  host = newsplit(&par);
+  if (!suser[0]) {
+    dprintf(idx, "Usage: .+suser <statuser> [host]\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# +suser %s %s", dcc[idx].nick, suser, host);
+  u = findsuser_by_name(suser);
+  if (u) {
+    dprintf(idx, "That statuser already exists!\n");
+    return 0;
+  }
+  u = addsuser(suser, 1, 1);
+  if (host[0]) {
+    saddhost(u, host, now);
+    putlog(LOG_MISC, "Added stats user %s with hostmask %s.", u->user, host);
+  } else
+    putlog(LOG_MISC, "Added stats user %s without a hostmask.", u->user);
+  update_schannel_members();
+  return 0;
+}
+
+static int cmd_mns_suser(struct userrec *user, int idx, char *par)
+{
+  struct stats_userlist *u;
+  char *suser;
+
+  Context;
+  suser = newsplit(&par);
+  if (!suser[0]) {
+    dprintf(idx, "Usage: .-suser <statuser>\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# -suser %s", dcc[idx].nick, suser);
+  u = findsuser_by_name(suser);
+  if (!u) {
+    dprintf(idx, "No such user.\n");
+    return 0;
+  }
+  delsuser(suser);
+  putlog(LOG_MISC, "Deleted stats user %s.", u->user);
+  update_schannel_members();
+  return 0;
+}
+
+static int cmd_schattr(struct userrec *user, int idx, char *par)
+{
+  struct stats_userlist *u;
+  char *suser, *mode1, *mode2;
+
+  Context;
+  suser = newsplit(&par);
+  mode1 = newsplit(&par);
+  mode2 = newsplit(&par);
+  if (!suser[0] || !mode1[0]) {
+    dprintf(idx, "Usage: .schattr <statuser> <+/-list +/-addhosts>\n");
+    return 0;
+  }
+  putlog(LOG_CMDS, "*", "#%s# schattr %s %s %s", dcc[idx].nick, suser, mode1, mode2);
+  u = findsuser_by_name(suser);
+  if (!u) {
+    dprintf(idx, "No such user.\n");
+    return 0;
+  }
+  if (!strcasecmp(mode1, "+list"))
+    u->list = 1;
+  else if (!strcasecmp(mode1, "-list"))
+    u->list = 0;
+  if (!strcasecmp(mode1, "+addhosts"))
+    u->addhosts = 1;
+  else if (!strcasecmp(mode1, "-addhosts"))
+    u->addhosts = 0;
+  dprintf(idx, "New settings for %s: %clist %caddhosts\n", u->user,
+          u->list ? '+' : '-', u->addhosts ? '+' : '-');
+  return 0;
+}
+
+static int cmd_updateschans(struct userrec *user, int idx, char *par)
+{
+  Context;
+  putlog(LOG_CMDS, "*", "#%s# updateschans", dcc[idx].nick);
+  update_schannel_members();
+  return 0;
+}
+
+static int cmd_smatch(struct userrec *user, int idx, char *par)
+{
+  char *tmp, *mask;
+  int list, addhosts, match, matches, deb;
+  struct stats_userlist *u;
+  struct stats_hostlist *h;
+
+  Context;
+  mask = NULL;
+  list = addhosts = -1;
+  putlog(LOG_CMDS, "*", "#%s# smatch %s", dcc[idx].nick, par);
+  while (par[0]) {
+    tmp = newsplit(&par);
+    if ((tmp[0] == '+') || (tmp[0] == '-')) {
+      if (!strcasecmp(tmp + 1, "addhosts"))
+        addhosts = (tmp[0] == '+');
+      else if (!strcasecmp(tmp + 1, "list"))
+        list = (tmp[0] == '+');
+      else {
+        dprintf(idx, "Unknown stats-user flag: %s\n", tmp);
+        return 0;
+      }
+    } else {
+      if (mask) {
+	dprintf(idx, "Usage: .smatch [hostmask|nickmask] [+|-addhosts] [+|-list]\n");
+	return 0;
+      }
+      mask = tmp;
+    }
+  }
+  if (!mask && (list == -1) && (addhosts == -1)) {
+    dprintf(idx, "Usage: .smatch [hostmask|nickmask] [+|-addhosts] [+|-list]\n");
+    return 0;
+  }
+  debug3("mask: %s, list: %d, addhosts: %d", mask, list, addhosts);
+  matches = deb =  0;
+  for (u = suserlist; u; u = u->next) {
+    deb++;
+    match = 0;
+    if ((list != (-1)) && (u->list != list))
+      continue;
+    if ((addhosts != (-1)) && (u->addhosts != addhosts))
+      continue;
+    if (mask) {
+      if (wild_match(mask, u->user)) {
+        match = 1;
+      } else {
+	for (h = u->hosts; h && !match; h = h->next) {
+	  if (wild_match(mask, h->mask)) {
+	    match = 1;
+	  }
+	}
+      }
+    } else
+      match = 1;
+    if (match) {
+      matches++;
+      if (matches > 20) {
+        dprintf(idx, "More than 20 matches. Truncated.\n");
+        break;
+      } else
+        dump_suser(idx, u);
+    }
+  }
+  if (!matches)
+    dprintf(idx, "No matches!\n");
+  else if (matches <= 20)
+    dprintf(idx, "===\n%d matches found.\n", matches);
+  debug2("%d matches, %d cycles", matches, deb);
+  return 0;
+}
+
+static int cmd_chsusername(struct userrec *user, int idx, char *par)
+{
+  char *oldnick, *newnick;
+  struct stats_userlist *u;
+
+  Context;
+  putlog(LOG_CMDS, "*", "#%s# chsusername %s", dcc[idx].nick, par);
+  oldnick = newsplit(&par);
+  newnick = newsplit(&par);
+  if (!newnick[0] || par[0]) {
+    dprintf(idx, "Usage: .chsusername old_user_name new_user_name\n");
+    return 0;
+  }
+  u = findsuser_by_name(oldnick);
+  if (!u) {
+    dprintf(idx, "No such user: %s\n", oldnick);
+    return 0;
+  }
+  u = findsuser_by_name(newnick);
+  if (u && strcasecmp(oldnick, newnick)) {
+    dprintf(idx, "User %s already exists!\n", newnick);
+    return 0;
+  }
+  if (get_user_by_handle(userlist, oldnick)) {
+    dprintf(idx, "%s exists in the eggdrop userfile. Please use .chnick or "
+            ".chhand instead to keep the stats synched.\n");
+    return 0;
+  }
+  track_stat_user(oldnick, newnick);
+  update_schannel_members();
+  return 0;
+}
+
+static cmd_t mydcc[] =
+{
+  {"savestats", "m", cmd_savestats, NULL},
+  {"loadstats", "m", cmd_loadstats, NULL},
+  {"writewebstats", "m|-", cmd_writewebstats, NULL},
+  {"purgestats", "m|-", cmd_purgestats, NULL},
+  {"sumuser", "n|-", cmd_sumuser, NULL},
+  {"resetuser", "m|-", cmd_resetuser, NULL},
+  {"schannel", "o|o", cmd_schannel, NULL},
+  {"swhois", "o|o", cmd_swhois, NULL},
+  {"+shost", "m|-", cmd_pls_shost, NULL},
+  {"-shost", "m|-", cmd_mns_shost, NULL},
+  {"+suser", "m|-", cmd_pls_suser, NULL},
+  {"-suser", "m|-", cmd_mns_suser, NULL},
+  {"schattr", "m|-", cmd_schattr, NULL},
+  {"updateschans", "-|-", cmd_updateschans, NULL},
+  {"smatch", "m", cmd_smatch, NULL},
+  {"chsusername", "m", cmd_chsusername, NULL},
+  {0, 0, 0, 0}
+};

+ 147 - 0
help/set/stats.help

@@ -0,0 +1,147 @@
+%{help=stats settings}%{+n}
+###  Settings for the %bstats module%b
+  save-stats webupdate topnr graphnr autoadd expire-users
+  quote-frequency log-wordstats min-word-length
+  use-eggdrop-userfile table-border display-urls kick-context
+  display-kicks display-average-users show-userlist
+  show-usersonchan list-secret-chans statsfile topstats
+  webdir smilies graphstats graphgif graphcolor livestats-loglevel
+  livestats-log stat-reply livestats-ignore-msg livestats-ip
+  max-stat-cmds max-livestats-access
+%{help=set save-stats}
+%{+n}
+###  %bset save-stats%b <#>
+  save data every # minutes
+%{help=set webupdate}
+%{+n}
+###  %bset webupdate%b <#>
+  update webfiles every # minutes
+  (outdated, use livestats instead!)
+%{help=set topnr}
+%{+n}
+###  %bset topnr%b <#>
+  list top # users in the toptalker-tables
+%{help=set graphnr}
+%{+n}
+###  %bset graphnr%b <#>
+  list top # users in the graphical stats
+%{help=set autoadd}
+%{+n}
+###  %bset autoadd%b <#>
+  autoadd users to the userbase if they stay on the channel
+  for more than # minutes
+%{help=set expire-users}
+%{+n}
+###  %bset expire-users%b <#>
+  delete users/hosts if they haven't been seen for more than
+  x days
+%{help=set quote-frequency}
+%{+n}
+###  %bset quote-frequency%b <#>
+  store every #th sentence of a user as a quote
+%{help=set log-wordstats}
+%{+n}
+###  %bset log-wordstats%b <1/0>
+  log wordstats? (uses much memory and cpu)
+%{help=set min-word-length}
+%{+n}
+###  %bset min-word-length%b <#>
+  minimum length of a word to get logged in the wordstats
+%{help=set use-eggdrop-userfile}
+%{+n}
+###  %bset use-eggdrop-userfile%b <0/1>
+  add new users to the eggdrop userfile instead of the internal stats
+  database? (hint: _bad_ idea)
+%{help=set table-border}
+%{+n}
+###  %bset table-border%b <#>
+  defines the table border (anything else than 0 should look... ermm.. weird)
+%{help=set display-urls}
+%{+n}
+###  %bset display-urls%b <#>
+  defines how many random urls should be displayed on the miscstats page
+%{help=set kick-context}
+%{+n}
+###  %bset kick-context%b <#>
+  defines how many previous lines should be displayed with a kick on the miscstats page
+%{help=set display-kicks}
+%{+n}
+###  %bset display-kicks%b <#>
+  defines how many random kicks should be displayed on the livestats page
+%{help=set display-average-users}
+%{+n}
+###  %bset display-average-users%b <1/0>
+  show how many users have been on the chan in average?
+%{help=set show-userlist}
+%{+n}
+###  %bset show-userlist%b <1/0>
+  display a complete list of all users?
+%{help=set show-usersonchan}
+%{+n}
+###  %bset show-usersonchan%b <1/0>
+  display who's currently on the chan?
+%{help=set list-secret-chans}
+%{+n}
+###  %bset list-secret-chans%b <1/0>
+  list secret (.chanset +secret) chans on the index page?
+%{help=set statsfile}
+%{+n}
+###  %bset statsfile%b <filename>
+  file where all stat- and user-data will be stored
+%{help=set topstats}
+%{+n}
+###  %bset topstats%b <list of stat-types>
+  defines which stat-types you want to be displayed in the topX tables and on the user-page
+%{help=set webdir}
+%{+n}
+###  %bset webdir%b <directory>
+  the directory to which static webfiles (*cough*!) are written
+%{help=set smileys}
+%{+n}
+###  %bset smilies%b <smiley-list>
+  a list of smilies that should be counted
+%{help=set graphstats}
+%{+n}
+###  %bset graphstats%b <list of stat-types>
+  defines which stats should be listed on the graphical stat-pages
+%{help=set graphgif}
+%{+n}
+###  %bset graphgif%b <full URL>
+  optional background image for the graphs to make them look more "3D"
+  NOTE: this must be a full URL. The image must be placed somewhere
+  	on your webpage since stats.mod can't transfer binary images.
+%{help=set graphcolor}
+%{+n}
+###  %bset graphcolor%b <color>
+  defines the color of the graphs
+%{help=set livestats-loglevel}
+%{+n}
+###  %bset livestats-loglevel%b <loglevel>
+  defines where access to the livestats should be logged
+  (use .console +<loglevel> to watch the chosen loglevel)
+%{help=set livestats-log}
+%{+n}
+###  %bset livestats-log%b <logfile>
+  defines the file to which livestats access should be logged (for use with a
+  loganalyzer, for example)
+%{help=set stat-reply}
+%{+n}
+###  %bset stat-reply%b <list of stat-types>
+  defines which stat-types should be displayed if someone uses the command !stat
+%{help=set livestats-ignore-msg}
+%{+n}
+###  %bset livestats-ignore-msg%b <msg>
+  this message is sent to users who tried to access livestats, but are ignored
+%{help=set livestats-ip}
+%{+n}
+###  %bset livestats-ip%b <IP>
+  defines the IP on which livestats will be reachable
+  set this to "" to use all IPs
+%{help=set max-stat-cmds}
+%{+n}
+###  %bset max-stat-cmds%b <n:s>
+  defines the maximum number of commands (n) which will be answered in s seconds
+%{help=set max-livestats-access}
+%{+n}
+###  %bset max-livestats-access%b <n:s>
+  defines the maximum number of livestats requests (n) which will be processed in s seconds

+ 95 - 0
help/stats.help

@@ -0,0 +1,95 @@
+%{help=savestats}%{+m}
+###  %bsavestats%b
+  Saves all data.
+%{help=resetuser}%{+m|-}
+###  %bresetuser%b <user> <chan>
+  resets all statistics of <user> in <chan> to 0.
+%{help=schannel}%{+o|+o}
+###  %bschannel%b [channel]
+  displays a list of users on the channel, listing their nicks and
+  corresponding stats-usernames.
+%{help=swhois}%{+o|+o}
+###  %bswhois%b <user>
+  displays information about <user> (stat-flags, hosts, etc...)
+  flags:
+    +/-list: list a user in the top10/20/etc?
+    +/-addhosts: automatically add new hosts to user?
+%{help=+shost}%{+m|-}
+###  %b+shost%b <user> <host>
+  adds a host to <user>
+%{help=-shost}%{+m|-}
+###  %b-shost%b <user> <host>
+  removes a host from <user>
+%{help=+suser}%{+m|-}
+###  %b+suser%b <user> [host]
+  creates a new stat-user record
+%{help=-suser}%{+m|-}
+###  %b-suser%b <user>
+  deletes a stat-user
+%{help=schattr}%{+m|-}
+###  %bschattr%b <user> <flags>
+  changes the flags of a stat-user
+%{help=smatch}%{+m}
+###  %bsmatch%b [user] [host] [flags]
+  displays all matching users
+  wildcards allowed.
+%{help=chsusername}%{+m}
+###  %bchsusername%b <user> <newname>
+  changes the name of an user
+%{help=writewebstats}%{+m|-}
+###  %bwritewebstats%b <channels>
+   writes the static (*cough*!) webfiles for the given channels.
+%{help=purgestats}%{+m|-}
+###  %bpurgestats%b
+   purges all outdates statistics and deletes old users
+%{help=sumuser}%{+n|-}
+###  %bsumuser%b <user1> <user2>
+   transfers all stats from user2 to user1 and deletes user2
+%{help=stats module}%{+m}
+###  help on the %bstats module%b
+   Commands available: (use %b'.help <command>'%b for more info)
+%{+o|o}
+   for ops:
+      %bschannel%b
+      %bswhois%b
+%{+m|m}
+   for masters:
+      %bsavestats%b
+      %bresetuser%b
+      %b+shost%b
+      %b-shost%b
+      %b+suser%b
+      %b-suser%b
+      %bschattr%b
+      %bsmatch%b
+      %bchsusername%b
+      %bwritewebstats%b
+      %bpurgestats%b
+      %bsumuser%b
+%{+n}
+   There is also a list of tcl settings avalible,
+   use %b'.help stats settings'%b   
+%{help=all}%{+m}
+###  commands for the %bstats module%b
+   Commands available: (use %b'.help <command>'%b for more info)
+%{+o|o}
+   for ops:
+      %bschannel%b
+      %bswhois%b
+%{+m|m}
+   for masters:
+      %bsavestats%b
+      %bresetuser%b
+      %b+shost%b
+      %b-shost%b
+      %b+suser%b
+      %b-suser%b
+      %bschattr%b
+      %bsmatch%b
+      %bchsusername%b
+      %bwritewebstats%b
+      %bpurgestats%b
+      %bsumuser%b
+%{+n}
+   There is also a list of tcl settings avalible,
+   use %b'.help stats settings'%b

+ 1558 - 0
livestats.c

@@ -0,0 +1,1558 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static struct dcc_table LIVESTATS_LISTEN =
+{
+  "LIVESTATS_LISTEN",
+  DCT_VALIDIDX,
+  eof_livestats,
+  livestats_accept,
+  0,
+  timeout_listen_livestats,
+  display_livestats_accept,
+  0,
+  NULL,
+  0
+};
+
+static struct dcc_table LIVESTATS =
+{
+  "LIVESTATS",
+  DCT_VALIDIDX,
+  eof_livestats,
+  livestats_activity,
+  &livestats_timeout,
+  timeout_livestats,
+  display_livestats,
+  expmem_livestats,
+  kill_livestats,
+#ifdef OLDBOT
+  out_livestats,
+#else
+  out_livestats,
+  outdone_livestats
+#endif
+};
+
+static int inactivechan(char *chan)
+{
+  struct chanset_t *ch;
+
+  ch = findchan_by_dname(chan);
+  if (!ch)
+    return 1;
+  if (ch->status & CHAN_INACTIVE)
+    return 1;
+  return 0;
+}
+
+static int nostats(char *chan)
+{
+#if EGG_IS_MIN_VER(10503)
+  if (ngetudef("nostats", chan))
+    return 1;
+#endif
+  return 0;
+}
+
+static void start_listen_livestats(int port)
+{
+  int i, zz;
+  char tmp[50];
+
+  stop_listen_livestats();
+  sprintf(tmp, "set my-ip \"%s\";", livestats_ip);
+  do_tcl("livestats-hack-start",
+      "set my-ip-livestats-backup ${my-ip};"
+      "set my-hostname-livestats-backup ${my-hostname};"
+      "set my-hostname \"\"");
+  do_tcl("livestats-hack-setip", tmp);
+  zz = open_listen(&port);
+  do_tcl("livestats-hack-end",
+      "set my-ip ${my-ip-livestats-backup};"
+      "set my-hostname ${my-hostname-livestats-backup}");
+  if (zz == (-1)) {
+    putlog(LOG_MISC, "*", "ERROR! Cannot open listening socket for livestats!");
+    return;
+  }
+  if ((i = new_dcc(&LIVESTATS_LISTEN, 0)) == -1) {
+    putlog(LOG_MISC, "*", "ERROR! Cannot open listening socket for livestats! DCC table is full!");
+    return;
+  }
+  dcc[i].sock = zz;
+  dcc[i].addr = (IP) (-559026163);
+  dcc[i].port = port;
+  strcpy(dcc[i].nick, "livestats");
+  strcpy(dcc[i].host, "*");
+  dcc[i].timeval = now;
+  putlog(LOG_MISC, "*", "Now listening for livestats connections on port %d", port);
+}
+
+static void stop_listen_livestats()
+{
+  int i;
+
+  for (i = 0; i < dcc_total; i++) {
+    if (dcc[i].type == &LIVESTATS_LISTEN) {
+      putlog(LOG_MISC, "*",
+      	     "no longer listening for livestats connections on port %d",
+             dcc[i].port);
+      killsock(dcc[i].sock);
+      lostdcc(i);
+    } else if (dcc[i].type == &LIVESTATS) {
+      putlog(LOG_MISC, "*", "killing livestats connection from %s", dcc[i].host);
+      killsock(dcc[i].sock);
+      lostdcc(i);
+    }
+  }
+}
+
+static void livestats_activity(int idx, char *buf, int len)
+{
+  char *cmd, *path, *newpath, *imask;
+  int lev;
+
+  imask = nmalloc(strlen(dcc[idx].host) + 13);
+  sprintf(imask, "livestats!*@%s", dcc[idx].host);
+  if (match_ignore(imask)) {
+    debug1("Ignoring livestats access from %s", dcc[idx].host);
+    stats_info_access(idx)->code = 401;
+    if (livestats_ignore_msg[0])
+    dprintf(idx, "HTTP/1.0 401 Access Forbidden\nContent-Type: text/html\n\n%s", livestats_ignore_msg);
+    killsock(dcc[idx].sock);
+    lostdcc(idx);
+    nfree(imask);
+    return;
+  }
+  nfree(imask);
+  if (!strncasecmp(buf, "GET ", 4)) {
+    if (!stats_info_access(idx)->cmd) {
+      stats_info_access(idx)->cmd = nmalloc(strlen(buf) + 1);
+      strcpy(stats_info_access(idx)->cmd, buf);
+      stats_info_access(idx)->code = 200;
+    }
+  }
+  cmd = newsplit(&buf);
+  if (!strcmp(cmd, "GET")) {
+    if (livestats_flood()) {
+      stats_info_access(idx)->code = 401;
+      killsock(dcc[idx].sock);
+      lostdcc(idx);
+      return;
+    }
+    lev = logmodes(stats_loglevel);
+    if (lev)
+      putlog(lev, "*", "%s: GET %s", dcc[idx].host, buf);
+    path = newsplit(&buf);
+    if (!strncasecmp(path, "/robots.txt", 11)) {
+      dprintf(idx, "HTTP/1.1 404 Not Found\nServer: stats.mod/%s\nConnection: close", MODULE_VERSION);
+      dprintf(idx, "Content-Type: text/html; charset=iso-8859-1\n\n");
+      dprintf(idx, "<HTML><HEAD><TITLE>404 Not Found</TITLE></HEAD><BODY><H1>Not Found</H1>");
+      dprintf(idx, "The requested document was not found on this server.</BODY></HTML>");
+      stats_info_access(idx)->code = 404;
+      return;
+    }
+    if (path[strlen(path) - 1] != '/') {
+      newpath = nmalloc(strlen(path) + 2);
+      strcpy(newpath, path);
+      newpath[strlen(path)] = '/';
+      newpath[strlen(path) + 1] = 0;
+      dprintf(idx, "HTTP/1.1 301 Moved Permanently\nServer: stats.mod/%s\n", MODULE_VERSION);
+      dprintf(idx, "Location: %s\nConnection: close\nContent-Type: text/html\n\n", newpath);
+      dprintf(idx, "<HTML><body>The concluding \"/\" is important!<br><center>");
+      dprintf(idx, "<a href=\"%s\">%s</a></center><br>", newpath, newpath);
+      stats_info_access(idx)->code = 301;
+      nfree(newpath);
+      return;
+    }
+    dprintf(idx, "HTTP/1.0 200 OK\nServer: stats.mod/%s\nContent-Type: text/html\n\n", MODULE_VERSION);
+    send_livestats(idx, path);
+  } else if (!strcasecmp(cmd, "User-Agent:")) {
+    if (stats_info_access(idx)->browser)
+      return;
+    stats_info_access(idx)->browser = nmalloc(strlen(buf) + 1);
+    strcpy(stats_info_access(idx)->browser, buf);
+  } else if (!strcasecmp(cmd, "Referer:")) {
+    if (stats_info_access(idx)->referer)
+      return;
+    stats_info_access(idx)->referer = nmalloc(strlen(buf) + 1);
+    strcpy(stats_info_access(idx)->referer, buf);
+  } else if (!buf[0]) {
+    dcc[idx].status = 1;
+#ifndef OLDBOT
+    /* If there's no data in our socket, we immediately get rid of it.
+     */
+    if (!sock_has_data(SOCK_DATA_OUTGOING, dcc[idx].sock)) {
+      killsock(dcc[idx].sock);
+      lostdcc(idx);
+    }
+#endif
+  }
+}
+
+#ifndef OLDBOT
+static void outdone_livestats(int idx)
+{
+  if (dcc[idx].status) {
+    killsock(dcc[idx].sock);
+    lostdcc(idx);
+  } else
+    dcc[idx].status = 1;
+}
+#endif
+
+static void display_livestats(int idx, char *buf)
+{
+  sprintf(buf, "livestats");
+}
+
+static void display_livestats_accept(int idx, char *buf)
+{
+  sprintf(buf, "lstn port");
+}
+
+static void timeout_livestats(int idx)
+{
+  killsock(dcc[idx].sock);
+  lostdcc(idx);
+}
+
+static void timeout_listen_livestats(int idx)
+{
+  debug0("timeout listen");
+  killsock(dcc[idx].sock);
+  lostdcc(idx);
+}
+
+static void kill_livestats(int idx, void *x)
+{
+  register struct stats_clientinfo *p = (struct stats_clientinfo *) x;
+  char ts[41], test[11];
+  time_t tt;
+  FILE *f;
+
+  Context;
+  tt = now;
+  if (!p) {
+    putlog(LOG_MISC, "*", "Can't kill clientinfo, no pointer. This should not happen!");
+    return;
+  }
+  ctime(&tt);
+  /* 05/Feb/2000:12:08:17 +0100 */
+  strftime(test, 19, "%z", localtime(&tt));
+  if (test[0] != 'z')
+    strftime(ts, 40, "%d/%b/%Y:%H:%M:%S %z", localtime(&tt));
+  else
+    strftime(ts, 40, "%d/%b/%Y:%H:%M:%S", localtime(&tt));
+  if (livestats_log[0]) {
+    f = fopen(livestats_log, "a");
+    if (f == NULL)
+      putlog(LOG_MISC, "*", "ERROR writing livestats log.");
+    else {
+      if (test[0] != 'z')
+        fprintf(f,
+    	 "%s - - [%s] \"%s\" %d %d \"%s\" \"%s\"\n", dcc[idx].host, ts,
+	 p->cmd ? p->cmd : "", p->code, p->traffic,
+	 p->referer ? p->referer : "-", p->browser ?  p->browser : "");
+      else
+        fprintf(f,
+    	 "%s - - [%s %+05d] \"%s\" %d %d \"%s\" \"%s\"\n", dcc[idx].host,
+	 ts, offset * (-1) * 100, p->cmd   ? p->cmd : "", p->code ,p->traffic,
+	 p->referer ? p->referer : "-", p->browser ?  p->browser : "");
+      fclose(f);
+    }
+  }
+  if (p->browser)
+    nfree(p->browser);
+  if (p->referer)
+    nfree(p->referer);
+  if (p->cmd)
+    nfree(p->cmd);
+  nfree(p);
+}
+
+static int expmem_livestats(void *x)
+{
+  register struct stats_clientinfo *p = (struct stats_clientinfo *) x;
+  int tot = sizeof(struct stats_clientinfo);
+
+  Context;
+  if (!p) {
+    putlog(LOG_MISC, "*", "Can't expmem clientinfo, no pointer. This should not happen!");
+    return 0;
+  }
+  if (p->browser)
+    tot += strlen(p->browser) + 1;
+  if (p->referer)
+    tot += strlen(p->referer) + 1;
+  if (p->cmd)
+    tot += strlen(p->cmd) + 1;
+  return tot;
+}
+
+static void out_livestats(int idx, char *buf, void *x)
+{
+  register struct stats_clientinfo *p = (struct stats_clientinfo *) x;
+
+  if (!p) {
+    putlog(LOG_MISC, "*", "No stats_clientinfo pointer. This should not happen!");
+    return;
+  }
+  p->traffic += strlen(buf);
+  tputs(dcc[idx].sock, buf, strlen(buf));
+}
+
+static void livestats_accept(int idx, char *buf, int len)
+{
+  unsigned long ip;
+  unsigned short port;
+  int j = 0, sock, i;
+  char s[UHOSTLEN];
+
+  Context;
+  if (dcc_total + 1 >= max_dcc) {
+    j = answer(dcc[idx].sock, s, &ip, &port, 0);
+    if (j != -1) {
+      dprintf(-j, "Sorry, too many connections already.\r\n");
+      killsock(j);
+    }
+    return;
+  }
+  sock = answer(dcc[idx].sock, s, &ip, &port, 0);
+  if (sock < 0) {
+    neterror(s);
+    putlog(LOG_MISC, "*", "Stats.mod: Error accepting livestats connection: %s", s);
+    return;
+  }
+  if ((i = new_dcc(&LIVESTATS, sizeof(struct stats_clientinfo))) == (-1)) {
+    putlog(LOG_MISC, "*", "Error accepting livestats connection. DCC table is full.");
+    killsock(sock);
+    return;
+  }
+  dcc[i].sock = sock;
+  dcc[i].addr = ip;
+  dcc[i].port = port;
+  strcpy(dcc[i].nick, "httpstats");
+#ifndef OLDBOT
+  sprintf(s, "%s", iptostr(my_htonl(ip)));
+#else
+  sprintf(s, "%lu.%lu.%lu.%lu", (ip >> 24) & 0xff, (ip >> 16) & 0xff,
+  	  (ip >> 8) & 0xff, ip & 0xff); /* dw */
+#endif
+  strcpy(dcc[i].host, s);
+  dcc[i].timeval = now;
+  dcc[i].status = 0;
+  ((struct stats_clientinfo *) dcc[i].u.other)->traffic = 0;
+  ((struct stats_clientinfo *) dcc[i].u.other)->code = 200;
+  ((struct stats_clientinfo *) dcc[i].u.other)->browser = NULL;
+  ((struct stats_clientinfo *) dcc[i].u.other)->referer = NULL;
+  ((struct stats_clientinfo *) dcc[i].u.other)->cmd = NULL;
+}
+
+static int mlstat_time = 0, mlstat_thr = 0;
+static int livestats_flood()
+{
+  if (!maxlivestats_thr || !maxlivestats_time)
+    return 0;
+  if ((now - mlstat_time) > maxlivestats_time) {
+    mlstat_time = now;
+    mlstat_thr = 0;
+  }
+  mlstat_thr++;
+  if (mlstat_thr > maxlivestats_thr)
+    return 1;
+  return 0;
+}
+
+static void eof_livestats(int idx)
+{
+  debug0("eof accept");
+  killsock(dcc[idx].sock);
+  lostdcc(idx);
+}
+
+static void send_livestats(int idx, char *buf)
+{
+  char *channel, *command;
+  char what[512], *pwhat, *type, *stoday, *user, prefix[4];
+  struct chanset_t *chan;
+  int today = 0;
+  globstats *gs;
+  locstats *ls, *ls2;
+  time_t tt, ttbuf;
+  struct userrec *u;
+  int itype, nr, i, ii;
+  wordstats *ws;
+  quotestr *qs;
+  unsigned long x;
+  int wert;
+  float r, g, b;
+  float r2, g2, b2;
+  float rstep, gstep, bstep;
+  struct stats_userlist *suser;
+
+  ttbuf = now;
+  tt = now;
+  if (!strcmp(buf, "/")) {
+    setslglobs(NULL, 0, 0, 0);
+    dprintf(idx, "<html><head><title>%s</title>\n%s\n</head>\n", ROOTTITLE, SLCSS);
+    dprintf(idx, "%s\n", SLBODYTAG);
+    long_dprintf(idx, SLHEADER);
+    dprintf(idx, "<br><br><br><table border=1 width=100%%>\n");
+    for (gs = sdata; gs; gs = gs->next) {
+      if (!inactivechan(gs->chan) && !secretchan(gs->chan) && !nostats(gs->chan)) {
+	if (gs->chan[0] == '!')
+	  strcpy(prefix, "!");
+	else if (gs->chan[0] == '+')
+	  strcpy(prefix, "+");
+	else if (gs->chan[0] == '&')
+	  strcpy(prefix, "&");
+	else
+	  prefix[0] = 0;
+#ifndef OLDBOT
+        chan = findchan_by_dname(gs->chan);
+#else
+        chan = findchan(gs->chan);
+#endif
+        i = 7;
+        if (!show_userlist)
+          i--;
+        if (!show_usersonchan)
+          i--;
+        dprintf(idx, "<tr><td rowspan=%d align=center width=50%%>", i);
+        if (chan && chan->channel.topic)
+          dprintf(idx, "<a href=\"%s%s/\">%s</a><br><font size=-1>(%s)</font></td>",
+                  prefix, gs->chan + 1, gs->chan, filt2(chan->channel.topic));
+        else
+          dprintf(idx, "<a href=\"%s%s/\">%s</a></td>",
+                  prefix, gs->chan + 1, gs->chan);
+        dprintf(idx, "<td rowspan=4 align=center>%s</td>", SLTOP);
+        setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+        dprintf(idx, "<td align=center><a href=\"%s%s/top/total/words/\">%s</a></td>",
+        	prefix, gs->chan + 1, SLTOTAL);
+        dprintf(idx, "</tr>\n<tr>");
+        dprintf(idx, "<td align=center><a href=\"%s%s/top/daily/words/\">%s</a></td>",
+        	prefix, gs->chan + 1, SLDAILY);
+        dprintf(idx, "</tr>\n<tr>");
+        dprintf(idx, "<td align=center><a href=\"%s%s/top/weekly/words/\">%s</a></td>",
+        	prefix, gs->chan + 1, SLWEEKLY);
+        dprintf(idx, "</tr>\n<tr>");
+        dprintf(idx, "<td align=center><a href=\"%s%s/top/monthly/words/\">%s</a></td>",
+        	prefix, gs->chan + 1, SLMONTHLY);
+        dprintf(idx, "</tr>\n");
+        if (show_userlist)
+          dprintf(idx, "<tr><td align=center colspan=2><a href=\"%s%s/users/\">%s</a></td></tr>",
+        	  prefix, gs->chan + 1, SLUSERS);
+        if (show_usersonchan)
+          dprintf(idx, "<tr><td align=center colspan=2><a href=\"%s%s/onchan/\">%s</a></td></tr>",
+                  prefix, gs->chan + 1, SLONCHAN);
+        dprintf(idx, "<tr><td align=center colspan=2><a href=\"%s%s/misc/\">%s</a></td>",
+        	prefix, gs->chan + 1, SLMISCSTATS);
+        dprintf(idx, "</tr><tr><td colspan=3 height=0><font size=-5>&nbsp;</font></td></tr>\n");
+      }
+    }
+    dprintf(idx, "<tr><th colspan=3 align=center>");
+    dprintf(idx, "<a href=\"http://www.visions-of-fantasy.de/stats.mod/\">");
+    dprintf(idx, "Stats.mod v%s</a></th></tr></table>\n", MODULE_VERSION);
+    long_dprintf(idx, SLFOOTER);
+    dprintf(idx, "</body></html>\n");
+    return;
+  }
+  if (buf[0] == '/') {
+    if (!strncasecmp(buf, "/e/", 3)) {
+      buf += 2;
+      buf[0] = '!';
+    } else if (!strncasecmp(buf, "/p/", 3)) {
+      buf += 2;
+      buf[0] = '+';
+    } else if (!strncasecmp(buf, "/a/", 3)) {
+      buf += 2;
+      buf[0] = '&';
+    } else if (buf[1] && strchr("+&!", buf[1]))
+      buf++;
+    else
+      buf[0] = '#';
+  }
+  if (buf[strlen(buf) - 1] == '/')
+    buf[strlen(buf) - 1] = 0;
+  channel = splitpath(&buf);
+#ifndef OLDBOT
+  chan = findchan_by_dname(channel);
+#else
+  chan = findchan(channel);
+#endif
+  if (!chan) {
+    dprintf(idx, "no such channel %s", channel);
+    if (idx >= 0)
+      stats_info_access(idx)->code = 404;
+    return;
+  }
+  gs = findglobstats(channel);
+  if (gs)
+    setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+  else
+    setslglobs(channel, 0, 0, 0);
+  if (!strcmp(buf, "/") || !buf[0]) {
+    dprintf(idx, "<html><head><title>%s</title>\n%s\n</head>%s", SLINDEXTITEL, SLCSS, SLBODYTAG);
+    long_dprintf(idx, SLHEADER);
+    dprintf(idx, "<br><br><br><center>\n");
+    dprintf(idx, "<table border=1 width=50%%>\n");
+    dprintf(idx, "<tr><th colspan=2><font size=+2>%s</font></th></tr>", channel);
+    dprintf(idx, "<tr><td rowspan=4 align=center>%s</td>", SLTOP);
+    dprintf(idx, "<td align=center><a href=\"top/total/words/\">%s</a></td>", SLTOTAL);
+    dprintf(idx, "</tr>\n<tr>");
+    dprintf(idx, "<td align=center><a href=\"top/daily/words/\">%s</a></td>", SLDAILY);
+    dprintf(idx, "</tr>\n<tr>");
+    dprintf(idx, "<td align=center><a href=\"top/weekly/words/\">%s</a></td>", SLWEEKLY);
+    dprintf(idx, "</tr>\n<tr>");
+    dprintf(idx, "<td align=center><a href=\"top/monthly/words/\">%s</a></td>", SLMONTHLY);
+    dprintf(idx, "</tr>\n");
+    if (show_userlist)
+      dprintf(idx, "<tr><td align=center colspan=2><a href=\"users/\">%s</a></td></tr>", SLUSERS);
+    if (show_usersonchan)
+      dprintf(idx, "<tr><td align=center colspan=2><a href=\"onchan/\">%s</a></td></tr>", SLONCHAN);
+    dprintf(idx, "<tr><td align=center colspan=2><a href=\"misc/\">%s</a></td></tr>", SLMISCSTATS);
+    dprintf(idx, "<tr><td colspan=2 height=0><font size=-5>&nbsp;</font></td></tr>\n");
+    dprintf(idx, "<tr><th colspan=2 align=center><font size=-2>");
+    dprintf(idx, "<a href=\"http://www.visions-of-fantasy.de/stats.mod/\">");
+    dprintf(idx, "Stats.mod v%s</a></font></th></tr></table>\n", MODULE_VERSION);
+    dprintf(idx, "</center>\n");
+    long_dprintf(idx, SLFOOTER);
+    dprintf(idx, "</body></html>\n");
+    return;
+  }
+  command = splitpath(&buf);
+  if (!strcasecmp(command, "top")) {
+    if (!strcmp(buf, "/") || !buf[0]) {
+      dprintf(idx, "<html><body><table>");
+      dprintf(idx, "<tr><td><a href=\"total/\">total</a></td></tr>");
+      dprintf(idx, "<tr><td><a href=\"today/\">today</a></td></tr>");
+      dprintf(idx, "</table></body></html>");
+      return;
+    }
+    stoday = splitpath(&buf);
+    if (!strcasecmp(stoday, "today") || !strcasecmp(stoday, "daily"))
+      today = S_DAILY;
+    else if (!strcasecmp(stoday, "weekly"))
+      today = S_WEEKLY;
+    else if (!strcasecmp(stoday, "monthly"))
+      today = S_MONTHLY;
+    else if (!strcasecmp(stoday, "total"))
+      today = 0;
+    else {
+      dprintf(idx, "<html><body>Error, invalid value %s<table>", stoday);
+      dprintf(idx, "<tr><td><a href=\"../total/\">total</a></td></tr>");
+      dprintf(idx, "<tr><td><a href=\"../daily/\">daily</a></td></tr>");
+      dprintf(idx, "<tr><td><a href=\"../weekly/\">weekly</a></td></tr>");
+      dprintf(idx, "<tr><td><a href=\"../monthly/\">monthly</a></td></tr>");
+      dprintf(idx, "</table></body></html>");
+      Assert(idx >= 0);
+      stats_info_access(idx)->code = 404;
+      return;
+    }
+    if (!strcmp(buf, "/") || !buf[0]) {
+      dprintf(idx, "<html><body><table>");
+      sprintf(what, "%s", webstats);
+      pwhat = what;
+      while (strlen(pwhat) > 0) {
+        type = newsplit(&pwhat);
+        itype = typetoi(type);
+        if (itype >= 0) {
+          dprintf(idx, "<tr><td><a href=\"%s/\">%s</a></td></tr>", type, type);
+        }
+      }
+      dprintf(idx, "</table></body></html>");
+      return;
+    }
+    gs = findglobstats(channel);
+    if (!gs) {
+      debug1("Error! Can't find global stats for %s", channel);
+      dprintf(idx, "<html><body>Error! Can't find global stats for %s</body></html>", channel);
+      if (idx >= 0)
+        stats_info_access(idx)->code = 404;
+      return;
+    }
+    if (!strncasecmp(buf, "graph", 5)) {
+      do_graphs(idx, today, gs, channel);
+      return;
+    }
+    do_toptalkers(idx, today, gs, channel, buf);
+    return;
+  } else if (!strcasecmp(command, "onchan")) {
+    display_users_on_chan(idx, channel, chan);
+  } else if (!strcasecmp(command, "users")) {
+    gs = findglobstats(channel);
+    if (!gs) {
+      debug1("Error! Can't find global stats for %s", channel);
+      dprintf(idx, "<html><body>Error! Can't find global stats for %s</body></html>", channel);
+      return;
+    }
+    setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+    if (!strcmp(buf, "/") || !buf[0]) {
+      if (!show_userlist)
+        return;
+      sort_stats_alphabetically(gs);
+      dprintf(idx, "<html><head><title>%s</title>\n%s\n</head>%s",
+              SLUSERSTITLE, SLCSS, SLBODYTAG);
+      long_dprintf(idx, SLHEADER);
+      dprintf(idx, "<center>\n");
+      dprintf(idx, "<table border=%d>\n<tr><th>User</th><th>Info</th></tr>\n", table_border);
+      i = 0;
+      for (ls = gs->local; ls; ls = ls->next)
+        i++;
+      wert = table_color;
+      b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+      wert = fade_table_to;
+      b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+      rstep = (r2 - r) / i;
+      gstep = (g2 - g) / i;
+      bstep = (b2 - b) / i;
+      for (ls = gs->local; ls; ls = ls->next) {
+        u = get_user_by_handle(userlist, ls->user);
+        what[0] = 0;
+        get_handle_chaninfo(ls->user, channel, what);
+        pwhat = what;
+        if (!what[0])
+          pwhat = get_user(&USERENTRY_INFO, u);
+        if (!pwhat) {
+          sprintf(what, "&nbsp;");
+          pwhat = what;
+        }
+        dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td><a href=\"%s/\">%s</a></td><td>%s</td></tr>\n",
+                (int) r, (int) g, (int) b, ls->user, ls->user, filt2(pwhat));
+        r += rstep;
+        g += gstep;
+        b += bstep;
+      }
+/*
+*     dprintf(idx, "<tr><td colspan=2 align=center><a href=\"http://www.visions-of-fantasy.de/stats.mod/\">");
+*     dprintf(idx, "Stats.mod v%s</a></td></tr></table>\n", MODULE_VERSION);
+*/
+      dprintf(idx, "</table>\n");
+      dprintf(idx, "<br><br><hr>\n");
+      dprintf(idx, "<table width=100%% border=0>\n");
+      dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+      dprintf(idx, "<td width=25%% align=center><font size=-1><%sa "
+              "href=\"../onchan/\">%s</a></font></td>\n",
+              ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLONCHAN));
+      dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../top/total/words/\">top%d</a></font></td>\n", webnr);
+      dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../misc/\">%s</a></font></td>\n", SLMISCSTATS);
+      dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../../\">%s</a></font></td>\n", SLOTHERCHANS);
+      dprintf(idx, "</tr></table></td></tr>\n");
+      dprintf(idx, "</table>\n");
+      dprintf(idx, "<center>Created by <a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod v%s</a></center></body></html>", MODULE_VERSION);
+      dprintf(idx, "</center>\n");
+      long_dprintf(idx, SLFOOTER);
+      dprintf(idx, "</body></html>");
+      return;
+    }
+    user = buf;
+    ls = findlocstats(channel, user);
+    if (!ls) {
+      debug2("Error! Can't find local stats for %s in %s", user, channel);
+      dprintf(idx, "<html><body>I don't have any stats for %s in %s, sorry</body></html>", user, channel);
+      if (idx >= 0)
+        stats_info_access(idx)->code = 404;
+      return;
+    }
+    i = countwords(webstats) + 2;
+    suser = findsuser_by_name(user);
+    dprintf(idx, "<html><head><META http-equiv=\"Pragma\" content=\"no-cache\"><META http-equiv=\"Expires\" content=\"now\">\n");
+    slgloblocstats = ls;
+    dprintf(idx, "<title>%s</title>\n%s\n</head>%s\n", SLUSERTITLE, SLCSS, SLBODYTAG);
+    long_dprintf(idx, SLHEADER);
+    dprintf(idx, "<center>\n");
+    dprintf(idx, "%s\n", SLUSERHEAD);
+    if (suser) {
+      dprintf(idx, "<center>");
+      if (suser->email)
+        dprintf(idx, "<a href=\"mailto:%s\" target=\"_new\">%s</a><br>", suser->email, SLEMAIL);
+      if (suser->homepage)
+        dprintf(idx, "<a href=\"%s\" target=\"_new\">%s</a><br>", suser->homepage, SLHOMEPAGE);
+      dprintf(idx, "</center><br>");
+    }
+    dprintf(idx, "<table border=%d><tr><td></td><td>%s</td>", table_border, SLPLACE);
+    sprintf(what, "%s", webstats);
+    pwhat = what;
+    while (strlen(pwhat) > 0) {
+      dprintf(idx, "<td>%s</td>", getslangtype(newsplit(&pwhat)));
+    }
+    dprintf(idx, "</tr>\n");
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / 4;
+    gstep = (g2 - g) / 4;
+    bstep = (b2 - b) / 4;
+    for (today = 0; today < 4; today++) {
+      dprintf(idx, "<tr><td align=center>%s</td>", RANGESTR_LONG);
+      nr = 0;
+      if (ls->values[today][T_WORDS] > 0) {
+        sortstats(gs, T_WORDS, today);
+        for (ls2 = gs->slocal[today][T_WORDS]; ls2; ls2 = ls2->snext[today][T_WORDS]) {
+          nr++;
+          if (!strcasecmp(ls2->user, user))
+            break;
+        }
+      }
+      dprintf(idx, "<td bgcolor=#%02x%02x%02x align=\"right\">\n", (int) r, (int) g, (int) b);
+      if (nr == 0)
+        dprintf(idx, "-</td>");
+      else
+        dprintf(idx, "%d</td>", nr);
+      sprintf(what, "%s", webstats);
+      pwhat = what;
+      while (strlen(pwhat) > 0) {
+        itype = typetoi(newsplit(&pwhat));
+        dprintf(idx, "<td bgcolor=#%02x%02x%02x align=\"right\">\n", (int) r, (int) g, (int) b);
+        if (itype == T_MINUTES)
+          dprintf(idx, "%s</td>", stats_duration(ls->values[today][itype] * 60));
+        else if (itype >= 0)
+          dprintf(idx, "%d</td>", ls->values[today][itype]);
+        else if (itype == T_WPL) {
+	    if (ls->values[today][T_LINES])
+            dprintf(idx, "%.2f</td>", (float) ls->values[today][T_WORDS] / (float) ls->values[today][T_LINES]);
+          else
+            dprintf(idx, "0</td>");
+        } else if (itype == T_IDLE) {
+          if (ls->values[today][T_LINES])
+            dprintf(idx, "%.2f</td>", (float) ls->values[today][T_MINUTES] / (float) ls->values[today][T_LINES]);
+          else
+            dprintf(idx, "0</td>");
+        } else
+          dprintf(idx, "ERR!</td>");
+      }
+      dprintf(idx, "</tr>\n");
+      r += rstep;
+      g += gstep;
+      b += bstep;
+    }
+    if (ls->quotes && quote_freq) {
+      nr = 0;
+      for (qs = ls->quotes; qs; qs = qs->next)
+        nr++;
+      x = random() % nr;
+      ii = 0;
+      qs = ls->quotes;
+      while (qs) {
+        if (ii == x) {
+          dprintf(idx, "<tr><td colspan=%d align=center>", i);
+          dprintf(idx, SLRANDQUOTE, filt2(qs->quote));
+          dprintf(idx, "</td></tr>\n");
+          break;
+        }
+        qs = qs->next;
+        ii++;
+      }
+    }
+    dprintf(idx, "</table></center>\n");
+    if (ls->words) {
+      if (ls->words) {
+        nr = 0;
+        for (ws = ls->words; ws; ws = ws->next)
+          nr++;
+        slglobint = nr;
+        dprintf(idx, "<center><br><br>%s<br>\n", SLUWORDSTATS);
+        sortwordstats(ls, NULL);
+        dprintf(idx, "<table>\n");
+        nr = 0;
+        ws = ls->words;
+        while (ws && (nr < 10)) {
+          nr++;
+          dprintf(idx, "<tr><td>%d.</td><td>%s</td><td>(%d)</td></tr>\n", nr, filt2(ws->word), ws->nr);
+          ws = ws->next;
+        }
+        dprintf(idx, "</table><br><br></center>\n");
+      }
+    }
+    dprintf(idx, "<br><br><hr>\n");
+    dprintf(idx, "<table width=100%% border=0>\n");
+    dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+    dprintf(idx, "<td width=20%% align=center><font size=-1><%sa href=\"../\">%s</a></font></td>\n",
+            ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLOTHERUSERS));
+    dprintf(idx, "<td width=20%% align=center><font size=-1><%sa href=\"../../onchan\">%s</a></font></td>\n",
+            ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLONCHAN));
+    dprintf(idx, "<td width=20%% align=center><font size=-1><a href=\"../../top/total/words/\">%s</a></font></td>\n", SLTOP);
+    dprintf(idx, "<td width=20%% align=center><font size=-1><a href=\"../../misc/\">%s</a></font></td>\n", SLMISCSTATS);
+    dprintf(idx, "<td width=20%% align=center><font size=-1><a href=\"../../../\">%s</a></font></td>\n", SLOTHERCHANS);
+    dprintf(idx, "</tr></table></td></tr>\n");
+    dprintf(idx, "</table>\n");
+    dprintf(idx, "<center>Created by <a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod v%s</a></center></body></html>", MODULE_VERSION);
+    long_dprintf(idx, SLFOOTER);
+    dprintf(idx, "</body></html>");
+    return;
+  } else if (!strcasecmp(command, "misc")) {
+    do_miscstats(idx, channel);
+    return;
+  } else {
+    dprintf(idx, "<html><body>Error! unknown command %s</body></html>", command);
+    if (idx >= 0)
+      stats_info_access(idx)->code = 404;
+    return;
+  }
+}
+
+static void do_graphs(int idx, int today, globstats *gs, char *channel)
+{
+  int itype, nr;
+  int total, rest, width, max;
+  float onep, percent, wpercent;
+  char bground[140];
+  locstats *ls;
+  char what[512], *pwhat, *type;
+
+
+  Context;
+  setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+  if (graphgif[0] == 0)
+    bground[0] = 0;
+  else
+    sprintf(bground, "background=\"%s\"", graphgif);
+  dprintf(idx, "<!-- Created by Stats.mod v%s-->\n", MODULE_VERSION);
+  dprintf(idx, "<html><head><META http-equiv=\"Pragma\" content=\"no-cache\"><META http-equiv=\"Expires\" content=\"now\">\n");
+  if (today == 0)
+    dprintf(idx, "<title>%s</title>\n", SLGRTTITLE);
+  else if (today == 1)
+    dprintf(idx, "<title>%s</title>\n", SLGRDTITLE);
+  else if (today == 2)
+    dprintf(idx, "<title>%s</title>\n", SLGRWTITLE);
+  else if (today == 3)
+    dprintf(idx, "<title>%s</title>\n", SLGRMTITLE);
+  dprintf(idx, "%s\n</head>%s\n", SLCSS, SLBODYTAG);
+  long_dprintf(idx, SLHEADER);
+  if (today == 0)
+    dprintf(idx, "%s", SLGRTHEAD);
+  else if (today == 1)
+    dprintf(idx, "%s", SLGRDHEAD);
+  else if (today == 2)
+    dprintf(idx, "%s", SLGRWHEAD);
+  else if (today == 3)
+    dprintf(idx, "%s", SLGRMHEAD);
+  sprintf(what, "%s", graphstats);
+  pwhat = what;
+  while (strlen(pwhat) > 0) {
+    type = newsplit(&pwhat);
+    itype = typetoi(type);
+    if (itype < 0) {
+      putlog(LOG_MISC, "*", "Stats.mod: Error serving livestats. Unsupported type %s. Skipping.", type);
+      continue;
+    }
+    sortstats(gs, itype, today);
+    max = 0;
+    for (ls = gs->slocal[today][itype]; ls; ls = ls->snext[today][itype]) {
+      if (listsuser(ls, channel)) {
+        max = ls->values[today][itype];
+        break;
+      }
+    }
+    if (max == 0)
+      continue;
+    total = gettotal(gs, itype, today);
+    rest = total;
+    dprintf(idx, "<br><br><br><table width=100%% border=1><tr><th width=100%% align=center>");
+    dprintf(idx, SLGRORDEREDBY, getslangtype(type));
+    dprintf(idx, "</td></tr></table>\n");
+    slglobint = total;
+    dprintf(idx, SLGRTOTAL, getslangtype(type));
+    dprintf(idx, "<br>\n");
+    dprintf(idx, "<table width=100%%>\n");
+    if (!total || !max)
+      continue;
+    onep = (float) total / 100.0;
+    nr = 0;
+    for (ls = gs->slocal[today][itype]; ls; ls = ls->snext[today][itype]) {
+      if (!listsuser(ls, channel))
+        continue;
+      if (!ls->values[today][itype])
+        break;
+      nr++;
+      if (nr > graphnr)
+        break;
+      dprintf(idx, "<tr>\n");
+      percent = (float) ls->values[today][itype] / onep;
+      wpercent = (float) ls->values[today][itype] / ((float) max / 100.0);
+      width = (int) wpercent * 0.8;
+      dprintf(idx, "  <td align=right width=10%%><a href=\"../../../users/%s/\">%s</a></td>\n", ls->user, ls->user);
+      dprintf(idx, "  <td align=left width=90%%>\n");
+      dprintf(idx, "    <table width=100%%><tr>\n");
+      dprintf(idx, "      <td width=%d%% bgcolor=\"%s\" %s>&nbsp;</td>\n", width, graphcolor, bground);
+      dprintf(idx, "      <td align=left width=%d%%>%.2f%% <font size=-2>[%d]</font></td>\n", 100 - width, percent, ls->values[today][itype]);
+      dprintf(idx, "    </tr></table>\n");
+      dprintf(idx, "  </td>\n");
+      dprintf(idx, "</tr>\n");
+      rest -= ls->values[today][itype];
+    }
+    dprintf(idx, "<tr>\n");
+    percent = (float) rest / ((float) total / 100.0);
+    wpercent = (float) rest / ((float) max / 100.0);
+    width = (int) wpercent * 0.8;
+    dprintf(idx, "  <td align=right width=10%%>%s</td>\n", SLGROTHERS);
+    dprintf(idx, "  <td align=left width=90%%>\n");
+    dprintf(idx, "    <table width=100%%><tr>\n");
+    dprintf(idx, "      <td width=%d%% bgcolor=\"%s\" %s>&nbsp;</td>\n", width, graphcolor, bground);
+    dprintf(idx, "      <td align=left width=%d%%>%.2f%% <font size=-2>[%d]</font></td>\n", 100 - width, percent, rest);
+    dprintf(idx, "    </tr></table>\n");
+    dprintf(idx, "  </td>\n");
+    dprintf(idx, "</tr></table>\n");
+  }
+  dprintf(idx, "</table>\n");
+  dprintf(idx, "<br><hr><br>\n");
+  dprintf(idx, "<table width=100%% border=0>\n");
+  dprintf(idx, "<tr><td width=25%% align=center><a href=\"../words/\">%s</a></td></tr>\n", SLGRTABLE);
+  dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../total/graphs/\">%s</a></td>\n", SLTOTAL);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../daily/graphs/\">%s</a></td>\n", SLDAILY);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../weekly/graphs/\">%s</a></td>\n", SLWEEKLY);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../monthly/graphs/\">%s</a></td>\n", SLMONTHLY);
+  dprintf(idx, "</tr></table></td></tr>\n");
+  dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../../misc/\">%s</a></td>\n", SLMISCSTATS);
+  dprintf(idx, "<td width=25%% align=center><%sa href=\"../../../users/\">%s</a></td>\n",
+          ISLINK(show_userlist), ISTEXT(show_userlist, SLUSERS));
+  dprintf(idx, "<td width=25%% align=center><%sa href=\"../../../onchan/\">%s</a></td>\n",
+          ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLONCHAN));
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../../../\">%s</a></td>\n", SLOTHERCHANS);
+  dprintf(idx, "</tr></table></td></tr>\n");
+  dprintf(idx, "</table>\n");
+  dprintf(idx, "<br><center>Created by "
+          "<a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod "
+          "v%s</a></center>\n", MODULE_VERSION);
+  long_dprintf(idx, SLFOOTER);
+  dprintf(idx, "</body></html>");
+  return;
+}
+
+static void do_toptalkers(int idx, int today, globstats *gs, char *channel, char *buf)
+{
+  char what2[512], *pwhat2, *type, *type2;
+  locstats *ls;
+  int itype, itype2, pitype, nr, i;
+  int wert;
+  float r, g, b;
+  float r2, g2, b2;
+  float rstep, gstep, bstep;
+  quotestr *q;
+  unsigned long x;
+
+  Context;
+  setslglobs(channel, gs->peak[today], countstatmembers(gs), gs->started);
+  type = buf;
+  itype = typetoi(type);
+  if (itype == T_ERROR) {
+    dprintf(idx, "<html><body>Error! No such type %s</body></html>", type);
+    if (idx >= 0)
+      stats_info_access(idx)->code = 404;
+    return;
+  }
+  if (itype < 0)
+    pitype = (itype * -1) + (TOTAL_TYPES - 1);
+  else
+    pitype = itype;
+  dprintf(idx, "<html><head><META http-equiv=\"Pragma\" content=\"no-cache\"><META http-equiv=\"Expires\" content=\"now\">\n");
+  if (today == 0)
+    dprintf(idx, "<title>%s</title>\n", SLTTOPTITLE);
+  else if (today == 1)
+    dprintf(idx, "<title>%s</title>\n", SLDTOPTITLE);
+  else if (today == 2)
+    dprintf(idx, "<title>%s</title>\n", SLWTOPTITLE);
+  else if (today == 3)
+    dprintf(idx, "<title>%s</title>\n", SLMTOPTITLE);
+  dprintf(idx, "<META Name=\"Stats.mod\" Content=\"%s\">\n", MODULE_VERSION);
+  dprintf(idx, "<META Name=\"channel\" Content=\"%s\">\n", gs->chan);
+  dprintf(idx, "<META Name=\"network\" Content=\"%s\">\n", network);
+  dprintf(idx, "<META Name=\"written\" Content=\"%lu\">\n", now);
+  dprintf(idx, "%s\n</head>\n", SLCSS);
+  dprintf(idx, "%s", SLBODYTAG);
+  long_dprintf(idx, SLHEADER);
+  if (today == 0)
+    dprintf(idx, "%s\n", SLTTOPHEAD);
+  else if (today == 1)
+    dprintf(idx, "%s\n", SLDTOPHEAD);
+  else if (today == 2)
+    dprintf(idx, "%s\n", SLWTOPHEAD);
+  else if (today == 3)
+    dprintf(idx, "%s\n", SLMTOPHEAD);
+
+  dprintf(idx, SLORDEREDBY, getslangtype(type));
+  dprintf(idx, "<BR>\n");
+  dprintf(idx, SLPEAK);
+  dprintf(idx, "<BR>\n");
+  dprintf(idx, "<P><center><table border=%d cellpadding=1><tr align=right>", table_border);
+  dprintf(idx, "<th>%s</th>", SLTNR);
+  dprintf(idx, "<th align=center>Nick</th>");
+  sprintf(what2, "%s", webstats);
+  pwhat2 = what2;
+  while (strlen(pwhat2) > 0) {
+    type2 = newsplit(&pwhat2);
+    dprintf(idx, "<th><a href=\"../%s/\">%s</a></th>\n", type2, getslangtype(type2));
+  }
+  sortstats(gs, itype, today);
+  nr = 0;
+  wert = table_color;
+  b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+  wert = fade_table_to;
+  b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+  rstep = (r2 - r) / webnr;
+  gstep = (g2 - g) / webnr;
+  bstep = (b2 - b) / webnr;
+  for (ls = gs->slocal[today][pitype]; ls; ls = ls->snext[today][pitype]) {
+    if (!listsuser(ls, channel))
+      continue;
+    if ((itype >= 0) && !ls->values[today][itype])
+      break;
+    nr++;
+    if (nr > webnr)
+      break;
+    dprintf(idx, "<tr align=\"right\" bgcolor=#%02x%02x%02x><td>%d</td><td><a href=\"../../../users/%s/\">%s</a></td>",
+            (int) r, (int) g, (int) b, nr, ls->user, ls->user);
+    r += rstep;
+    g += gstep;
+    b += bstep;
+    sprintf(what2, "%s", webstats);
+    pwhat2 = what2;
+    while (strlen(pwhat2) > 0) {
+      type2 = newsplit(&pwhat2);
+      itype2 = typetoi(type2);
+      dprintf(idx, "<td>");
+      // mark the sorted value bold
+      if (itype == itype2)
+        dprintf(idx, "<b>");
+      // now output the values
+      // T_MINUTES gets a special handling, because it's kinda hard to read
+      // something like "2348 Minutes" *g*
+      if (itype2 == T_MINUTES)
+        dprintf(idx, "%s", stats_duration(ls->values[today][itype2] * 60));
+      // no positive type needs any special handling, so lt's just output it
+      else if (itype2 >= 0)
+        dprintf(idx, "%d", ls->values[today][itype2]);
+      // words per line need to be calculated
+      else if (itype2 == T_WPL) {
+        if (ls->values[today][T_LINES])
+          dprintf(idx, "%.2f", ((float) ls->values[today][T_WORDS]) / ((float) ls->values[today][T_LINES]));
+        else
+          dprintf(idx, "0");
+      // idle-factor also needs to be calculated
+      } else if (itype2 == T_IDLE) {
+        if (ls->values[today][T_LINES])
+          dprintf(idx, "%.2f", ((float) ls->values[today][T_MINUTES]) / ((float) ls->values[today][T_LINES]));
+        else
+          dprintf(idx, "0");
+      } else if (itype2 == T_QUOTE) {
+        if (!ls->quotes)
+          dprintf(idx, "&nbsp;");
+        else {
+          nr = 0;
+          for (q = ls->quotes; q; q = q->next)
+            nr++;
+          x = random() % nr;
+          i = 0;
+          q = ls->quotes;
+          while (q) {
+            if (i == x) {
+              dprintf(idx, "%s", filt2(q->quote));
+              break;
+            }
+            q = q->next;
+            i++;
+          }
+	}
+      } else  // output an error, if we missed something
+        dprintf(idx, "ERROR! itype2: %d", itype2);
+      if (itype == itype2)
+        dprintf(idx, "</b>");
+      dprintf(idx, "</td>");
+    }
+    dprintf(idx, "</tr>\n");
+  }
+  dprintf(idx, "<tr>");
+  dprintf(idx, "<td colspan=%d align=center><b>", countwords(webstats) + 2);
+  dprintf(idx, SLTOTALUSERS);
+  dprintf(idx, "</B></td></tr>\n");
+  dprintf(idx, "<tr><td colspan=%d align=center>", countwords(webstats) + 2);
+  dprintf(idx, "<table width=100%% border=0>\n");
+  dprintf(idx, "<tr><td width=100%% align=center colspan=4><a href=\"../graphs/\">%s</a></td></tr>\n", SLGRAPHS);
+  dprintf(idx, "<tr><td width=25%% align=center><a href=\"../../total/%s/\">%s</a></td>\n", type, SLTOTAL);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../daily/%s/\">%s</a></td>\n", type, SLDAILY);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../weekly/%s/\">%s</a></td>\n", type, SLWEEKLY);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../monthly/%s/\">%s</a></td>\n", type, SLMONTHLY);
+  dprintf(idx, "</tr></table></td></tr>\n");
+  dprintf(idx, "<tr><td colspan=%d align=center><table width=100%% border=0><tr>\n", countwords(webstats) + 2);
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../../misc/\">%s</a></td>\n", SLMISCSTATS);
+  dprintf(idx, "<td width=25%% align=center><%sa href=\"../../../users/\">%s</a></td>\n",
+          ISLINK(show_userlist), ISTEXT(show_userlist, SLUSERS));
+  dprintf(idx, "<td width=25%% align=center><%sa href=\"../../../onchan/\">%s</a></td>\n",
+          ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLONCHAN));
+  dprintf(idx, "<td width=25%% align=center><a href=\"../../../../\">%s</a></td>\n", SLOTHERCHANS);
+  dprintf(idx, "</tr>\n</table></td></tr>\n");
+  dprintf(idx, "</table><br>");
+  if (!today)
+    dprintf(idx, SLGSTARTED);
+  dprintf(idx, "<br><a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod v%s</a>.", MODULE_VERSION);
+  dprintf(idx, "</CENTER>\n");
+  long_dprintf(idx, SLFOOTER);
+  dprintf(idx, "</body></html>");
+  return;
+}
+
+static void do_miscstats(int idx, char *channel)
+{
+  globstats *gs, *tlds, *isps;
+  wordstats *ws;
+  int nr, tr, i, wert, wieoft, pitype;
+  locstats *ls;
+  char *host, *s;
+  topicstr *t;
+  char ts[20];
+  float f, max, onep;
+  hoststr *h, *isp, *tld;
+  struct slang_lang *l;
+  struct slang_bntypes *ty;
+  struct slang_bnplaces *p;
+  struct slang_texts *txt;
+  unsigned long x;
+  float r, g, b;
+  float r2, g2, b2;
+  float rstep, gstep, bstep;
+  struct stats_url *url;
+  struct stats_kick *kick;
+  struct stats_quote *log;
+
+  Context;
+  gs = findglobstats(channel);
+  if (!gs) {
+    dprintf(idx, "<html><body>ERROR! Can't find stats for %s!</body></html>\n", channel);
+    return;
+  }
+  setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+  dprintf(idx, "<html><head><META http-equiv=\"Pragma\" content=\"no-cache\">"
+          "<META http-equiv=\"Expires\" content=\"now\"><title>%s</title>\n",
+          SLMISCTITLE);
+  long_dprintf(idx, SLCSS);
+  dprintf(idx, "</head>\n");
+  dprintf(idx, "%s\n", SLBODYTAG);
+  long_dprintf(idx, SLHEADER);
+  dprintf(idx, "%s<br>\n", SLMISCHEAD);
+  do_globwordstats(gs);
+  max = 0.0;
+  wert = nr = 0; /* I'm recycling these vars, so ignore the strange names */
+  for (i = 0; i < 24; i++) {
+    if (i < 12) {
+      if (gs->users[S_USERCOUNTS][i] > 0)
+        wert = 1;
+    } else {
+      if (gs->users[S_USERCOUNTS][i] > 0)
+        nr = 1;
+    }
+    if (gs->users[S_USERCOUNTS][i] > 0)
+      if ((((float) gs->users[S_USERSUM][i]) / ((float) gs->users[S_USERCOUNTS][i])) > max)
+        max = ((float) gs->users[S_USERSUM][i]) / ((float) gs->users[S_USERCOUNTS][i]);
+  }
+  if (display_average_users && (max > 0.0)) {
+    dprintf(idx, "%s<br><font size=\"-5\">\n", SLMAUSERS);
+    if (wert && nr)
+      dprintf(idx, "<table border=0 width=\"75%%\"><tr><td width=\"50%%\" align=\"center\" valign=\"bottom\">\n");
+    else
+      dprintf(idx, "<table border=0 width=\"37%%\"><tr><td width=\"100%%\" align=\"center\" valign=\"bottom\">\n");
+    dprintf(idx, "<table border=%d width=\"100%%\">", table_border);
+    onep = max / 100.0;
+    for (i = 0; i < 24; i++) {
+      if ((i == 12) && (wert && nr))
+        dprintf(idx, "</table></td><td width=\"50%%\" align=\"center\" valign=\"bottom\"><table border=%d width=\"100%%\">", table_border);
+      if (gs->users[S_USERCOUNTS][i] > 0) {
+        f = ((float) gs->users[S_USERSUM][i]) / ((float) gs->users[S_USERCOUNTS][i]);
+        dprintf(idx, "<tr><td width=\"1%%\"><font size=\"-5\">%d.00-%d.59</font></td><td width=\"99%%\"><table width=\"100%%\"><tr>", i, i);
+        if (((int) (f / onep)) > 0)
+          dprintf(idx, "<td width=\"%d%%\" bgcolor=\"%s\"><font size=\"-5\">&nbsp;</font></td><td><font size=\"-5\">%.2f</font></td></tr></table></td>", (int) (f / onep), graphcolor, f);
+        else if ((f / onep) > 0.0)
+          dprintf(idx, "<td width=\"100%%\"><font size=\"-5\">%.2f</font></td></tr></table></td>", f);
+        else
+          dprintf(idx, "<td width=\"100%%\"><font size=\"-5\">%.2f</font></td></tr></table></td>", f);
+        dprintf(idx, "</tr>\n");
+      }
+    }
+    dprintf(idx, "</table>");
+    dprintf(idx, "</td></tr></table>");
+    dprintf(idx, "</font>\n");
+  }
+  if (gs->topics) {
+    dprintf(idx, "<br><br>%s<br>\n<table border=%d>", SLMTOPICS, table_border);
+    i = 0;
+    for (t = gs->topics; t; t = t->next)
+      i++;
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / i;
+    gstep = (g2 - g) / i;
+    bstep = (b2 - b) / i;
+    for (t = gs->topics; t; t = t->next) {
+      strftime(ts, 20, "%H:%M", localtime(&t->when));
+      setslnick(t->by);
+      dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td>\"%s\"</td><td>",
+              (int) r, (int) g, (int) b, filt2(t->topic));
+      dprintf(idx, SLMTOPICBY, ts);
+      dprintf(idx, "</td></tr>\n");
+      r += rstep;
+      g += gstep;
+      b += bstep;
+    }
+    dprintf(idx, "</table><br><br>\n");
+  }
+  if (gs->urls && log_urls) {
+    nr = 0;
+    for (url = gs->urls; url; url = url->next) {
+      nr++;
+      url->shown = 0;
+    }
+    if (nr > log_urls)
+      wieoft = log_urls;
+    else
+      wieoft = nr;
+    slglobint = wieoft;
+    dprintf(idx, "<br><br><table border=%d><caption>%s</caption>", table_border, SLMURLS);
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / wieoft;
+    gstep = (g2 - g) / wieoft;
+    bstep = (b2 - b) / wieoft;
+    while (wieoft > 0) {
+      x = random() % nr;
+      i = 0;
+      url = gs->urls;
+      while (url) {
+        if (url->shown) {
+          url = url->next;
+          continue;
+        }
+        if (i == x) {
+          dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td>\"<a href=\"%s\">%s</a>\"</td><td>",
+                  (int) r, (int) g, (int) b, url->url, url->url);
+          strftime(ts, 20, "%H:%M", localtime(&url->when));
+          setslnick(url->by);
+          dprintf(idx, SLMURLBY, ts);
+          dprintf(idx, "</td></tr>\n");
+          url->shown = 1;
+          nr--;
+          r += rstep;
+          g += gstep;
+          b += bstep;
+          break;
+        }
+        url = url->next;
+        i++;
+      }
+      wieoft--;
+    }
+    dprintf(idx, "</table>\n");
+  }
+  if (gs->hosts) {
+    tlds = nmalloc(sizeof(globstats));
+    tlds->hosts = NULL;
+    isps = nmalloc(sizeof(globstats));
+    isps->hosts = NULL;
+    for (h = gs->hosts; h; h = h->next) {
+      // skip IPv6 hosts
+      if (strchr(h->host, ':'))
+        continue;
+      host = strrchr(h->host, '.') + 1;
+      if (!atoi(host) && (host[0] != '0')) {
+        addhost(host, tlds);
+        host = h->host;
+        while ((s = strchr(host, '.')) && strchr(s + 1, '.'))
+          host = s + 1;
+        addhost(host, isps);
+      } else {
+        addhost("[IP]", tlds);
+        addhost("[IP]", isps);
+      }
+    }
+    sorthosts(isps);
+    sorthosts(tlds);
+    dprintf(idx, "<br><br><table border=%d><tr><th colspan=2>%s</th></tr><tr><th>%s</th><th>%s</th></tr>\n",
+            table_border, SLMMOSTUSED, SLMISPS, SLMTLDS);
+    i = 0;
+    isp = isps->hosts;
+    tld = tlds->hosts;
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / 5;
+    gstep = (g2 - g) / 5;
+    bstep = (b2 - b) / 5;
+    while ((i <= 5) && (isp || tld)) {
+      i++;
+      dprintf(idx, "<tr bgcolor=#%02x%02x%02x>\n", (int) r, (int) g, (int) b);
+      r += rstep;
+      g += gstep;
+      b += bstep;
+      if (isp)
+        dprintf(idx, "<td>%s (%d)</td>", isp->host, isp->nr);
+      else
+        dprintf(idx, "<td>-</td>");
+      if (tld)
+        dprintf(idx, "<td>%s (%d)</td>", tld->host, tld->nr);
+      else
+        dprintf(idx, "<td>-</td>");
+      dprintf(idx, "</tr>\n");
+      if (isp)
+        isp = isp->next;
+      if (tld)
+        tld = tld->next;
+    }
+    dprintf(idx, "</table>\n");
+    free_hosts(isps->hosts);
+    free_hosts(tlds->hosts);
+    nfree(isps);
+    nfree(tlds);
+  }
+  if (gs->kicks && (display_kicks > 0)) {
+    nr = 0;
+    for (kick = gs->kicks; kick; kick = kick->next) {
+      nr++;
+      kick->shown = 0;
+    }
+    if (nr > display_kicks)
+      wieoft = display_kicks;
+    else
+      wieoft = nr;
+    slglobint = wieoft;
+    dprintf(idx, "<br><br><table border=%d><caption>%s</caption>", table_border, SLMKICKS);
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / wieoft;
+    gstep = (g2 - g) / wieoft;
+    bstep = (b2 - b) / wieoft;
+    while (wieoft > 0) {
+      x = random() % nr;
+      i = 0;
+      kick = gs->kicks;
+      while (kick) {
+        if (kick->shown) {
+          kick = kick->next;
+          continue;
+        }
+        if (i == x) {
+          dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td><cite><font size=-2>", (int) r, (int) g, (int) b);
+          for (log = kick->log; log; log = log->next) {
+	    if (log->next)
+              dprintf(idx, "%s<br>", filt2(log->quote));
+            else
+              dprintf(idx, "</font>%s</b><br>", filt2(log->quote));
+	  }
+          dprintf(idx, "</cite></td></tr>\n");
+          kick->shown = 1;
+          nr--;
+          r += rstep;
+          g += gstep;
+          b += bstep;
+          break;
+        }
+        kick = kick->next;
+        i++;
+      }
+      wieoft--;
+    }
+    dprintf(idx, "</table>\n");
+  }
+  if (gs->words) {
+    nr = 0;
+    for (ws = gs->words; ws; ws = ws->next)
+      nr++;
+    dprintf(idx, "<br><br>"); dprintf(idx, SLMCWORDSTATS, nr); dprintf(idx, "<br>\n");
+    dprintf(idx, "%s<br><table border=%d>\n", SLMMOSTUSEDWORDS, table_border);
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / 10;
+    gstep = (g2 - g) / 10;
+    bstep = (b2 - b) / 10;
+    ws = gs->words;
+    nr = 0;
+    while (ws && (nr < 10)) {
+      nr++;
+      dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td>%d.</td><td>%s</td><td>(%d)</td></tr>\n",
+              (int) r, (int) g, (int) b, nr, filt2(ws->word), ws->nr);
+      r += rstep;
+      g += gstep;
+      b += bstep;
+      ws = ws->next;
+    }
+    dprintf(idx, "</table>\n");
+  }
+  dprintf(idx, "<br><br><table border=%d>\n", table_border);
+  for (l = slangs; l; l = l->next) {
+    if ((!l->lang && !slgloblang) || (l->lang && slgloblang && !strcasecmp(l->lang, slgloblang))) {
+      i = 0;
+      for (ty = l->bignumbers; ty; ty = ty->next)
+        i++;
+      wert = table_color;
+      b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+      wert = fade_table_to;
+      b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+      rstep = (r2 - r) / i;
+      gstep = (g2 - g) / i;
+      bstep = (b2 - b) / i;
+      for (ty = l->bignumbers; ty; ty = ty->next) {
+        i = typetoi(ty->type);
+        sortstats(gs, i, S_DAILY);
+        tr = 0;
+        for (p = ty->places; p; p = p->next) {
+          nr = 1;
+          if (i < 0)
+            pitype = (i * -1) + (TOTAL_TYPES - 1);
+          else
+            pitype = i;
+          ls = gs->slocal[S_DAILY][pitype];
+          while (ls && (nr < p->place)) {
+            nr++;
+            ls = ls->snext[S_DAILY][pitype];
+          }
+          // just skip this entry if any vital information is missing.
+          if (!ls)
+            continue;
+          else if ((i >= 0) && !ls->values[S_DAILY][i])
+            continue;
+          else if ((i == T_WPL) && (!ls->values[S_DAILY][T_WORDS] || !ls->values[S_DAILY][T_LINES]))
+            continue;
+          else if ((i == T_IDLE) && (!ls->values[S_DAILY][T_MINUTES] || !ls->values[S_DAILY][T_LINES]))
+            continue;
+          else if ((i == T_VOCABLES) && !ls->vocables)
+            continue;
+	  if (!tr) {
+	    dprintf(idx, "<tr bgcolor=#%02x%02x%02x><td>\n", (int) r, (int) g, (int) b);
+            r += rstep;
+            g += gstep;
+            b += bstep;
+	    tr = 1;
+	  }
+          slgloblocstats = ls;
+          slglobtype = ty->type;
+          x = random() % p->entries;
+          txt = p->texts;
+          while (txt) {
+            if (!x)
+              dprintf(idx, "%s\n", dynamicslang(txt->text));
+            x--;
+            txt = txt->next;
+	  }
+	}
+	if (tr)
+	  dprintf(idx, "</td></tr>\n");
+      }
+    }
+  }
+  dprintf(idx, "</table>\n");
+  dprintf(idx, "<br><br><hr>\n");
+  dprintf(idx, "<table width=100%% border=0>\n");
+  dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+  dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../top/total/words/\">top%d</a></font></td>\n", webnr);
+  dprintf(idx, "<td width=25%% align=center><font size=-1><%sa href=\"../users/\">%s</a></font></td>\n",
+          ISLINK(show_userlist), ISTEXT(show_userlist, SLUSERS));
+  dprintf(idx, "<td width=25%% align=center><font size=-1><%sa href=\"../onchan/\">%s</a></font></td>\n",
+          ISLINK(show_usersonchan), ISTEXT(show_usersonchan, SLONCHAN));
+  dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../../\">%s</a></font></td>\n", SLOTHERCHANS);
+  dprintf(idx, "</tr></table></td></tr>\n");
+  dprintf(idx, "</table>\n");
+  dprintf(idx, "<center>Created by <a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod v%s</a></center></body></html>", MODULE_VERSION);
+  long_dprintf(idx, SLFOOTER);
+  dprintf(idx, "</body></html>");
+}
+
+static void display_users_on_chan(int idx, char *channel, struct chanset_t *chan)
+{
+  memberlist *m;
+  struct stats_memberlist *mm;
+  int wert;
+  float r, g, b;
+  float r2, g2, b2;
+  float rstep, gstep, bstep;
+  int i;
+  char s1[121];
+
+  Context;
+  dprintf(idx, "<html><head><title>%s</title>\n%s\n</head>\n", SLUSERSONCHANTITLE, SLCSS);
+  dprintf(idx, "%s\n", SLBODYTAG);
+  long_dprintf(idx, SLHEADER);
+  if (show_usersonchan && chan && (chan->channel.members > 0)) {
+    dprintf(idx, "<br><br><center><table border=%d>\n<tr><th colspan=3>%s</th></tr>\n", table_border, SLNOWONCHAN);
+    dprintf(idx, "<tr><th align=\"center\">nick</th><th align=\"center\">user</th><th align=\"center\">%s</th></tr>\n", SLIDLETIME);
+    i = 0;
+    for (m = chan->channel.member; m && m->nick[0]; m = m->next)
+      i++;
+    wert = table_color;
+    b = wert & 0xff; g = (wert & 0xff00) >> 8; r = (wert & 0xff0000) >> 16;
+    wert = fade_table_to;
+    b2 = wert & 0xff; g2 = (wert & 0xff00) >> 8; r2 = (wert & 0xff0000) >> 16;
+    rstep = (r2 - r) / i;
+    gstep = (g2 - g) / i;
+    bstep = (b2 - b) / i;
+    for (m = chan->channel.member; m && m->nick[0]; m = m->next) {
+      dprintf(idx, "<tr bgcolor=#%02x%02x%02x>", (int) r, (int) g, (int) b);
+      dprintf(idx, "<td align=\"left\">%s%s</td>",
+              chan_hasop(m) ? "@" : (chan_hasvoice(m) ? "+" : "&nbsp;"),
+              m->nick);
+      r += rstep;
+      g += gstep;
+      b += bstep;
+      if (!m->user && ((strlen(m->nick) + strlen(m->userhost) + 1) < 120)) {
+        sprintf(s1, "%s!%s", m->nick, m->userhost);
+        m->user = get_user_by_host(s1);
+      }
+      if (!use_userfile) {
+        mm = nick2suser(m->nick, channel);
+        if (mm && mm->user)
+          dprintf(idx, "<td align=\"center\"><a href=\"../users/%s/\">%s</a></td>", mm->user->user, mm->user->user);
+        else
+          dprintf(idx, "<td align=\"center\">-</td>");
+      } else {
+        if (m->user)
+          dprintf(idx, "<td align=\"center\"><a href=\"../users/%s/\">%s</a></td>", m->user->handle, m->user->handle);
+        else
+          dprintf(idx, "<td align=\"center\">-</td>");
+      }
+      if (chan_issplit(m))
+        dprintf(idx, "<td align=\"right\">%s</td>", SLNETSPLITTED);
+      else if (!rfc_casecmp(m->nick, botname))
+        dprintf(idx, "<td align=\"right\">%s</td>", SLITSME);
+      else
+        dprintf(idx, "<td align=\"right\">&nbsp;%s</td>", stats_duration(now - m->last));
+      dprintf(idx, "</tr>\n");
+    }
+    dprintf(idx, "</table></center>\n<br><br>");
+  }
+  dprintf(idx, "<table width=100%% border=0>\n");
+  dprintf(idx, "<tr><td align=center><table width=100%% border=0><tr>\n");
+  dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../top/total/words/\">top%d</a></font></td>\n", webnr);
+  dprintf(idx, "<td width=25%% align=center><font size=-1><%sa href=\"../users/\">%s</a></font></td>\n",
+          ISLINK(show_userlist), ISTEXT(show_userlist, SLUSERS));
+  dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../misc/\">%s</a></font></td>\n", SLMISCSTATS);
+  dprintf(idx, "<td width=25%% align=center><font size=-1><a href=\"../../\">%s</a></font></td>\n", SLOTHERCHANS);
+  dprintf(idx, "</tr></table></td></tr>\n");
+  dprintf(idx, "</table>\n");
+  dprintf(idx, "<center>Created by <a href=\"http://www.visions-of-fantasy.de/stats.mod/\">Stats.mod v%s</a></center></body></html>", MODULE_VERSION);
+  long_dprintf(idx, SLFOOTER);
+  dprintf(idx, "</body></html>");
+}

+ 407 - 0
misc.c

@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+/* stolen from tcl_duration in tclmisc.c */
+char duration_temp[256];
+static char *stats_duration(int seconds)
+{
+  char s[256];
+  time_t sec;
+
+  sec = seconds;
+  s[0] = 0;
+  if (sec < 1) {
+    sprintf(duration_temp, "0 seconds");
+    return duration_temp;
+  }
+  if (sec >= 31536000) {
+    sprintf(s, "%d %s ", (int) (sec / 31536000),
+            ((int) (sec / 31536000) > 1) ? SLYEARS : SLYEAR);
+    sec -= (((int) (sec / 31536000)) * 31536000);
+  }
+  if (sec >= 604800) {
+    sprintf(&s[strlen(s)], "%d %s ", (int) (sec / 604800),
+            ((int) (sec / 604800) > 1) ? SLWEEKS : SLWEEK);
+    sec -= (((int) (sec / 604800)) * 604800);
+  }
+  if (sec >= 86400) {
+    sprintf(&s[strlen(s)], "%d %s ", (int) (sec / 86400),
+            ((int) (sec / 86400) > 1) ? SLDAYS : SLDAY);
+    sec -= (((int) (sec / 86400)) * 86400);
+  }
+  if (sec >= 3600) {
+    sprintf(&s[strlen(s)], "%d %s ", (int) (sec / 3600),
+            ((int) (sec / 3600) > 1) ? SLHOURS : SLHOUR);
+    sec -= (((int) (sec / 3600)) * 3600);
+  }
+  if (sec >= 60) {
+    sprintf(&s[strlen(s)], "%d %s ", (int) (sec / 60),
+            ((int) (sec / 60) > 1) ? SLMINUTES : SLMINUTE);
+    sec -= (((int) (sec / 60)) * 60);
+  }
+/*  is there any place where seconds might be displayed??? I think not. */
+/*
+  if (sec > 0) {
+    sprintf(&s[strlen(s)], "%d %s", (int) (sec / 1),
+            ((int) (sec / 1) > 1) ? SLSECONDS : SLSECOND);
+  }
+*/
+  sprintf(duration_temp, "%s", s);
+  return duration_temp;
+}
+
+static int countsmileys(char *text)
+{
+  char buf[512], *pbuf, *smiley, *p;
+  int ismileys = 0;
+
+  sprintf(buf, "%s", smileys);
+  pbuf = buf;
+  while (strlen(pbuf) > 0) {
+    smiley = newsplit(&pbuf);
+    p = strstr(text, smiley);
+    while (p) {
+      ismileys++;
+      p += strlen(smiley);
+      p = strstr(p, smiley);
+    }
+  }
+  return ismileys;
+}
+
+static int countstatmembers(globstats *gs)
+{
+  int members = 0;
+  struct userrec *u;
+  locstats *ls;
+
+  Context;
+  if (!gs)
+    return 0;
+  for (ls = gs->local; ls; ls = ls->next) {
+    u = get_user_by_handle(userlist, ls->user);
+    if (matchattr(u, nostatsflags, gs->chan))
+      continue;
+    members++;
+  }
+  return members;
+}
+
+static int gettotal(globstats *gs, int type, int today)
+{
+  int total = 0;
+  struct userrec *u;
+  struct stats_userlist *su;
+  locstats *ls;
+
+  for (ls = gs->local;ls; ls = ls->next) {
+    if (use_userfile) {
+      u = get_user_by_handle(userlist, ls->user);
+      if (matchattr(u, nostatsflags, gs->chan))
+        continue;
+    } else {
+      su = findsuser_by_name(ls->user);
+      if (su && !su->list)
+        continue;
+    }
+    total += ls->values[today][type];
+  }
+  return total;
+}
+
+/* stolen from tcl_matchattr */
+static int matchattr (struct userrec *u, char *flags, char *chan) {
+  struct flag_record plus, minus, user;
+  int ok = 0, f;
+
+#ifndef OLDBOT
+  if (u && (!chan || findchan_by_dname(chan))) {
+#else
+  if (u && (!chan || findchan(chan))) {
+#endif
+    user.match = FR_GLOBAL | (chan ? FR_CHAN : 0) | FR_BOT;
+    get_user_flagrec(u, &user, chan);
+    plus.match = user.match;
+    break_down_flags(flags, &plus, &minus);
+    f = (minus.global || minus.udef_global || minus.chan ||
+   minus.udef_chan || minus.bot);
+    if (flagrec_eq(&plus, &user)) {
+      if (!f)
+  ok = 1;
+      else {
+  minus.match = plus.match ^ (FR_AND | FR_OR);
+  if (!flagrec_eq(&minus, &user))
+    ok = 1;
+      }
+    }
+  }
+  return ok;
+}
+
+static int countwords(char *buf)
+{
+  int i, words = 1;
+
+  for (i = 0; i < strlen(buf); i++) {
+    if ((buf[i] == ' ') && (buf[i+1] != ' '))
+      words++;
+  }
+  return words;
+}
+
+static int countquestions(char *buf)
+{
+  int i, questions = 0;
+
+  for (i = 0; i < strlen(buf); i++) {
+    if ((buf[i] == '?') && (buf[i+1] != '?'))
+      questions++;
+  }
+  return questions;
+}
+
+static char *splitpath(char **rest)
+{
+  register char *o, *r;
+
+  if (!rest)
+    return *rest = "";
+  o = *rest;
+  while (*o == ' ')
+    o++;
+  r = o;
+  while (*o && (*o != '/'))
+    o++;
+  if (*o)
+    *o++ = 0;
+  *rest = o;
+  return r;
+}
+
+static void strlower(char *text)
+{
+  int i;
+
+  for (i = 0; i < strlen(text); i++)
+    text[i] = tolower(text[i]);
+}
+
+static void filt(char *text)
+{
+  int i;
+
+  for (i = 0; i < strlen(text); i++)
+    if (text[i] == '%')
+      text[i] = ' ';
+}
+
+char filt2_buf[512];
+static char *filt2(char *text)
+{
+  char *buf;
+  int i = 0;
+
+  buf = filt2_buf;
+  while ((i < 500) && text[0]) {
+    if (text[0] == '<') {
+      buf[0] = '&';
+      buf[1] = 'l';
+      buf[2] = 't';
+      buf[3] = ';';
+      buf += 4;
+      i += 4;
+    } else if (text[0] == '>') {
+      buf[0] = '&';
+      buf[1] = 'g';
+      buf[2] = 't';
+      buf[3] = ';';
+      buf += 4;
+      i += 4;
+    } else {
+      buf[0] = text[0];
+      buf++;
+      i++;
+    }
+    text++;
+  }
+  buf[0] = 0;
+  return filt2_buf;
+}
+
+static int gethour()
+{
+  char ts[10];
+  time_t tt;
+
+  tt = now;
+  strftime(ts, 9, "%H", localtime(&tt));
+  ts[9] = 0;
+  return atoi(ts);
+}
+
+static int getmonth()
+{
+  char ts[10];
+  time_t tt;
+
+  tt = now;
+  strftime(ts, 9, "%m", localtime(&tt));
+  ts[9] = 0;
+  return atoi(ts);
+}
+
+static int ismonday()
+{
+  char ts[10];
+  time_t tt;
+
+  tt = now;
+  strftime(ts, 9, "%a", localtime(&tt));
+  ts[9] = 0;
+  return (!strcasecmp(ts, "mon"));
+}
+
+static int secretchan(char *chan)
+{
+  struct chanset_t *ch;
+
+  if (list_secret_chans)
+    return 0;
+  ch = findchan_by_dname(chan);
+  if (!ch)
+    return 0;
+  if (ch->status & CHAN_SECRET)
+    return 1;
+  return 0;
+}
+
+static void long_dprintf(int idx, char *text)
+{
+  char buf[501];
+
+  if (strlen(text) < 500)
+    dprintf(idx, "%s", text);
+  else {
+    while (text[0]) {
+      strncpy(buf, text, 500);
+      buf[500] = 0;
+      dprintf(idx, "%s", buf);
+      text += strlen(buf);
+    }
+  }
+}
+
+/* maskstricthost():
+ * basically the same as maskhost() from src/misc.c, but _never_ stripts
+ * "~+-^=" off the host
+ * maskhost() version: * $Id: misc.c,v 1.30 2000/10/27 19:27:32 fabian Exp $
+ */
+static void maskstricthost(const char *s, char *nw)
+{
+  register const char *p, *q, *e, *f;
+  int i;
+
+  *nw++ = '*';
+  *nw++ = '!';
+  p = (q = strchr(s, '!')) ? q + 1 : s;
+  /* Strip of any nick, if a username is found, use last 8 chars */
+  if ((q = strchr(p, '@'))) {
+    int fl = 0;
+
+    if ((q - p) > 9) {
+      nw[0] = '*';
+      p = q - 7;
+      i = 1;
+    } else
+      i = 0;
+    while (*p != '@') {
+      if (!fl && strchr("~+-^=", *p)) {
+//        if (strict_host)
+      nw[i] = '?';
+//    else
+//      i--;
+      } else
+    nw[i] = *p;
+      fl++;
+      p++;
+      i++;
+    }
+    nw[i++] = '@';
+    q++;
+  } else {
+    nw[0] = '*';
+    nw[1] = '@';
+    i = 2;
+    q = s;
+  }
+  nw += i;
+  e = NULL;
+  /* Now q points to the hostname, i point to where to put the mask */
+  if ((!(p = strchr(q, '.')) || !(e = strchr(p + 1, '.'))) && !strchr(q, ':'))
+    /* TLD or 2 part host */
+    strcpy(nw, q);
+  else {
+    if (e == NULL) {        /* IPv6 address?        */
+      const char *mask_str;
+
+      f = strrchr(q, ':');
+      if (strchr(f, '.')) { /* IPv4 wrapped in an IPv6? */
+    f = strrchr(f, '.');
+    mask_str = ".*";
+      } else            /* ... no, true IPv6.       */
+    mask_str = ":*";
+      strncpy(nw, q, f - q);
+      /* No need to nw[f-q] = 0 here, as the strcpy below will
+       * terminate the string for us.
+       */
+      nw += (f - q);
+      strcpy(nw, mask_str);
+    } else {
+      for (f = e; *f; f++);
+      f--;
+      if (*f >= '0' && *f <= '9') { /* Numeric IP address */
+    while (*f != '.')
+      f--;
+    strncpy(nw, q, f - q);
+    /* No need to nw[f-q] = 0 here, as the strcpy below will
+     * terminate the string for us.
+     */
+    nw += (f - q);
+    strcpy(nw, ".*");
+      } else {              /* Normal host >= 3 parts */
+    /*    a.b.c  -> *.b.c
+     *    a.b.c.d ->  *.b.c.d if tld is a country (2 chars)
+     *             OR   *.c.d if tld is com/edu/etc (3 chars)
+     *    a.b.c.d.e -> *.c.d.e   etc
+     */
+    const char *x = strchr(e + 1, '.');
+
+    if (!x)
+      x = p;
+    else if (strchr(x + 1, '.'))
+      x = e;
+    else if (strlen(x) == 3)
+      x = p;
+    else
+      x = e;
+    sprintf(nw, "*%s", x);
+      }
+    }
+  }
+}
+

+ 290 - 0
msgcmds.c

@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int validchan(char *chan)
+{
+  struct chanset_t *ch;
+
+#ifndef OLDBOT
+  ch = findchan_by_dname(chan);
+#else
+  ch = findchan(chan);
+#endif
+  if (ch)
+    return 1;
+  return 0;
+}
+
+static int msg_top10 (char *nick, char *uhost, struct userrec *u, char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2030));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! top10 %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_top(chan, nick, nick, rest, 1, 10, 0);
+  return 1;
+}
+
+static int msg_ttop10 (char *nick, char *uhost, struct userrec *u, char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2031));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! ttop10 %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_top(chan, nick, nick, rest, 1, 10, 1);
+  return 1;
+}
+
+static int msg_top20 (char *nick, char *uhost, struct userrec *u, char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2032));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! top20 %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_top(chan, nick, nick, rest, 11, 20, 0);
+  return 1;
+}
+
+static int msg_ttop20 (char *nick, char *uhost, struct userrec *u, char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2033));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! ttop20 %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_top(chan, nick, nick, rest, 11, 20, 1);
+  return 1;
+}
+
+static int msg_place(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2034));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! place %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_place(nick, nick, u ? u->handle : "*", chan, rest, S_TOTAL);
+  return 1;
+}
+
+static int msg_tplace(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2035));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! tplace %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_place(nick, nick, u ? u->handle : "*", chan, rest, S_TODAY);
+  return 1;
+}
+
+static int msg_stat(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2036));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! stat %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_stat(nick, nick, u ? u->handle : "*", chan, rest, S_TOTAL);
+  return 1;
+}
+
+static int msg_tstat(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2037));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! tstat %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_stat(nick, nick, u ? u->handle : "*", chan, rest, S_TODAY);
+  return 1;
+}
+
+static int msg_wordstats(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2038));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! wordstats %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_wordstats(nick, nick, u ? u->handle : "*", chan, rest);
+  return 1;
+}
+
+static int msg_topwords(char *nick, char *uhost, struct userrec *u,
+        char *rest)
+{
+  char *chan;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  chan = newsplit(&rest);
+  if (!validchan(chan)) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, getslang(2039));
+    return 1;
+  }
+  putlog(LOG_CMDS, "*", "(%s!%s) !%s! topwords %s %s", nick, uhost, u ? u->handle : "*", chan, rest);
+  tell_topwords(nick, nick, u ? u->handle : "*", chan);
+  return 1;
+}
+
+static int msg_setemail(char *nick, char *uhost, struct userrec *user, char *rest)
+{
+  struct stats_userlist *u;
+  char *host;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  if (user)
+    u = findsuser_by_name(user->handle);
+  else {
+    host = nmalloc(strlen(nick) + 1 + strlen(uhost) + 1);
+    sprintf(host, "%s!%s", nick, uhost);
+    u = findsuser(host);
+    nfree(host);
+  }
+  if (!u) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLDONTRECOGNIZE);
+    return 1;
+  }
+  if (strchr(rest, ' ')) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLEMAILUSAGE);
+    return 1;
+  }
+  setemail(u, rest);
+  dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLSETEMAIL);
+  return 1;
+}
+
+static int msg_sethomepage(char *nick, char *uhost, struct userrec *user, char *rest)
+{
+  struct stats_userlist *u;
+  char *host, *url;
+
+  Context;
+  setslglobs("", 0, 0, 0);
+  setslnick(nick);
+  if (user)
+    u = findsuser_by_name(user->handle);
+  else {
+    host = nmalloc(strlen(nick) + 1 + strlen(uhost) + 1);
+    sprintf(host, "%s!%s", nick, uhost);
+    u = findsuser(host);
+    nfree(host);
+  }
+  if (!u) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLDONTRECOGNIZE);
+    return 1;
+  }
+  if (strchr(rest, ' ')) {
+    dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLHPUSAGE);
+    return 1;
+  }
+  if (strncasecmp(rest, "http://", 7)) {
+    url = nmalloc(7 + strlen(rest) + 1);
+    sprintf(url, "http://%s", rest);
+  } else {
+    url = nmalloc(strlen(rest) + 1);
+    strcpy(url, rest);
+  }
+  sethomepage(u, url);
+  nfree(url);
+  dprintf(DP_HELP, "PRIVMSG %s :%s\n", nick, SLSETHOMEPAGE);
+  return 1;
+}
+
+static cmd_t stats_msg[] =
+{
+  {"top10", "", msg_top10, 0},
+  {"ttop10", "", msg_ttop10, 0},
+  {"top20", "", msg_top20, 0},
+  {"ttop20", "", msg_ttop20, 0},
+  {"place", "", msg_place, 0},
+  {"tplace", "", msg_tplace, 0},
+  {"stat", "", msg_stat, 0},
+  {"tstat", "", msg_tstat, 0},
+  {"wordstats", "", msg_wordstats, 0},
+  {"topwords", "", msg_topwords, 0},
+  {"setemail", "", msg_setemail, 0},
+  {"sethomepage", "", msg_sethomepage, 0},
+  {0, 0, 0, 0}
+};

+ 784 - 0
pubcmds.c

@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int nopubstats(char *chan)
+{
+  if (!chan) {
+    debug0("WARNING: nopubstats() called with NULL pointer");
+    return 1;
+  }
+#if EGG_IS_MIN_VER(10503)
+  if (ngetudef("nopubstats", chan))
+    return 1;
+#endif
+  return 0;
+}
+
+static int quietstats(char *chan)
+{
+  if (!chan) {
+    debug0("WARNING: quietstats() called with NULL pointer");
+    return 1;
+  }
+#if EGG_IS_MIN_VER(10503)
+  if (ngetudef("quietstats", chan))
+    return 1;
+#endif
+  return 0;
+}
+
+static int pub_top10(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! top10 %s", nick, hand, text);
+  tell_top(channel, channel, nick, text, 1, 10, 0);
+  return 1;
+}
+
+static int pub_top20(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! top20 %s", nick, hand, text);
+  tell_top(channel, channel, nick, text, 11, 20, 0);
+  return 1;
+}
+
+static int pub_ttop10(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! ttop10 %s", nick, hand, text);
+  tell_top(channel, channel, nick, text, 1, 10, 1);
+  return 1;
+}
+
+static int pub_ttop20(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! ttop20 %s", nick, hand, text);
+  tell_top(channel, channel, nick, text, 11, 20, 1);
+  return 1;
+}
+
+static void tell_top(char *channel, char *dest, char *nick, char *type, int from, int to, int today)
+{
+  char t[50], *result, *tosend, *slang, *slangtype;
+  int i, ti, pi;
+  struct stats_global *l;
+  struct stats_local *ll;
+#if EGG_IS_MIN_VER(10500)
+  struct chanset_t *chan;
+#endif
+
+  Context;
+  filt(type);
+  if (stat_flood())
+    return;
+#if EGG_IS_MIN_VER(10500)
+  if (!strcasecmp(channel, dest)) {
+    chan = findchan_by_dname(dest);
+    if (chan)
+      dest = chan->name;
+  }
+#endif
+  l = findglobstats(channel);
+  if (!l) {
+    debug1("StatMod: Can't do topx for %s, no such entry in linked list", channel);
+    return;
+  }
+  if (!strncasecmp(type, "word ", 5)) {
+    tell_top_word(channel, dest, nick, type, from, to, l);
+    return;
+  }
+  if (!type[0] || (strlen(type) > 45))
+    strcpy(t, "words");
+  else
+    strcpy(t, type);
+  ti = slangtypetoi(t);
+  setslglobs(l->chan, l->peak[today], 0, l->started);
+  if ((ti < 0) && (ti != T_WPL) && (ti != T_IDLE)) {
+    slang = SLNOSUCHTYPE;
+    if (quietstats(dest)) { /* if dest != chan, we want a PRIVMSG anyway */
+      tosend = nmalloc(15 + strlen(nick) + strlen(slang) + 1);
+      sprintf(tosend, "NOTICE %s :%s\n", nick, slang);
+    } else {
+      tosend = nmalloc(16 + strlen(dest) + strlen(slang) + 1);
+      sprintf(tosend, "PRIVMSG %s :%s\n", dest, slang);
+    }
+    dprintf(DP_HELP, "%s", tosend);
+    nfree(tosend);
+    return;
+  }
+  sortstats(l, ti, today);
+  slglobint = to;
+  slang = getslang(1001 + today);
+  slangtype = getslangtype(t);
+  result = nmalloc(strlen(slang) + strlen(slangtype) + 1);
+  sprintf(result, slang, slangtype);
+  i = 1;
+//  pi = (T_WPL * (-1)) + TOTAL_TYPES - 1;  // THIS APPEARS WRONG... DOUBLE-CHECK LATER!!!
+  if (ti < 0)
+    pi = (ti * -1) + (TOTAL_TYPES - 1);
+  else
+    pi = ti;
+  for (ll = l->slocal[today][pi]; ll; ll = ll->snext[today][pi]) {
+    if (!listsuser(ll, channel))
+      continue;
+    if ((ti >= 0) && !ll->values[today][ti])
+      break;
+    if ((i >= from) && (i <= to)) {
+      if (ti >= 0) {
+        // how much should I alloc for an integer? *sigh*
+        // I'll just use 10... should be enough
+        result = nrealloc(result, strlen(result) + 10 + 5 + strlen(ll->user) + 10 + 1);
+        sprintf(result, "%s %d. %s(%ld)", result, i, ll->user, ll->values[today][ti]);
+      } else if (ti == T_WPL) {
+        result = nrealloc(result, strlen(result) + 10 + 5 + strlen(ll->user) + 10 + 1);
+        if (ll->values[today][T_LINES])
+          sprintf(result, "%s %d. %s(%.2f)", result, i, ll->user, (float) ll->values[today][T_WORDS] / (float) ll->values[today][T_LINES]);
+        else
+          sprintf(result, "%s %d. %s(0)", result, i, ll->user);
+      } else if (ti == T_IDLE) {
+        result = nrealloc(result, strlen(result) + 10 + 5 + strlen(ll->user) + 10 + 1);
+        if (ll->values[today][T_LINES])
+          sprintf(result, "%s %d. %s(%.2f)", result, i, ll->user, (float) ll->values[today][T_MINUTES] / (float) ll->values[today][T_LINES]);
+        else
+          sprintf(result, "%s %d. %s(0)", result, i, ll->user);
+      } else {
+        result = nrealloc(result, strlen(result) + 11 + 10 + 1);
+        sprintf(result, "%s ERROR (%d)", result, ti);
+      }
+    }
+    i++;
+  }
+  if (quietstats(dest)) {
+    tosend = nmalloc(12 + strlen(nick) + strlen(result) + 1);
+    sprintf(tosend, "NOTICE %s :%s\n", nick, result);
+  } else {
+    tosend = nmalloc(13 + strlen(dest) + strlen(result) + 1);
+    sprintf(tosend, "PRIVMSG %s :%s\n", dest, result);
+  }
+  dprintf(DP_HELP, "%s", tosend);
+  nfree(tosend);
+  nfree(result);
+  Context;
+}
+
+static void tell_top_word(char *channel, char *dest, char *nick, char *type, int from, int to, globstats *gs)
+{
+  locstats *e;
+  char *result, *tosend, *slang;
+  int i = 1, itype;
+
+  Context;
+  newsplit(&type);
+  strlower(type);
+  setword(gs, type);
+  sortstats(gs, T_WORDS, 1);
+  slglobint = to;
+  setslglobs(gs->chan, gs->peak[S_TODAY], 0, gs->started);
+  slang = SLTOPWORD;
+  result = nmalloc(strlen(slang) + strlen(type) + 1);
+  sprintf(result, slang, type);
+  itype = (T_WORDS * (-1)) + TOTAL_TYPES - 1;
+  for (e = gs->slocal[S_TODAY][itype]; e; e = e->snext[S_TODAY][itype]) {
+    if (!listsuser(e, channel))
+      continue;
+    if ((i > to) || !e->word)
+      break;
+    if (i >= from) {
+      result = nrealloc(result, strlen(result) + 13 + 10 + strlen(e->user) + 10 + 1);
+      sprintf(result, "%s %d. %s(%d)", result, i, e->user, e->word->nr);
+    }
+    i++;
+  }
+  if (quietstats(dest)) {
+    tosend = nmalloc(15 + strlen(nick) + strlen(result) + 1);
+    sprintf(tosend, "NOTICE %s :%s\n", nick, result);
+  } else {
+    tosend = nmalloc(16 + strlen(dest) + strlen(result) + 1);
+    sprintf(tosend, "PRIVMSG %s :%s\n", dest, result);
+  }
+  dprintf(DP_HELP, "%s", tosend);
+  nfree(result);
+  nfree(tosend);
+  Context;
+}
+
+static int pub_place(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! place %s", nick, hand, text);
+  tell_place(nick, channel, hand, channel, text, S_TOTAL);
+  Context;
+  return 1;
+}
+
+static int pub_tplace(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! tplace %s", nick, hand, text);
+  tell_place(nick, channel, hand, channel, text, S_TODAY);
+  Context;
+  return 1;
+}
+
+static void tell_place(char *nick, char *dest, char *hand, char *channel, char *text, int today)
+{
+  globstats *gs;
+  locstats *ls;
+  struct stats_memberlist *m;
+  struct stats_userlist *su;
+  struct userrec *u;
+  char *who, *slang;
+  int place = 0;
+  int itype;
+#if EGG_IS_MIN_VER(10500)
+  struct chanset_t *chan;
+#endif
+
+  Context;
+  // at first, check for flood...
+  if (stat_flood())
+    return;
+  // .. now init vars ...
+  m = NULL;
+  u = NULL;
+  su = NULL;
+  who = slang = NULL;
+  // ... get the global stat-struct for the channel ...
+  gs = findglobstats(channel);
+  // .. and init langsystem (stuff like peak and staring time
+  // shouldn't be needed)
+  setslglobs(channel, 0, countstatmembers(gs), 0);
+#if EGG_IS_MIN_VER(10500)
+  // make sure that we're going to send to the right chan (!sa23hCHAN is WRONG)
+  if (!strcasecmp(channel, dest)) {
+    chan = findchan_by_dname(dest);
+    if (chan)
+      dest = chan->name;
+  }
+#endif
+  // if there is no parameter, just default to "words"
+  if (!text[0]) {
+    itype = T_WORDS;
+  // else try resolving the type
+  } else {
+    itype = slangtypetoi(text);
+  }
+  // if we have a valid type now, then the parameter does not
+  // specify a username, so let's use the triggering user as target.
+  if (itype != T_ERROR) {
+    // use_userfile is true, so grab the handle
+    if (use_userfile) {
+      who = nmalloc(strlen(hand) + 1);
+      strcpy(who, hand);
+    } else {
+      // try grabbing the correct username from the userdatabase...
+      m = nick2suser(nick, channel);
+      if (m && m->user) {
+        who = nmalloc(strlen(m->user->user) + 1);
+        strcpy(who, m->user->user);
+      // if the user is not found, use "*", which identifies
+      // a non-existant user
+      } else {
+        who = nmalloc(2);
+        strcpy(who, "*");
+      }
+    }
+    // if who is "*", then the user isn't in any database. Serve him an error.
+    if (who[0] == '*') {
+      setslnick(nick);
+      if (quietstats(dest)) {
+        dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTYOU);
+      } else {
+        dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTYOU);
+      }
+      if (who)
+        nfree(who);
+      return;
+    }
+  } else { // itype == T_ERROR, parameter must be a username
+    itype = T_WORDS;
+    if (use_userfile) {   // grab user from egg-userfile
+      u = get_user_by_handle(userlist, text);
+      if (u) {
+        who = nmalloc(strlen(u->handle) + 1);
+        strcpy(who, u->handle);
+      } else {
+        // oops, user not found!
+        setslnick(text);
+        if (quietstats(dest)) {
+          dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+        } else {
+          dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+        }
+        // no need to free anything, we didn't allocate anything, yet
+        Assert(!who);
+        return;
+      }
+    } else {  // grab user from stats-userbase
+      // first check if nick is on chan (faster)
+      m = nick2suser(text, channel);
+      if (m && m->user) {
+        who = nmalloc(strlen(m->user->user) + 1);
+        strcpy(who, m->user->user);
+      } else {
+        // no success, yet? ok, then search through the complete database...
+        su = findsuser_by_name(text);
+        if (su) {
+          who = nmalloc(strlen(su->user) + 1);
+          strcpy(who, su->user);
+        } else {
+          // still no success? Poor user... serve him an error.
+          setslnick(text);
+          if (quietstats(dest)) {
+            dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+          } else {
+            dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+          }
+          Assert(!who);
+          return;
+        }
+      }
+    }
+  }
+  if (!gs) {
+    Assert(who);
+    nfree(who);
+    debug1("Stats.mod: Couldn't exec !place, I don't have any statistics in %s.", channel);
+    return;
+  }
+  // sort the stats
+  sortstats(gs, itype, today);
+  // if itype is < 0, get the modified itype that we need for accessing the data
+  if (itype < 0)
+    itype = (TOTAL_TYPES - 1) + (itype * -1);
+  // now calculate the place
+  for (ls = gs->slocal[today][itype]; ls; ls = ls->snext[today][itype]) {
+    if (!listsuser(ls, channel))
+      continue;
+    place++;
+    if (!rfc_casecmp(who, ls->user))
+      break;
+  }
+  // if ls is NULL now, then the user doesn't have any stats in this chan
+  if (!ls) {
+    setslnick(who);
+    if (quietstats(dest)) {
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+    } else {
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+    }
+  } else {
+    // stats found, so let's output them
+    setslnick(who);
+    slglobint = place;
+    // there are 4 slang-entries for today, this week, etc
+    slang = getslang(1012 + today);
+    if (quietstats(dest)) {
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, slang);
+    } else {
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, slang);
+    }
+  }
+  Assert(who);
+  nfree(who);
+  Context;
+}
+
+static int pub_stat(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! stat %s", nick, hand, text);
+  tell_stat(nick, channel, hand, channel, text, S_TOTAL);
+  Context;
+  return 1;
+}
+
+static int pub_tstat(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! tstat %s", nick, hand, text);
+  tell_stat(nick, channel, hand, channel, text, S_TODAY);
+  Context;
+  return 1;
+}
+
+static void tell_stat(char *nick, char *dest, char *hand, char *channel, char *text, int today)
+{
+  locstats *ls;
+  char *who, *tosend, what[128], *pwhat, *type, *dur, *stmp;
+  int itype, first;
+  struct stats_memberlist *m;
+  struct userrec *u;
+  struct stats_userlist *su;
+#if EGG_IS_MIN_VER(10500)
+  struct chanset_t *chan;
+#endif
+
+  Context;
+  if (stat_flood())
+    return;
+  who = tosend = type = dur = NULL;
+  setslglobs(channel, 0, 0, 0);
+#if EGG_IS_MIN_VER(10500)
+  if (!strcasecmp(channel, dest)) {
+    chan = findchan_by_dname(dest);
+    if (chan)
+      dest = chan->name;
+  }
+#endif
+  if (text[0] == 0) {
+    if (use_userfile) {
+      who = nmalloc(strlen(hand) + 1);
+      strcpy(who, hand);
+    } else {
+      m = nick2suser(nick, channel);
+      if (m && m->user) {
+        who = nmalloc(strlen(m->user->user) + 1);
+        strcpy(who, m->user->user);
+      } else {
+        who = nmalloc(2);
+        strcpy(who, "*");
+      }
+    }
+  } else {
+    if (use_userfile) {   // grab user from egg-userfile
+      u = get_user_by_handle(userlist, text);
+      if (u) {
+        who = nmalloc(strlen(u->handle) + 1);
+        strcpy(who, u->handle);
+      } else {
+        // oops, user not found!
+        setslnick(text);
+        if (quietstats(dest)) {
+          dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+        } else {
+          dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+        }
+        // no need to free anything, we didn't allocate anything, yet
+        Assert(!who);
+        return;
+      }
+    } else {  // grab user from stats-userbase
+      // first check if nick is on chan (faster)
+      m = nick2suser(text, channel);
+      if (m && m->user) {
+        who = nmalloc(strlen(m->user->user) + 1);
+        strcpy(who, m->user->user);
+      } else {
+        // no success, yet? ok, then search through the complete database...
+        su = findsuser_by_name(text);
+        if (su) {
+          who = nmalloc(strlen(su->user) + 1);
+          strcpy(who, su->user);
+        } else {
+          // still no success? Poor user... serve him an error.
+          setslnick(text);
+          if (quietstats(dest)) {
+            dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+          } else {
+            dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+          }
+          Assert(!who);
+          return;
+        }
+      }
+    }
+  }
+  setslglobs(channel, 0, 0, 0);
+  if (who[0] == '*') {
+    setslnick(nick);
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTYOU);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTYOU);
+    nfree(who);
+    return;
+  }
+  ls = findlocstats(channel, who);
+  if (!ls) {
+    setslnick(who);
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+  } else {
+    if (quietstats(dest)) {
+      tosend = nmalloc(14 + strlen(nick) + strlen(who) + 1);
+      sprintf(tosend, "NOTICE %s :%s:", nick, who);
+    } else {
+      tosend = nmalloc(15 + strlen(dest) + strlen(who) + 1);
+      sprintf(tosend, "PRIVMSG %s :%s:", dest, who);
+    }
+    what[0] = 0;
+    pwhat = what;
+    strncpy(pwhat, stat_reply, 127);
+    pwhat[127] = 0;
+    first = 1;
+    while (strlen(pwhat) > 0) {
+      type = newsplit(&pwhat);
+      itype = typetoi(type);
+      if ((itype < 0) && (itype != T_WPL) && (itype != T_IDLE))
+        continue;
+      if (!first) {
+        // if this isn't the first run, attach a "," to the string to
+        // seperate the values.
+        tosend = nrealloc(tosend, strlen(tosend) + 1 + 1);
+        strcat(tosend, ",");
+      } else {
+        first = 0;
+      }
+      if (itype == T_MINUTES) {
+        dur = stats_duration(ls->values[today][T_MINUTES] * 60);
+        stmp = getslangtype("minutes");
+        tosend = nrealloc(tosend, strlen(tosend) + 3 + strlen(stmp) + strlen(dur) + 1);
+        sprintf(tosend, "%s %s: %s", tosend, stmp, dur);
+      } else if (itype >= 0) {
+        // same as usual: use 10 bytes for the integer-string
+        stmp = getslangtype(type);
+        tosend = nrealloc(tosend, 8 + strlen(tosend) + 10 + strlen(stmp));
+        sprintf(tosend, "%s %ld %s", tosend, ls->values[today][itype], stmp);
+      } else if ((itype == T_WPL) && (ls->values[today][T_LINES] != 0)) {
+        stmp = getslangtype("wpl");
+        tosend = nrealloc(tosend, strlen(tosend) + 2 + 10 + strlen(stmp) + 1);
+        sprintf(tosend, "%s %.2f %s", tosend, (float) ls->values[today][T_WORDS] / (float) ls->values[today][T_LINES], stmp);
+      } else if ((itype == T_IDLE) && (ls->values[today][T_LINES] != 0)) {
+        stmp = getslangtype("idle");
+        tosend = nrealloc(tosend, strlen(tosend) + 2 + 10 + strlen(stmp) + 1);
+        sprintf(tosend, "%s %.2f %s", tosend, (float) ls->values[today][T_MINUTES] / (float) ls->values[today][T_LINES], stmp);
+      }
+    }
+    dprintf(DP_HELP, "%s.\n", tosend);
+    nfree(tosend);
+  }
+  nfree(who);
+  Context;
+}
+
+static int pub_wordstats(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! wordstats %s", nick, hand, text);
+  tell_wordstats(nick, channel, hand, channel, text);
+  Context;
+  return 0;
+}
+
+static void tell_wordstats(char *nick, char *dest, char *hand, char *channel, char *text)
+{
+  locstats *ls;
+  wordstats *ws;
+  char *who, *tosend, *slang;
+  int i;
+#if EGG_IS_MIN_VER(10500)
+  struct chanset_t *chan;
+#endif
+
+
+  Context;
+  filt(text);
+  if (stat_flood())
+    return;
+#if EGG_IS_MIN_VER(10500)
+  if (!strcasecmp(channel, dest)) {
+    chan = findchan_by_dname(dest);
+    if (chan)
+      dest = chan->name;
+  }
+#endif
+  if (text[0] == 0) {
+    who = nmalloc(strlen(hand) + 1);
+    strcpy(who, hand);
+  } else {
+    who = nmalloc(strlen(text) + 1);
+    strcpy(who, text);
+  }
+  setslglobs(channel, 0, 0, 0);
+  if (who[0] == '*') {
+    setslnick(nick);
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTYOU);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTYOU);
+    nfree(who);
+    return;
+  }
+  ls = findlocstats(channel, who);
+  if (!ls) {
+    setslnick(who);
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOSTATSABOUTSOMEONE);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOSTATSABOUTSOMEONE);
+  } else {
+    slgloblocstats = ls;
+    if (!ls->words) {
+      setslnick(who);
+      if (quietstats(dest))
+        dprintf(DP_HELP, "NOTICE %s :%s\n", nick, SLNOWORDSTATS);
+      else
+        dprintf(DP_HELP, "PRIVMSG %s :%s\n", dest, SLNOWORDSTATS);
+    } else {
+      slang = SLUSERSMOSTUSEDWORDS;
+      if (quietstats(dest)) {
+        tosend = nmalloc(13 + strlen(nick) + strlen(slang) + 1);
+        sprintf(tosend, "NOTICE %s :%s", nick, slang);
+      } else {
+        tosend = nmalloc(14 + strlen(dest) + strlen(slang) + 1);
+        sprintf(tosend, "PRIVMSG %s :%s", dest, slang);
+      }
+      i = 0;
+      sortwordstats(ls, NULL);
+      ws = ls->words;
+      while (ws && (i < 10)) {
+        i++;
+        tosend = nrealloc(tosend, 14 + strlen(tosend) + 5 + strlen(ws->word) + 10 + 1);
+        sprintf(tosend, "%s %d. %s (%d)", tosend, i, ws->word, ws->nr);
+        ws = ws->next;
+      }
+      dprintf(DP_HELP, "%s\n", tosend);
+      nfree(tosend);
+    }
+  }
+  nfree(who);
+  Context;
+}
+
+static int pub_topwords(char *nick, char *host, char *hand,
+        char *channel, char *text)
+{
+  Context;
+  if (nopubstats(channel))
+    return 1;
+  putlog(LOG_CMDS, channel, "<<%s>> !%s! topwords %s", nick, hand, text);
+  tell_topwords(nick, channel, hand, channel);
+  Context;
+  return 0;
+}
+
+static void tell_topwords(char *nick, char *dest, char *hand, char *channel)
+{
+  globstats *gs;
+  wordstats *ws;
+  char *tosend, *slang;
+  int i;
+#if EGG_IS_MIN_VER(10500)
+  struct chanset_t *chan;
+#endif
+
+  Context;
+  if (stat_flood())
+    return;
+#if EGG_IS_MIN_VER(10500)
+  if (!strcasecmp(channel, dest)) {
+    chan = findchan_by_dname(dest);
+    if (chan)
+      dest = chan->name;
+  }
+#endif
+  gs = findglobstats(channel);
+  if (!gs) {
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :I don't have any stats in %s\n", nick, channel);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :%s, I don't have any stats in %s\n", dest, nick, channel);
+    return;
+  }
+  do_globwordstats(gs);
+  ws = gs->words;
+  if (!ws) {
+    if (quietstats(dest))
+      dprintf(DP_HELP, "NOTICE %s :I don't have any wordstats in %s\n", nick, channel);
+    else
+      dprintf(DP_HELP, "PRIVMSG %s :I don't have any wordstats in %s\n", dest, channel);
+    return;
+  }
+  setslglobs(gs->chan, gs->peak[S_TODAY], 0, gs->started);
+  setslnick(nick);
+  slang = SLCHANSMOSTUSEDWORDS;
+  if (quietstats(dest)) {
+    tosend = nmalloc(13 + strlen(nick) + strlen(slang) + 1);
+    sprintf(tosend, "NOTICE %s :%s", nick, slang);
+  } else {
+    tosend = nmalloc(14 + strlen(dest) + strlen(slang) + 1);
+    sprintf(tosend, "PRIVMSG %s :%s", dest, slang);
+  }
+  i = 0;
+  while (ws && (i < 10)) {
+    i++;
+    tosend = nrealloc(tosend, 14 + strlen(tosend) + 5 + strlen(ws->word) + 10);
+    sprintf(tosend, "%s %d. %s (%d)", tosend, i, ws->word, ws->nr);
+    ws = ws->next;
+  }
+  dprintf(DP_HELP, "%s\n", tosend);
+  nfree(tosend);
+  Context;
+}
+
+static int stat_flood()
+{
+  if (!maxstat_thr || !maxstat_time)
+    return 0;
+  if ((now - mstat_time) > maxstat_time) {
+    mstat_time = now;
+    mstat_thr = 0;
+  }
+  mstat_thr++;
+  if (mstat_thr > maxstat_thr)
+    return 1;
+  return 0;
+}
+
+static cmd_t stats_pub[] =
+{
+  {"!top10", "", pub_top10, 0},
+  {"!ttop10", "", pub_ttop10, 0},
+  {"!top20", "", pub_top20, 0},
+  {"!ttop20", "", pub_ttop20, 0},
+  {"!place", "", pub_place, 0},
+  {"!tplace", "", pub_tplace, 0},
+  {"!stat", "", pub_stat, 0},
+  {"!tstat", "", pub_tstat, 0},
+  {"!wordstats", "", pub_wordstats, 0},
+  {"!topwords", "", pub_topwords, 0},
+  {0, 0, 0, 0}
+};

+ 499 - 0
sensors.c

@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int sensor_msgm(char *nick, char *uhost, char *hand, char *chan, char *rest)
+{
+  int i;
+  char buf[511];
+  struct stats_memberlist *m;
+  globstats *gs;
+
+  if (nostats(chan))
+    return 0;
+  strncpy(buf, rest, 510);
+  buf[510] = 0;
+  rest = buf;
+  gs = findglobstats(chan);
+  add_chanlog(gs, nick, rest, SL_PRIVMSG);
+  m = nick2suser(nick, chan);
+  if(!use_userfile) {
+    if (!m) {
+      putlog(LOG_LEV5, chan, "!m (%s)", nick);
+      check_for_url(nick, chan, rest);
+      return 0;
+    }
+    if (!m->user) {
+      putlog(LOG_LEV5, chan, "!m->user (%s)", nick);
+      check_for_url(nick, chan, rest);
+      return 0;
+    }
+    check_for_url(m->user->user, chan, rest);
+    // if there's no link to the stats, call initstats() which either
+    // returns an existing stats struct, or initializes a new one
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_WORDS, countwords(rest));
+    nincrstats(m->stats, T_LETTERS, strlen(rest));
+    nincrstats(m->stats, T_LINES, 1);
+    i = countsmileys(rest);
+    if (i)
+      nincrstats(m->stats, T_SMILEYS, i);
+    i = countquestions(rest);
+    if (i)
+      nincrstats(m->stats, T_QUESTIONS, i);
+    addquote(hand, gs, rest, m->stats);
+    calcwordstats(hand, gs, rest, m->stats);
+  } else {
+    if (hand[0] == '*') {
+      putlog(LOG_LEV5, chan, "hand[0] == '*' (%s)", nick);
+      check_for_url(nick, chan, rest);
+      return 0;
+    }
+    check_for_url(hand, chan, rest);
+    incrstats(hand, chan, T_WORDS, countwords(rest), 0);
+    incrstats(hand, chan, T_LETTERS, strlen(rest), 0);
+    incrstats(hand, chan, T_LINES, 1, 0);
+    i = countsmileys(rest);
+    if (i)
+      incrstats(hand, chan, T_SMILEYS, i, 0);
+    i = countquestions(rest);
+    if (i)
+      incrstats(hand, chan, T_QUESTIONS, i, 0);
+    addquote(hand, gs, rest, NULL);
+    calcwordstats(hand, gs, rest, NULL);
+  }
+  return 0;
+}
+
+static void sensor_minutely()
+{
+  struct chanset_t *chan;
+  memberlist *m;
+  struct stats_chanset *ch;
+  struct stats_memberlist *mm;
+
+  Context;
+  if (use_userfile) {
+    for (chan = chanset; chan; chan = chan->next) {
+#ifndef OLDBOT
+      if (nostats(chan->dname))
+#else
+      if (nostats(chan->name))
+#endif
+        continue;
+      if (chan->channel.members > 0) {
+        for (m = chan->channel.member; m; m = m->next) {
+#ifndef OLDBOT
+          if (m->user != NULL)
+            incrstats(m->user->handle, chan->dname, T_MINUTES, 1, 0);
+          else
+            stats_autoadd(m, chan->dname);
+#else
+          if (m->user != NULL)
+            incrstats(m->user->handle, chan->name, T_MINUTES, 1, 0);
+          else
+            stats_autoadd(m, chan->name);
+#endif
+        }
+      }
+    }
+  } else {
+    for (ch = schans; ch; ch = ch->next) {
+      if (nostats(ch->chan))
+        continue;
+      for (mm = ch->members; mm; mm = mm->next) {
+        if (mm->user) {
+          if (!mm->stats)
+            mm->stats = initstats(ch->chan, mm->user->user);
+          // now let's use a generic flag to check if we already increased this
+          // user's minutes (if he's a clone, for example)
+          // (thanks to Zev for this idea)
+          if (mm->stats->flag)
+            continue;
+          nincrstats(mm->stats, T_MINUTES, 1);
+          mm->stats->flag = 1;
+        } else {
+          stats_autosadd(mm, ch);
+        }
+      }
+      // now reset the flag
+      for (mm = ch->members; mm; mm = mm->next)
+        if (mm->stats)
+          mm->stats->flag = 0;
+    }
+  }
+  Context;
+}
+
+static void sensor_countusers()
+{
+  struct chanset_t *chan;
+  memberlist *m;
+  struct stats_chanset *ch;
+  struct stats_memberlist *mm;
+  globstats *gs;
+  int hour, nr;
+
+  for (chan = chanset; chan; chan = chan->next) {
+    if (chan->channel.members < 1)
+      continue;
+    nr = 0;
+#if EGG_IS_MIN_VER(10500)
+    if (nostats(chan->name))
+      continue;
+    gs = findglobstats(chan->dname);
+#else
+    if (nostats(chan->name))
+      continue;
+    gs = findglobstats(chan->name);
+#endif
+    if (!gs)
+      continue;
+    if (use_userfile) {
+      for (m = chan->channel.member; m; m = m->next) {
+        if (!m->nick[0])
+          continue;
+        if (m->user != NULL)
+#if EGG_IS_MIN_VER(10500)
+          if (matchattr(m->user, nostatsflags, chan->dname))
+#else
+          if (matchattr(m->user, nostatsflags, chan->name))
+#endif
+            continue;
+        nr++;
+      }
+    } else {
+#if EGG_IS_MIN_VER(10500)
+      ch = findschan(chan->dname);
+#else
+      ch = findschan(chan->name);
+#endif
+      if (!ch)
+        continue;
+      for (mm = ch->members; mm; mm = mm->next) {
+	if (mm->user && !mm->user->list)
+	  continue;
+	nr++;
+      }
+    }
+    hour = gethour();
+    if (hour != lasthour) {
+      gs->users[S_USERSUM][hour] = nr;
+      gs->users[S_USERCOUNTS][hour] = 1;
+      lasthour = hour;
+    } else {
+      gs->users[S_USERSUM][hour] += nr;
+      if (gs->users[S_USERCOUNTS][hour] < 0)
+        gs->users[S_USERCOUNTS][hour] = 1;
+      else
+        gs->users[S_USERCOUNTS][hour]++;
+    }
+  }
+}
+
+static void sensor_peak(char *channel)
+{
+  struct chanset_t *chan;
+  memberlist *m;
+  struct stats_chanset *ch;
+  struct stats_memberlist *mm;
+  globstats *gs;
+  int users = 0;
+
+  if (nostats(channel))
+    return;
+  gs = findglobstats(channel);
+  if (!gs)
+    return;
+#ifndef OLDBOT
+  chan = findchan_by_dname(channel);
+#else
+  chan = findchan(channel);
+#endif
+  if (!chan)
+    return;
+  if (use_userfile) {
+    if (chan->channel.members > 0) {
+      for (m = chan->channel.member; m; m = m->next) {
+        if (!m->nick[0])
+          continue;
+        if (m->user) {
+          if (matchattr(m->user, nopeak, channel))
+            continue;
+        }
+        users++;
+      }
+    }
+  } else {
+#if EGG_IS_MIN_VER(10500)
+    ch = findschan(chan->dname);
+#else
+    ch = findschan(chan->name);
+#endif
+    if (!ch)
+      return;
+    for (mm = ch->members; mm; mm = mm->next) {
+      if (mm->user && !mm->user->list)
+	continue;
+      users++;
+    }
+  }
+  if (users > gs->peak[S_TOTAL]) {
+    gs->peak[S_TOTAL] = users;
+    putlog(LOG_MISC, "*", "New user peak in %s: %d.", channel, users);
+  }
+  if (users > gs->peak[S_TODAY])
+    gs->peak[S_TODAY] = users;
+  if (users > gs->peak[S_WEEKLY])
+    gs->peak[S_WEEKLY] = users;
+  if (users > gs->peak[S_MONTHLY])
+    gs->peak[S_MONTHLY] = users;
+
+}
+
+static int sensor_topc(char *nick, char *uhost, char *hand, char *chan, char *topic)
+{
+  struct stats_memberlist *m;
+
+  Context;
+  if (nostats(chan))
+    return 0;
+  if (!use_userfile) {
+    m = nick2suser(nick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_TOPICS, 1);
+  } else if (hand[0] != '*')
+    incrstats(hand, chan, T_TOPICS, 1, 0);
+  addtopic(chan, topic, nick);
+  return 0;
+}
+
+static int sensor_action(char *nick, char *uhost, char *hand, char *chan, char *key, char *rest)
+{
+  char *pbuf;
+  struct stats_memberlist *m;
+
+  if (!strchr(CHANMETA, chan[0]))
+    return 0;
+  if (nostats(chan))
+    return 0;
+  if (!use_userfile) {
+    m = nick2suser(nick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_ACTIONS, 1);
+  } else if (hand[0] != '*')
+    incrstats(hand, chan, T_ACTIONS, 1, 0);
+  pbuf = nmalloc(strlen(nick) + strlen(rest) + 2);
+  sprintf(pbuf, "%s %s", nick, rest);
+  sensor_msgm(nick, uhost, hand, chan, pbuf);
+  nfree(pbuf);
+  return 0;
+}
+
+static int sensor_kick(char *nick, char *uhost, char *hand, char *chan, char *victim, char *reason)
+{
+  struct stats_memberlist *m;
+  char *buf;
+  globstats *gs;
+
+  if (nostats(chan))
+    return 0;
+  gs = findglobstats(chan);
+  buf = nmalloc(strlen(victim) + strlen(nick) + strlen(reason) + 23);
+  sprintf(buf, "*** %s was kicked by %s (%s)", victim, nick, reason);
+  add_chanlog(gs, nick, buf, SL_KICK);
+  save_kick(gs, buf);
+  nfree(buf);
+  skillmember(chan, victim);
+  if (!use_userfile) {
+    m = nick2suser(nick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_KICKS, 1);
+  } else if (hand[0] != '*')
+    incrstats(hand, chan, T_KICKS, 1, 0);
+  return 0;
+}
+
+static int sensor_mode(char *nick, char *uhost, char *hand, char *chan, char *mode, char *victim)
+{
+  struct stats_memberlist *m;
+  char *buf;
+
+  if (nostats(chan))
+    return 0;
+  Assert(mode);
+  if (mode[1] == 'k')
+  	return 0;
+  buf = nmalloc(strlen(nick) + strlen(mode) + strlen(victim) + 13);
+  sprintf(buf, "%s sets mode %s %s", nick, mode, victim);
+  add_chanlog(findglobstats(chan), nick, buf, SL_MODE);
+  nfree(buf);
+  if (!use_userfile) {
+    m = nick2suser(nick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_MODES, 1);
+    if ((mode[1] == 'b') && (mode[0] == '+'))
+      nincrstats(m->stats, T_BANS, 1);
+  } else if (hand[0] != '*') {
+    incrstats(hand, chan, T_MODES, 1, 0);
+    if ((mode[1] == 'b') && (mode[0] == '+'))
+      incrstats(hand, chan, T_BANS, 1, 0);
+  }
+  return 0;
+}
+
+static int sensor_nick(char *nick, char *uhost, char *hand, char *chan, char *newnick)
+{
+  struct stats_memberlist *m;
+
+  if (nostats(chan))
+    return 0;
+  add_chanlog(findglobstats(chan), nick, newnick, SL_NICK);
+  strackmember(chan, nick, newnick);
+  if (!use_userfile) {
+    m = nick2suser(newnick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_NICKS, 1);
+  } else if (hand[0] != '*')
+    incrstats(hand, chan, T_NICKS, 1, 0);
+  return 0;
+}
+
+static int sensor_join(char *nick, char *uhost, char *hand, char *chan)
+{
+  struct stats_memberlist *m;
+
+  if (nostats(chan))
+    return 0;
+  add_chanlog(findglobstats(chan), nick, NULL, SL_JOIN);
+  if (match_my_nick(nick))
+    free_one_chan(chan);
+  else
+    saddmember(nick, uhost, hand, chan);
+  sensor_peak(chan);
+  if (!use_userfile) {
+    m = nick2suser(nick, chan);
+    if (!m)
+      return 0;
+    if (!m->user)
+      return 0;
+    if (!m->stats)
+      m->stats = initstats(chan, m->user->user);
+    nincrstats(m->stats, T_JOINS, 1);
+  } else if (hand[0] != '*')
+    incrstats(hand, chan, T_JOINS, 1, 0);
+  addhost(uhost, findglobstats(chan));
+  return 0;
+}
+
+static int sensor_part(char *nick, char *uhost, char *hand, char *chan)
+{
+  if (nostats(chan))
+    return 0;
+  add_chanlog(findglobstats(chan), nick, NULL, SL_PART);
+  skillmember(chan, nick);
+  return 0;
+}
+
+static int sensor_sign(char *nick, char *uhost, char *hand, char *chan, char *reason)
+{
+  if (nostats(chan))
+    return 0;
+  add_chanlog(findglobstats(chan), nick, reason, SL_QUIT);
+  skillmember(chan, nick);
+  return 0;
+}
+
+static cmd_t stats_pubm[] =
+{
+  {"*", "", (Function) sensor_msgm, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_topc[] =
+{
+  {"*", "", (Function) sensor_topc, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_ctcp[] =
+{
+  {"ACTION", "", (Function) sensor_action, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_kick[] =
+{
+  {"*", "", (Function) sensor_kick, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_mode[] =
+{
+  {"*", "", (Function) sensor_mode, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_nick[] =
+{
+  {"*", "", (Function) sensor_nick, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_join[] =
+{
+  {"*", "", (Function) sensor_join, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_part[] =
+{
+  {"*", "", (Function) sensor_part, "stat"},
+  {0, 0, 0, 0}
+};
+
+static cmd_t stats_sign[] =
+{
+  {"*", "", (Function) sensor_sign, "stat"},
+  {0, 0, 0, 0}
+};

+ 842 - 0
slang.c

@@ -0,0 +1,842 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static char *dynslang = NULL;
+static char *filtbracketsstr = NULL;
+static char *slglobchan = NULL;
+static char *slgloblang = NULL;
+static char *slglobtype = NULL;
+static char *slglobnick = NULL;
+static int slglobpeak, slglobtotalusers, slglobint;
+static time_t slglobgstarted;
+static locstats *slgloblocstats;
+
+static int slang_expmem()
+{
+  int size = 0;
+  struct slang_lang *l;
+  struct slang_ids *i;
+  struct slang_types *ty;
+  struct slang_texts *t;
+  struct slang_chan *ch;
+  struct slang_bntypes *bt;
+  struct slang_bnplaces *bp;
+
+  Context;
+  for (l = slangs; l; l = l->next) {
+    size += sizeof(struct slang_lang);
+    if (l->lang)
+      size += strlen(l->lang) + 1;
+    for (i = l->ids; i; i = i->next) {
+      size += sizeof(struct slang_ids);
+      for (t = i->texts; t; t = t->next) {
+	size += sizeof(struct slang_texts);
+	size += strlen(t->text) + 1;
+      }
+    }
+    for (ty = l->types; ty; ty = ty->next) {
+      size += sizeof(struct slang_types);
+      size += strlen(ty->type) + 1;
+      for (t = ty->texts; t; t = t->next) {
+	size += sizeof(struct slang_texts);
+	size += strlen(t->text) + 1;
+      }
+    }
+    Context;
+    for (bt = l->bignumbers; bt; bt = bt->next) {
+      size += sizeof(struct slang_bntypes);
+      size += strlen(bt->type) + 1;
+      for (bp = bt->places; bp; bp = bp->next) {
+	size += sizeof(struct slang_bnplaces);
+        for (t = bp->texts; t; t = t->next) {
+	  size += sizeof(struct slang_texts);
+	  size += strlen(t->text) + 1;
+        }
+      }
+    }
+  }
+  Context;
+  for (ch = slangchans; ch; ch = ch->next) {
+    size += sizeof(struct slang_chan);
+    size += strlen(ch->chan) + 1;
+    if (ch->lang)
+      size += strlen(ch->lang) + 1;
+  }
+  if (dynslang)
+    size += strlen(dynslang) + 1;
+  if (slglobchan)
+    size += strlen(slglobchan) + 1;
+  if (slgloblang)
+    size += strlen(slgloblang) + 1;
+  if (slglobnick)
+    size += strlen(slglobnick) + 1;
+  if (filtbracketsstr)
+      size += strlen(filtbracketsstr) + 1;
+  return size;
+}
+
+static void free_slang()
+{
+  struct slang_lang *l, *ll;
+  struct slang_ids *i, *ii;
+  struct slang_texts *t, *tt;
+  struct slang_types *ty, *tty;
+  struct slang_chan *ch, *cch;
+  struct slang_bntypes *bt, *btt;
+  struct slang_bnplaces *bp, *bpp;
+
+  Context;
+  l = slangs;
+  while (l) {
+    l->durs[0] = l->durs[1] = l->durs[2] = l->durs[3] = l->durs[4] = NULL;
+    l->durs[5] = l->durs[6] = l->durs[7] = l->durs[8] = l->durs[9] = NULL;
+    i = l->ids;
+    while (i) {
+      t = i->texts;
+      while (t) {
+        tt = t->next;
+        nfree(t->text);
+        nfree(t);
+        t = tt;
+      }
+      ii = i->next;
+      nfree(i);
+      i = ii;
+    }
+    ty = l->types;
+    while (ty) {
+      t = ty->texts;
+      while (t) {
+        tt = t->next;
+        nfree(t->text);
+        nfree(t);
+        t = tt;
+      }
+      tty = ty->next;
+      nfree(ty->type);
+      nfree(ty);
+      ty = tty;
+    }
+    Context;
+    bt = l->bignumbers;
+    while (bt) {
+      bp = bt->places;
+      while (bp) {
+        t = bp->texts;
+        while (t) {
+          tt = t->next;
+          nfree(t->text);
+          nfree(t);
+          t = tt;
+        }
+        bpp = bp->next;
+        nfree(bp);
+        bp = bpp;
+      }
+      btt = bt->next;
+      nfree(bt->type);
+      nfree(bt);
+      bt = btt;
+    }
+    Context;
+    ll = l->next;
+    if (l->lang)
+      nfree(l->lang);
+    nfree(l);
+    l = ll;
+  }
+  Context;
+  ch = slangchans;
+  while (ch) {
+    cch = ch->next;
+    nfree(ch->chan);
+    if (ch->lang)
+      nfree(ch->lang);
+    nfree(ch);
+    ch = cch;
+  }
+  if (slglobchan)
+    nfree(slglobchan);
+  if (slgloblang)
+    nfree(slgloblang);
+  if (dynslang)
+    nfree(dynslang);
+  if (slglobnick)
+    nfree(slglobnick);
+  if (filtbracketsstr)
+      nfree(filtbracketsstr);
+  slglobchan = slgloblang = dynslang = slglobnick = filtbracketsstr = NULL;
+  Context;
+}
+
+static int loadslang(char *lang, char *file)
+{
+  FILE *f;
+  char *s, buf[MAXSLANGLENGTH], *id;
+  struct slang_lang *l, *nl;
+
+  Context;
+  if (lang)
+    putlog(LOG_MISC, "*", "Loading slangfile for %s: %s", lang, file);
+  else
+    putlog(LOG_MISC, "*", "Loading default slangfile: %s", file);
+  f = fopen(file, "r");
+  if (f == NULL) {
+    putlog(LOG_MISC, "*", "ERROR reading slangfile");
+    return 0;
+  }
+  l = slangs;
+  while (l) {
+    if (!lang && !l->lang)
+      break;
+    if (lang && l->lang)
+      if (!strcasecmp(lang, l->lang))
+        break;
+    l = l->next;
+  }
+  if (!l) {
+    l = slangs;
+    while (l && l->next)
+      l = l->next;
+    nl = nmalloc(sizeof(struct slang_lang));
+    if (!lang) {
+      nl->lang = NULL;
+    } else {
+      nl->lang = nmalloc(strlen(lang) + 1);
+      strcpy(nl->lang, lang);
+    }
+    nl->next = NULL;
+    nl->ids = NULL;
+    nl->types = NULL;
+    nl->bignumbers = NULL;
+    nl->durs[0] = nl->durs[1] = nl->durs[2] = nl->durs[3] = nl->durs[4] = NULL;
+    nl->durs[5] = nl->durs[6] = nl->durs[7] = nl->durs[8] = nl->durs[9] = NULL;
+    if (l)
+      l->next = nl;
+    else
+      slangs = nl;
+    l = nl;
+  }
+  while (!feof(f)) {
+    buf[0] = 0;
+    s = buf;
+    fgets(s, MAXSLANGLENGTH - 1, f);
+    s[MAXSLANGLENGTH - 1] = 0;
+    if (!s[0])
+      continue;
+    if ((s[strlen(s) - 1] == '\n') || (s[strlen(s) - 1] == '\r'))
+      s[strlen(s) - 1] = 0;
+    if ((s[strlen(s) - 1] == '\n') || (s[strlen(s) - 1] == '\r'))
+      s[strlen(s) - 1] = 0;
+    id = newsplit(&s);
+    if (!strcasecmp(id, "T"))
+      addslangtype(l, s);
+    else if (!strcasecmp(id, "BN"))
+      addslangbn(l, s);
+    else
+      addslangitem(l, atoi(id), s);
+  }
+  fclose(f);
+  Context;
+  return 1;
+}
+
+static void addslangitem(struct slang_lang *lang, int idnr, char *text)
+{
+  struct slang_ids *id, *nid;
+  struct slang_texts *t, *nt;
+
+  Context;
+  if (idnr == 0)
+    return;
+  for (id = lang->ids; id; id = id->next)
+    if (id->id == idnr)
+      break;
+  if (!id) {
+    id = lang->ids;
+    while (id && id->next)
+      id = id->next;
+    nid = nmalloc(sizeof(struct slang_ids));
+    nid->id = idnr;
+    nid->entries = 0;
+    nid->texts = NULL;
+    nid->next = NULL;
+    if (id)
+      id->next = nid;
+    else
+      lang->ids = nid;
+    id = nid;
+  }
+  t = id->texts;
+  while (t && t->next)
+    t = t->next;
+  nt = nmalloc(sizeof(struct slang_texts));
+  nt->text = nmalloc(strlen(text) + 1);
+  strcpy(nt->text, text);
+  nt->dynamic = isdynamicslang(text);
+  nt->next = NULL;
+  if (t)
+    t->next = nt;
+  else
+    id->texts = nt;
+  id->entries++;
+  if ((idnr >= 250) && (idnr < 260))
+    lang->durs[idnr - 250] = nt->text;
+}
+
+static void addslangtype(struct slang_lang *lang, char *text)
+{
+  char *type;
+  struct slang_types *ty, *nty;
+  struct slang_texts *t, *nt;
+
+  Context;
+  type = newsplit(&text);
+  strlower(type);
+  if (!text[0])
+    return;
+  for (ty = lang->types; ty; ty = ty->next)
+    if (!strcmp(ty->type, type))
+      break;
+  if (!ty) {
+    for (ty = lang->types; ty && ty->next; ty = ty->next);
+    nty = nmalloc(sizeof(struct slang_types));
+    nty->type = nmalloc(strlen(type) + 1);
+    strcpy(nty->type, type);
+    nty->entries = 0;
+    nty->next = NULL;
+    nty->texts = NULL;
+    if (ty)
+      ty->next = nty;
+    else
+      lang->types = nty;
+    ty = nty;
+  }
+  for (t = ty->texts; t && t->next; t = t->next);
+  nt = nmalloc(sizeof(struct slang_texts));
+  nt->text = nmalloc(strlen(text) + 1);
+  strcpy(nt->text, text);
+  nt->dynamic = isdynamicslang(text);
+  nt->next = NULL;
+  if (t)
+    t->next = nt;
+  else
+    ty->texts = nt;
+  ty->entries++;
+}
+
+static void addslangbn(struct slang_lang *lang, char *text)
+{
+  char *type, *splace;
+  struct slang_bntypes *ty, *nty;
+  struct slang_bnplaces *p, *np;
+  struct slang_texts *t, *nt;
+  int place;
+
+  Context;
+  type = newsplit(&text);
+  strlower(type);
+  splace = newsplit(&text);
+  place = atoi(splace);
+  if (!place) {
+    debug2("place for %s is %d", type, place);
+    return;
+  }
+  if (!text[0])
+    return;
+  for (ty = lang->bignumbers; ty; ty = ty->next)
+    if (!strcmp(ty->type, type))
+      break;
+  if (!ty) {
+    for (ty = lang->bignumbers; ty && ty->next; ty = ty->next);
+    nty = nmalloc(sizeof(struct slang_bntypes));
+    nty->type = nmalloc(strlen(type) + 1);
+    strcpy(nty->type, type);
+    nty->next = NULL;
+    nty->places = NULL;
+    if (ty)
+      ty->next = nty;
+    else
+      lang->bignumbers = nty;
+    ty = nty;
+  }
+  for (p = ty->places; p; p = p->next)
+    if (p->place == place)
+      return;
+  if (!p) {
+    for (p = ty->places; p && p->next; p = p->next);
+    np = nmalloc(sizeof(struct slang_bnplaces));
+    np->next = NULL;
+    np->place = place;
+    np->entries = 0;
+    np->texts = NULL;
+    if (p)
+      p->next = np;
+    else
+      ty->places = np;
+    p = np;
+  }
+  for (t = p->texts; t && t->next; t = t->next);
+  nt = nmalloc(sizeof(struct slang_texts));
+  nt->text = nmalloc(strlen(text) + 1);
+  strcpy(nt->text, text);
+  nt->dynamic = isdynamicslang(text);
+  nt->next = NULL;
+  if (t)
+    t->next = nt;
+  else
+    p->texts = nt;
+  p->entries++;
+  Context;
+}
+
+char badslang[15];
+static char *getslang(int idnr)
+{
+  struct slang_lang *l;
+  struct slang_ids *id;
+  struct slang_texts *t;
+  char *lang;
+  unsigned long x;
+
+  Context;
+  lang = slgloblang;
+  for (l = slangs; l; l = l->next) {
+    if ((!l->lang && !lang) || (l->lang && lang && !strcasecmp(l->lang, lang))) {
+      for (id = l->ids; id; id = id->next) {
+	if (id->id == idnr) {
+	  x = random() % id->entries;
+	  t = id->texts;
+	  while (t) {
+	    if (!x) {
+	      if (t->dynamic)
+	        return dynamicslang(t->text);
+	      else
+	        return t->text;
+	    }
+	    x--;
+	    t = t->next;
+	  }
+	}
+      }
+    }
+  }
+  sprintf(badslang, "SLANG%d", idnr);
+  return badslang;
+}
+
+char baddurs[15];
+static char *getdur(int id)
+{
+  struct slang_lang *l;
+  char *lang;
+
+  lang = slgloblang;
+  for (l = slangs; l; l = l->next) {
+    if ((!l->lang && !lang) || (l->lang && lang && !strcasecmp(l->lang, lang))) {
+      if (!l->durs[id]) {
+        sprintf(baddurs, "DURNS%d", id);
+        return baddurs;
+      } else {
+        return l->durs[id];
+      }
+    }
+  }
+  sprintf(baddurs, "DURNF%d", id);
+  return baddurs;
+}
+
+static char *getslangtype(char *type)
+{
+  struct slang_lang *l;
+  struct slang_types *ty;
+  struct slang_texts *t;
+  unsigned long x;
+  char *lang;
+
+  Context;
+  lang = slgloblang;
+  // I originally used strlower here, but it seems that it causes
+  // minor problems (like crashes ^_^), so I had to remove it and use
+  // strcasecmp() instead. I hope it doesn't cost too much cpu time.
+  // strlower(type);
+  for (l = slangs; l; l = l->next) {
+    if ((!l->lang && !lang) || (l->lang && lang && !strcasecmp(l->lang, lang))) {
+      for (ty = l->types; ty; ty = ty->next) {
+	if (!strcmp(ty->type, type)) {
+	  x = random() % ty->entries;
+	  t = ty->texts;
+	  while (t) {
+	    if (!x)
+	      return t->text;
+	    x--;
+	    t = t->next;
+	  }
+	}
+      }
+    }
+  }
+  return type;
+}
+
+/* slangtypetoi():
+ * find the index of a "slanged" stat-type
+ * if it isn't slanged, typetoi() is automatically called
+ */
+static int slangtypetoi(char *type)
+{
+  struct slang_lang *l;
+  struct slang_types *ty;
+  struct slang_texts *t;
+  char *lang;
+
+  Context;
+  lang = slgloblang;
+  for (l = slangs; l; l = l->next) {
+    if ((!l->lang && !lang) || (l->lang && lang && !strcasecmp(l->lang, lang))) {
+      for (ty = l->types; ty; ty = ty->next) {
+        for (t = ty->texts; t; t = t->next) {
+          if (!strcasecmp(t->text, type)) {
+            return typetoi(ty->type);
+          }
+        }
+      }
+    }
+  }
+  return typetoi(type);
+}
+
+static int isdynamicslang(char *text)
+{
+  Context;
+  if (strstr(text, "[bot]"))
+    return 1;
+  else if (strstr(text, "[topnr]"))
+    return 1;
+  else if (strstr(text, "[graphnr]"))
+    return 1;
+  else if (strstr(text, "[int]"))
+    return 1;
+  else if (strstr(text, "[chan]"))
+    return 1;
+  else if (strstr(text, "[user]"))
+    return 1;
+  else if (strstr(text, "[value]"))
+    return 1;
+  else if (strstr(text, "[peak]"))
+    return 1;
+  else if (strstr(text, "[totalusers]"))
+    return 1;
+  else if (strstr(text, "[chanstarted]"))
+    return 1;
+  else if (strstr(text, "[nick]"))
+    return 1;
+  else if (strstr(text, "[source "))
+    return 1;
+  return 0;
+}
+
+static char *dynamicslang(char *text)
+{
+  char *p, *tmp, *s, nr[61], *ende, *fbuf;
+  time_t tt, ttbuf;
+  int len, itype;
+  int changed;
+  FILE *f;
+
+  if (dynslang)
+    nfree(dynslang);
+  tmp = nmalloc(strlen(text) + 1);
+  strcpy(tmp, text);
+  changed = 1;
+  while (changed) {
+
+  changed = 0;
+  if ((p = strstr(tmp, "[bot]"))) {
+    len = strlen(tmp) + strlen(botnetnick);
+    p[0] = 0;
+    p += 5;
+    s = nmalloc(len + 1 - 5);
+    sprintf(s, "%s%s%s", tmp, filtbrackets(botnetnick), p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[topnr]"))) {
+    sprintf(nr, "%d", webnr);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 7;
+    s = nmalloc(len + 1 - 7);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[graphnr]"))) {
+    sprintf(nr, "%d", graphnr);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 9;
+    s = nmalloc(len + 1 - 9);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[peak]"))) {
+    sprintf(nr, "%d", slglobpeak);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 6;
+    s = nmalloc(len + 1 - 6);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[totalusers]"))) {
+    sprintf(nr, "%d", slglobtotalusers);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 12;
+    s = nmalloc(len + 1 - 12);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[int]"))) {
+    sprintf(nr, "%d", slglobint);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 5;
+    s = nmalloc(len + 1 - 5);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[chanstarted]"))) {
+    ttbuf = now;
+    tt = slglobgstarted;
+    strftime(nr, 60, "%d.%m. %Y  %H:%M", localtime(&tt));
+    ctime(&ttbuf); /* workaround for eggdrop bug */
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 13;
+    s = nmalloc(len + 1 - 13);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[value]"))) {
+    itype = typetoi(slglobtype);
+    if (itype >= 0)
+      sprintf(nr, "%ld", slgloblocstats->values[S_DAILY][itype]);
+    else if (itype == T_WPL)
+      sprintf(nr, "%.2f", (float) slgloblocstats->values[S_TODAY][T_WORDS] / (float) slgloblocstats->values[S_TODAY][T_LINES]);
+    else if (itype == T_IDLE)
+      sprintf(nr, "%.2f", (float) slgloblocstats->values[S_TODAY][T_MINUTES] / (float) slgloblocstats->values[S_TODAY][T_LINES]);
+    else if (itype == T_VOCABLES)
+      sprintf(nr, "%d", slgloblocstats->vocables);
+    else
+      sprintf(nr, "UNKNOWN iTYPE: %d", itype);
+    len = strlen(tmp) + strlen(nr);
+    p[0] = 0;
+    p += 7;
+    s = nmalloc(len + 1 - 7);
+    sprintf(s, "%s%s%s", tmp, nr, p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if (slglobchan && (p = strstr(tmp, "[chan]"))) {
+    len = strlen(tmp) + strlen(slglobchan);
+    p[0] = 0;
+    p += 6;
+    s = nmalloc(len + 1 - 6);
+    sprintf(s, "%s%s%s", tmp, filtbrackets(slglobchan), p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if (slgloblocstats && (p = strstr(tmp, "[user]"))) {
+    len = strlen(tmp) + strlen(slgloblocstats->user);
+    p[0] = 0;
+    p += 6;
+    s = nmalloc(len + 1 - 6);
+    sprintf(s, "%s%s%s", tmp, filtbrackets(slgloblocstats->user), p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if (slglobnick && (p = strstr(tmp, "[nick]"))) {
+    len = strlen(tmp) + strlen(slglobnick);
+    p[0] = 0;
+    p += 6;
+    s = nmalloc(len + 1 - 6);
+    sprintf(s, "%s%s%s", tmp, filtbrackets(slglobnick), p);
+    nfree(tmp);
+    tmp = s;
+    changed = 1;
+  }
+  if ((p = strstr(tmp, "[source "))) {
+    len = strlen(tmp);
+    p[0] = 0;
+    p += 8;
+    ende = strchr(p, ']');
+    ende[0] = 0;
+    fbuf = NULL;
+    f = fopen(p, "r");
+    if (f == NULL) {
+      fbuf = nmalloc(16 + strlen(p) + 1);
+      sprintf(fbuf, "File not found: %s", p);
+    } else {
+      s = nmalloc(512);
+      while (!feof(f)) {
+        s[0] = 0;
+        fgets(s, 512 - 1, f);
+        if (!s[0])
+          continue;
+        if (fbuf) {
+          fbuf = nrealloc(fbuf, strlen(fbuf) + strlen(s) + 1);
+          strcat(fbuf, s);
+        } else {
+          fbuf = nmalloc(strlen(s) + 1);
+          strcpy(fbuf, s);
+        }
+      }
+      fclose(f);
+      nfree(s);
+    }
+    p = ende + 1;
+    len += strlen(fbuf);
+    s = nmalloc(len + 1 - 8);
+    sprintf(s, "%s%s%s", tmp, filtbrackets(fbuf), p);
+    nfree(tmp);
+    nfree(fbuf);
+    tmp = s;
+    changed = 1;
+  }
+
+  }
+  dynslang = tmp;
+  return dynslang;
+}
+
+static char *filtbrackets(char *text)
+{
+  char *p;
+
+  if (filtbracketsstr) {
+    nfree(filtbracketsstr);
+    filtbracketsstr = NULL;
+  }
+  if (strchr(text, '[') || strchr(text, ']')) {
+    filtbracketsstr = nmalloc(strlen(text) + 1);
+    strcpy(filtbracketsstr, text);
+    for (p = filtbracketsstr; p[0]; p++) {
+      if (p[0] == '[')
+        p[0] = '{';
+      else if (p[0] == ']')
+        p[0] = '}';
+    }
+    return filtbracketsstr;
+  } else {
+    return text;
+  }
+}
+
+static char *chanlang(char *channel)
+{
+  struct slang_chan *chan;
+
+  Context;
+  for (chan = slangchans; chan; chan = chan->next)
+    if (!rfc_casecmp(chan->chan, channel))
+      return chan->lang;
+  return NULL;
+}
+
+static void setchanlang(char *channel, char *lang)
+{
+  struct slang_chan *chan, *nchan;
+
+  Context;
+  if (!strcasecmp(lang, "default"))
+    lang = NULL;
+  for (chan = slangchans; chan; chan = chan->next)
+    if (!rfc_casecmp(chan->chan, channel))
+      break;
+  if (!chan) {
+    for (chan = slangchans; chan && chan->next; chan = chan->next);
+    nchan = nmalloc(sizeof(struct slang_chan));
+    nchan->chan = nmalloc(strlen(channel) + 1);
+    strcpy(nchan->chan, channel);
+    nchan->lang = NULL;
+    nchan->next = NULL;
+    if (chan)
+      chan->next = nchan;
+    else
+      slangchans = nchan;
+    chan = nchan;
+  }
+  if (chan->lang) {
+    nfree(chan->lang);
+    chan->lang = NULL;
+  }
+  if (lang) {
+    chan->lang = nmalloc(strlen(lang) + 1);
+    strcpy(chan->lang, lang);
+  }
+}
+
+static void setslglobs(char *chan, int peak, int totalusers, time_t globstarted)
+{
+  char *lang = NULL;
+
+  Context;
+  if (slglobchan)
+    nfree(slglobchan);
+  if (slgloblang)
+    nfree(slgloblang);
+  slgloblang = NULL;
+  slglobchan = NULL;
+  if (chan) {
+    slglobchan = nmalloc(strlen(chan) + 1);
+    strcpy(slglobchan, chan);
+    lang = chanlang(chan);
+  }
+  if (lang) {
+    slgloblang = nmalloc(strlen(lang) + 1);
+    strcpy(slgloblang, lang);
+  }
+  slglobpeak = peak;
+  slglobtotalusers = totalusers;
+  slglobgstarted = globstarted;
+}
+
+static void setslnick(char *nick)
+{
+  if (slglobnick)
+    nfree(slglobnick);
+  slglobnick = nmalloc(strlen(nick) + 1);
+  strcpy(slglobnick, nick);
+}

+ 110 - 0
slang.h

@@ -0,0 +1,110 @@
+#define SLCSS		getslang(1)
+#define SLBODYTAG       getslang(5)
+#define SLHEADER        getslang(10)
+#define SLFOOTER        getslang(11)
+
+#define ROOTTITLE	getslang(100)
+#define SLTOP		getslang(105)
+
+#define SLTOTAL		getslang(110)
+#define SLDAILY		getslang(111)
+#define SLWEEKLY	      getslang(112)
+#define SLMONTHLY	      getslang(113)
+#define SLLTOTAL        getslang(114)
+#define SLLTODAY        getslang(115)
+#define SLLWEEKLY       getslang(116)
+#define SLLMONTHLY      getslang(117)
+#define SLUSERS		getslang(118)
+#define SLONCHAN        getslang(119)
+#define SLMISCSTATS	getslang(120)
+
+#define SLINDEXTITEL	getslang(130)
+
+#define SLTTOPTITLE	getslang(200)
+#define SLDTOPTITLE	getslang(201)
+#define SLWTOPTITLE	getslang(202)
+#define SLMTOPTITLE	getslang(203)
+#define SLTTOPHEAD	getslang(210)
+#define SLDTOPHEAD	getslang(211)
+#define SLWTOPHEAD	getslang(212)
+#define SLMTOPHEAD	getslang(213)
+#define SLORDEREDBY	getslang(214)
+#define SLPEAK		getslang(215)
+#define SLTNR           getslang(216)
+#define SLTOTALUSERS	getslang(217)
+#define SLGSTARTED	getslang(218)
+#define SLGRAPHS	      getslang(220)
+#define SLOTHERCHANS	getslang(221)
+
+#define SLYEAR		getdur(0)
+#define SLYEARS         getdur(1)
+#define SLWEEK          getdur(2)
+#define SLWEEKS         getdur(3)
+#define SLDAY           getdur(4)
+#define SLDAYS          getdur(5)
+#define SLHOUR          getdur(6)
+#define SLHOURS         getdur(7)
+#define SLMINUTE        getdur(8)
+#define SLMINUTES       getdur(9)
+#define SLSECOND	      getslang(260)
+#define SLSECONDS	      getslang(261)
+
+#define SLGRTTITLE	getslang(300)
+#define SLGRDTITLE	getslang(301)
+#define SLGRWTITLE	getslang(302)
+#define SLGRMTITLE	getslang(303)
+#define SLGRTHEAD	      getslang(310)
+#define SLGRDHEAD	      getslang(311)
+#define SLGRWHEAD	      getslang(312)
+#define SLGRMHEAD	      getslang(313)
+#define SLGRORDEREDBY	getslang(320)
+#define SLGRTOTAL	      getslang(321)
+#define SLGROTHERS	getslang(322)
+#define SLGRTABLE	      getslang(323)
+
+#define SLUSERSTITLE	getslang(400)
+#define SLUSERSONCHANTITLE      getslang(410)
+#define SLNOWONCHAN	getslang(420)
+#define SLIDLETIME	getslang(430)
+#define SLNETSPLITTED   getslang(435)
+#define SLITSME         getslang(436)
+
+#define SLEMAIL		getslang(450)
+#define SLHOMEPAGE	getslang(451)
+
+#define SLUSERTITLE	getslang(500)
+#define SLUSERHEAD	getslang(501)
+#define SLPLACE getslang(502)
+#define SLRANDQUOTE	getslang(510)
+#define SLUWORDSTATS	getslang(520)
+#define SLOTHERUSERS	getslang(530)
+
+#define SLMISCTITLE	getslang(600)
+#define SLMISCHEAD	getslang(610)
+#define SLMAUSERS       getslang(620)
+#define SLMTOPICS	      getslang(630)
+#define SLMTOPICBY     	getslang(631)
+#define SLMURLS         getslang(640)
+#define SLMURLBY        getslang(641)
+#define SLMMOSTUSED	getslang(650)
+#define SLMISPS		getslang(651)
+#define SLMTLDS		getslang(652)
+#define SLMKICKS        getslang(660)
+#define SLMCWORDSTATS	getslang(670)
+#define SLMMOSTUSEDWORDS	getslang(671)
+
+#define SLSTATICTITLE   getslang(900)
+#define SLSTATICBODY    getslang(901)
+
+#define SLNOSUCHTYPE    getslang(1000)
+#define SLTOPWORD       getslang(1005)
+#define SLNOSTATSABOUTYOU     getslang(1010)
+#define SLNOSTATSABOUTSOMEONE getslang(1011)
+#define SLNOWORDSTATS         getslang(1016)
+#define SLUSERSMOSTUSEDWORDS  getslang(1017)
+#define SLCHANSMOSTUSEDWORDS  getslang(1018)
+#define SLDONTRECOGNIZE	getslang(1020)
+#define SLEMAILUSAGE	getslang(1021)
+#define SLSETEMAIL	getslang(1022)
+#define SLHPUSAGE	getslang(1023)
+#define SLSETHOMEPAGE	getslang(1024)

+ 766 - 0
stats.c

@@ -0,0 +1,766 @@
+/*
+*   Statistics module for eggdrop 1.4+
+*     by G`Quann
+*/
+
+/*
+ * stats.c   - this is never gonna work... (Oct 99)
+*/
+
+/*
+*  oops... seems that it works :) (Dec 99)
+*/
+
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+#define MAKING_STATS
+#define MODULE_NAME "stats"
+#define MODULE_VERSION "1.3.3dev3"
+#include "../module.h"
+#include "../irc.mod/irc.h"
+#include "../server.mod/server.h"
+#include "../channels.mod/channels.h"
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <time.h> /* for time_t */
+#include <ctype.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#undef global
+static Function *global = NULL, *irc_funcs = NULL, *server_funcs = NULL, *channels_funcs = NULL;
+
+#ifndef EGG_IS_MIN_VER
+#define EGG_IS_MIN_VER(ver)             ((ver) <= 10400)
+#endif
+
+#if !EGG_IS_MIN_VER(10500)
+#define OLDBOT 1
+#endif
+
+#ifndef Context
+#define Context context
+#endif
+
+#ifndef findchan_by_dname
+#define findchan_by_dname findchan
+#endif
+
+#ifndef my_htonl
+#define my_htonl htonl
+#endif
+
+#include "stats.h"
+#include "slang.h"
+
+static struct stats_global *sdata = NULL;
+static struct stats_chanset *schans = NULL;
+static struct stats_userlist *suserlist = NULL;
+static struct slang_lang *slangs = NULL;
+static struct slang_chan *slangchans = NULL;
+
+static char statsfile[121] = "statsmod.dat";
+static char webstats[510] = "words letters lines actions smileys joins kicks modes topics minutes";
+static char graphstats[510] = "words letters lines actions smileys joins kicks modes topics minutes";
+static char stat_reply[128] = "words letters smileys minutes";
+static char graphgif[128] = "";
+static char graphcolor[20] = "blue";
+static char webdir[256] = "../public_html";
+static char webfiles[256] = "";
+static char webfile_suffix[20] = ".html";
+static char bodytag[512] = "<BODY BGCOLOR=#000000 TEXT=#1A9DFF LINK=#00D993 VLINK=#71C2FF ALINK=#2FFFBB>";
+static char smileys[128] = ":-) :) ;) ;-) ^_^ :-D :-P :P =) ;D";
+static char badflags[20] = "ofvb|ofv";
+static char nostatsflags[20] = "b|-";
+static char nopeak[20] = "b|-";
+static char network[41] = "unknown-net";
+static char stats_loglevel[20] = "1";
+static char livestats_log[121];
+static char livestats_ignore_msg[256] = "<H1>You are on ignore.</H1>";
+static char livestats_ip[21] = "";
+static int statsfilemode = 0600;
+static int webupdate = 0;
+static int webnr = 15;
+static int graphnr = 15;
+static int stats_save_time = 10;
+static int autoadd = 5;
+static int stat_expire_user = 0;
+static int write_today = 1;
+static int maxstat_thr = 0;
+static int maxstat_time = 0;
+static int maxlivestats_thr = 0;
+static int maxlivestats_time = 0;
+static int mstat_thr = 0;
+static time_t mstat_time = 0;
+static int livestats_timeout;
+static int offset = 0;
+static int max_words = 20;
+static int topwords_limit = 5;
+static int quote_freq = 10;
+static int log_wordstats = 0;
+static int lasthour = 0;
+static int lastmonth = 0;
+static int min_word_length = 0;
+static int use_userfile = 0;
+static int table_color = 0x3850B8;
+static int fade_table_to = 0x000000;
+static int table_border = 0;
+static int log_urls = 1;
+static int kick_context = 0;
+static int display_kicks = 0;
+static int display_average_users = 1;
+static int show_userlist = 1;
+static int show_usersonchan = 1;
+static int list_secret_chans = 1;
+
+#include "datahandling.c"
+#include "slang.c"
+#include "sensors.c"
+#include "userrec.c"
+#include "misc.c"
+#include "pubcmds.c"
+#include "msgcmds.c"
+#include "dcccmds.c"
+#include "tclstats.c"
+#if EGG_IS_MIN_VER(10500)
+#include "webfiles.c"
+#endif
+#include "user.c"
+#include "livestats.c"
+
+static int stats_save_temp = 1;
+static int webupdate_temp = 1;
+
+static int stats_expmem()
+{
+  int size = 0;
+  struct stats_global *sl = sdata;
+
+  Context;
+  while (sl) {
+    size += sizeof(struct stats_global);
+    size += strlen(sl->chan) + 1;
+    size += localstats_expmem(sl->local);
+    size += wordstats_expmem(sl->words);
+    size += topics_expmem(sl->topics);
+    size += urls_expmem(sl->urls);
+    size += hosts_expmem(sl->hosts);
+    size += quotes_expmem(sl->log);
+    size += kicks_expmem(sl->kicks);
+    sl = sl->next;
+  }
+  Context;
+  size += suserlist_expmem(suserlist);
+  size += chanlist_expmem(schans);
+  size += slang_expmem();
+  return size;
+}
+
+static int suserlist_expmem(struct stats_userlist *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += sizeof(struct stats_userlist);
+    size += strlen(e->user) + 1;
+    if (e->email)
+      size += strlen(e->email) + 1;
+    if (e->homepage)
+      size += strlen(e->homepage) + 1;
+    size += hostlist_expmem(e->hosts);
+    e = e->next;
+  }
+  return size;
+}
+
+static int hostlist_expmem(struct stats_hostlist *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += sizeof(struct stats_hostlist);
+    size += strlen(e->mask) + 1;
+    e = e->next;
+  }
+  return size;
+}
+
+static int chanlist_expmem(struct stats_chanset *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += sizeof(struct stats_chanset);
+    size += strlen(e->chan) + 1;
+    size += memberlist_expmem(e->members);
+    e = e->next;
+  }
+  return size;
+}
+
+static int memberlist_expmem(struct stats_memberlist *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += sizeof(struct stats_memberlist);
+    size += strlen(e->nick) + 1;
+    size += strlen(e->uhost) + 1;
+    e = e->next;
+  }
+  return size;
+}
+
+static int localstats_expmem(struct stats_local *sl)
+{
+  int size = 0;
+
+  Context;
+  while (sl) {
+    size += sizeof(struct stats_local);
+    size += strlen(sl->user) + 1;
+    size += wordstats_expmem(sl->words);
+    size += quotes_expmem(sl->quotes);
+    sl = sl->next;
+  }
+  Context;
+  return size;
+}
+
+static int wordstats_expmem(wordstats *l)
+{
+  int size = 0;
+
+  Context;
+  while (l) {
+    size += strlen(l->word) + 1;
+    size += sizeof(wordstats);
+    l = l->next;
+  }
+  return size;
+}
+
+static int quotes_expmem(quotestr *l)
+{
+  int size = 0;
+
+  Context;
+  while (l) {
+    size += strlen(l->quote) + 1;
+    size += sizeof(quotestr);
+    l = l->next;
+  }
+  return size;
+}
+
+static int topics_expmem(topicstr *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += strlen(e->topic) + 1;
+    size += strlen(e->by) + 1;
+    size += sizeof(topicstr);
+    e = e->next;
+  }
+  return size;
+}
+
+static int urls_expmem(struct stats_url *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += strlen(e->url) + 1;
+    size += strlen(e->by) + 1;
+    size += sizeof(struct stats_url);
+    e = e->next;
+  }
+  return size;
+}
+
+static int kicks_expmem(struct stats_kick *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += sizeof(struct stats_kick);
+    size += quotes_expmem(e->log);
+    e = e->next;
+  }
+  return size;
+}
+
+static int hosts_expmem(hoststr *e)
+{
+  int size = 0;
+
+  Context;
+  while (e) {
+    size += strlen(e->host) + 1;
+    size += sizeof(hoststr);
+    e = e->next;
+  }
+  return size;
+}
+
+static void free_stats()
+{
+  struct stats_global *sl;
+
+  Context;
+  while (sdata) {
+    sl = sdata->next;
+    free_localstats(sdata->local);
+    free_wordstats(sdata->words);
+    free_topics(sdata->topics);
+    free_urls(sdata->urls);
+    free_quotes(sdata->log);
+    free_hosts(sdata->hosts);
+    free_kicks(sdata->kicks);
+    nfree(sdata->chan);
+    nfree(sdata);
+    sdata = sl;
+  }
+  free_chanlist(schans);
+  free_suserlist(suserlist);
+  free_slang();
+  Context;
+  return;
+}
+
+static void free_suserlist(struct stats_userlist *e)
+{
+  struct stats_userlist *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->user);
+    if (e->email)
+      nfree(e->email);
+    if (e->homepage)
+      nfree(e->homepage);
+    free_hostlist(e->hosts);
+    nfree(e);
+    e = ee;
+  }
+}
+
+static void free_hostlist(struct stats_hostlist *e)
+{
+  struct stats_hostlist *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->mask);
+    nfree(e);
+    e = ee;
+  }
+}
+
+static void free_chanlist(struct stats_chanset *e)
+{
+  struct stats_chanset *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->chan);
+    free_memberlist(e->members);
+    nfree(e);
+    e = ee;
+  }
+}
+
+static void free_one_chan(char *channel)
+{
+  struct stats_chanset *e, *ee;
+
+  Context;
+  e = schans;
+  ee = NULL;
+  while (e) {
+    if (!rfc_casecmp(e->chan, channel)) {
+      nfree(e->chan);
+      free_memberlist(e->members);
+      if (ee)
+	ee->next = e->next;
+      else
+        schans = e->next;
+      nfree(e);
+      return;
+    }
+    ee = e;
+    e = e->next;
+  }
+}
+
+static void free_memberlist(struct stats_memberlist *e)
+{
+  struct stats_memberlist *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->nick);
+    nfree(e->uhost);
+    nfree(e);
+    e = ee;
+  }
+}
+
+static void free_localstats(struct stats_local *sl)
+{
+  struct stats_local *sll;
+
+  Context;
+  while (sl) {
+    Context;
+    sll = sl->next;
+    free_wordstats(sl->words);
+    free_quotes(sl->quotes);
+    nfree(sl->user);
+    nfree(sl);
+    sl = sll;
+    Context;
+  }
+  Context;
+  return;
+}
+
+static void free_wordstats(wordstats *l)
+{
+  wordstats *ll;
+
+  Context;
+  while (l) {
+    ll = l->next;
+    nfree(l->word);
+    nfree(l);
+    l = ll;
+  }
+  return;
+}
+
+static void free_quotes(quotestr *l)
+{
+  quotestr *ll;
+
+  Context;
+  while (l) {
+    ll = l->next;
+    nfree(l->quote);
+    nfree(l);
+    l = ll;
+  }
+  return;
+}
+
+static void free_topics(topicstr *e)
+{
+  topicstr *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->topic);
+    nfree(e->by);
+    nfree(e);
+    e = ee;
+  }
+  return;
+}
+
+static void free_urls(struct stats_url *e)
+{
+  struct stats_url *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->url);
+    nfree(e->by);
+    nfree(e);
+    e = ee;
+  }
+  return;
+}
+
+static void free_kicks(struct stats_kick *e)
+{
+  struct stats_kick *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    free_quotes(e->log);
+    nfree(e);
+    e = ee;
+  }
+}
+
+static void free_hosts(hoststr *e)
+{
+  hoststr *ee;
+
+  Context;
+  while (e) {
+    ee = e->next;
+    nfree(e->host);
+    nfree(e);
+    e = ee;
+  }
+  return;
+}
+
+/* a report on the module status */
+static void stats_report(int idx, int details)
+{
+  int size, users, hosts;
+
+  Context;
+  size = stats_expmem();
+  users = countsusers();
+  hosts = counthosts();
+  dprintf(idx, "    Stats-userbase contains %d users and %d hosts\n", users, hosts);
+  if (details)
+    dprintf(idx, "    using %d bytes\n", size);
+}
+
+static void stats_minutely ()
+{
+  sensor_minutely();
+  if (stats_save_temp >= stats_save_time) {
+    write_stats();
+    stats_save_temp = 1;
+  } else
+    stats_save_temp++;
+  if ((webupdate_temp >= webupdate) && (webupdate > 0)) {
+    webupdate_temp = 1;
+#if EGG_IS_MIN_VER(10500)
+    write_new_webstats();
+#else
+    putlog(LOG_MISC, "*", "Couldn't write static webstats! Feature unsupported on eggdrop1.4!");
+#endif
+  } else
+    webupdate_temp++;
+  check_desynch();
+}
+
+static void stats_5minutely ()
+{
+  Context;
+  sensor_countusers();
+}
+
+static void stats_daily ()
+{
+  Context;
+  deloldstatusers();
+  purgestats();
+  reset_tstats();
+  if (lastmonth != getmonth()) {
+    reset_mwstats(S_MONTHLY);
+    lastmonth = getmonth();
+  }
+  if (ismonday())
+    reset_mwstats(S_WEEKLY);
+  update_schannel_members();
+  Context;
+}
+
+static tcl_ints my_tcl_ints[] =
+{
+  {"save-stats", &stats_save_time, 0},
+  {"webupdate", &webupdate, 0},
+  {"webnr", &webnr, 0},
+  {"topnr", &webnr, 0},
+  {"graphnr", &graphnr, 0},
+  {"autoadd", &autoadd, 0},
+  {"expire-users", &stat_expire_user, 0},
+  {"write-today", &write_today, 0},
+  {"stats-file-mode", &statsfilemode, 0},
+  {"livestats-timeout", &livestats_timeout, 0},
+  {"offset", &offset, 0},
+  {"max-words", &max_words, 0},
+  {"topwords-limit", &topwords_limit, 0},
+  {"quote-frequency", &quote_freq, 0},
+  {"log-wordstats", &log_wordstats, 0},
+  {"min-word-length", &min_word_length, 0},
+  {"use-eggdrop-userfile", &use_userfile, 0},
+  {"table-color", &table_color, 0},
+  {"fade-table-to", &fade_table_to, 0},
+  {"table-border", &table_border, 0},
+  {"display-urls", &log_urls, 0},
+  {"kick-context", &kick_context, 0},
+  {"display-kicks", &display_kicks, 0},
+  {"display-average-users", &display_average_users, 0},
+  {"show-userlist", &show_userlist, 0},
+  {"show-usersonchan", &show_usersonchan, 0},
+  {"list-secret-chans", &list_secret_chans, 0},
+  {0, 0, 0}
+};
+
+static tcl_strings my_tcl_strings[] =
+{
+  {"statsfile", statsfile, 121, 0},
+  {"webstats", webstats, 510, 0},
+  {"topstats", webstats, 510, 0},
+  {"webdir", webdir, 256, 0},
+  {"webfiles", webfiles, 256, 0},
+  {"webfile-suffix", webfile_suffix, 20, 0},
+  {"bodytag", bodytag, 512, 0},
+  {"smileys", smileys, 128, 0},
+  {"smilies", smileys, 128, 0},
+  {"graphstats", graphstats, 510, 0},
+  {"graphgif", graphgif, 128, 0},
+  {"graphcolor", graphcolor, 20, 0},
+  {"anti-autoadd-flags", badflags, 20, 0},
+  {"anti-stats-flag", nostatsflags, 20, 0},
+  {"anti-peak-flag", nopeak, 20, 0},
+  {"network", network, 40, 0},
+  {"livestats-loglevel", stats_loglevel, 20, 0},
+  {"livestats-log", livestats_log, 121, 0},
+  {"stat-reply", stat_reply, 128, 0},
+  {"livestats-ignore-msg", livestats_ignore_msg, 128, 0},
+  {"livestats-ip", livestats_ip, 20, 0},
+  {0, 0, 0, 0}
+};
+
+static tcl_coups my_tcl_coups[] =
+{
+  {"max-stat-cmds", &maxstat_thr, &maxstat_time},
+  {"max-livestats-access", &maxlivestats_thr, &maxlivestats_time},
+  {0, 0, 0},
+};
+
+static char *stats_close()
+{
+  Context;
+  stop_listen_livestats();
+  write_stats();
+  free_stats();
+  rem_builtins(H_dcc, mydcc);
+  rem_builtins(H_pubm, stats_pubm);
+  rem_builtins(H_pub, stats_pub);
+  rem_builtins(H_msg, stats_msg);
+  rem_builtins(H_topc, stats_topc);
+  rem_builtins(H_ctcp, stats_ctcp);
+  rem_builtins(H_kick, stats_kick);
+  rem_builtins(H_mode, stats_mode);
+  rem_builtins(H_nick, stats_nick);
+  rem_builtins(H_join, stats_join);
+  rem_builtins(H_nkch, stats_nkch);
+  rem_builtins(H_sign, stats_sign);
+  rem_builtins(H_part, stats_part);
+  rem_tcl_ints(my_tcl_ints);
+  rem_tcl_coups(my_tcl_coups);
+  rem_tcl_strings(my_tcl_strings);
+  rem_tcl_commands(mytcls);
+  rem_help_reference("stats.help");
+  del_hook(HOOK_MINUTELY, (Function) stats_minutely);
+  del_hook(HOOK_DAILY, (Function) stats_daily);
+  del_hook(HOOK_5MINUTELY, (Function) stats_5minutely);
+  module_undepend(MODULE_NAME);
+  return NULL;
+}
+
+char *stats_start();
+
+static Function stats_table[] =
+{
+  (Function) stats_start,
+  (Function) stats_close,
+  (Function) stats_expmem,
+  (Function) stats_report,
+};
+
+char *stats_start(Function * global_funcs)
+{
+  global = global_funcs;
+  Context;
+  sdata = NULL;
+  schans = NULL;
+  suserlist = NULL;
+  slangs = NULL;
+  slangchans = NULL;
+  Context;
+  module_register(MODULE_NAME, stats_table, 1, 3);
+  if (!(irc_funcs = module_depend(MODULE_NAME, "irc", 1, 0)))
+    return "You need the irc module to use the stats module.";
+  if (!(server_funcs = module_depend(MODULE_NAME, "server", 1, 0)))
+    return "You need the server module to use the stats module.";
+  if (!(channels_funcs = module_depend(MODULE_NAME, "channels", 1, 0)))
+    return "You need the channels module to use the stats module.";
+  if (!module_depend(MODULE_NAME, "eggdrop", 107, 0)) {
+    if (!module_depend(MODULE_NAME, "eggdrop", 106, 0)) {
+      if (!module_depend(MODULE_NAME, "eggdrop", 105, 0)) {
+        if (!module_depend(MODULE_NAME, "eggdrop", 104, 0)) {
+          module_undepend(MODULE_NAME);
+          return "Sorry, stats.mod doesn't work with this eggdrop version.";
+        }
+      }
+    }
+  }
+#ifndef OLDBOT
+  livestats_timeout = 10;
+#else
+  livestats_timeout = 1;
+#endif
+  livestats_log[0] = 0;
+  add_builtins(H_dcc, mydcc);
+  add_builtins(H_pubm, stats_pubm);
+  add_builtins(H_pub, stats_pub);
+  add_builtins(H_msg, stats_msg);
+  add_builtins(H_topc, stats_topc);
+  add_builtins(H_ctcp, stats_ctcp);
+  add_builtins(H_kick, stats_kick);
+  add_builtins(H_mode, stats_mode);
+  add_builtins(H_nick, stats_nick);
+  add_builtins(H_join, stats_join);
+  add_builtins(H_nkch, stats_nkch);
+  add_builtins(H_sign, stats_sign);
+  add_builtins(H_part, stats_part);
+  add_tcl_ints(my_tcl_ints);
+  add_tcl_coups(my_tcl_coups);
+  add_tcl_strings(my_tcl_strings);
+  add_tcl_commands(mytcls);
+  add_hook(HOOK_MINUTELY, (Function) stats_minutely);
+  add_hook(HOOK_DAILY, (Function) stats_daily);
+  add_hook(HOOK_5MINUTELY, (Function) stats_5minutely);
+  add_help_reference("stats.help");
+  lastmonth = getmonth();
+  read_stats();
+#if EGG_IS_MIN_VER(10503)
+  initudef(1, "nopubstats", 1);
+  initudef(1, "quietstats", 1);
+  initudef(1, "nostats", 1);
+#endif
+  putlog(LOG_MISC, "*", "Stats.mod v%s loaded.", MODULE_VERSION);
+  return NULL;
+}

+ 244 - 0
stats.conf

@@ -0,0 +1,244 @@
+######
+#####
+###   General Settings
+#####
+######
+
+# the file where the data is stored
+# WARNING: Always set this var _before_ the module is loaded, or it won't
+# find your stats.
+set statsfile "statsmod.dat"
+
+# load the module itself
+loadmodule stats
+
+# save data every x minutes
+set save-stats 15
+
+# add new users to the database if they stay more than x minutes 
+# in the channel
+# >0: enabled
+#  0: don't add new users, but recheck every minute if a user idented himself
+# -1: don't add new users and don't recheck every minute. If someone idents
+#     him/herself, he/she must cycle the chan to be recognized. (lowest CPU usage)
+set autoadd 5
+
+# delete users that have been added by the module if they haven't been seen
+# for more than x days
+set expire-users 30
+
+# log wordstats (most used words) (this stats are resetted daily)
+# NOTE: This is very cpu- and memory consuming, so don't
+# turn this on if you're worried about memory or cpu usage
+set log-wordstats 0
+
+# minimum length of a word to be logged
+# (0 is best to get exact results)
+set min-word-length 0
+
+# display how many random URLs in the misc stats?
+# (setting this to 0 also disables the logging of URLs
+# and can therefore lower your cpu-usage)
+set display-urls 5
+
+# display how many random kicks in the misc stats?
+set display-kicks 5
+
+# when logging kicks, also save the last x lines of activity in the chan
+set kick-context 5
+
+# log every Xth quote
+# If this value is lower, then the random quotes will be more random,
+# but the memory usage will also increase.
+set quote-frequency 5
+
+######
+#####
+###   languages
+#####
+######
+
+# reset old slangs before we load the new ones...
+resetslang
+
+# load the default language
+loadslang language/stats.lang
+
+##  load additional languages
+# loadslang ger language/stats.ger.lang
+#
+## assign a language to a channel
+# setchanslang #xwp ger
+
+######
+#####
+###   public commands
+#####
+######
+
+# the char that marks public commands (!top10, etc...)
+# "" is a valid option
+set cmdchar "!"
+
+# don't answer more than x commands in y seconds
+set max-stat-cmds 5:60
+
+# information that the !stat reply will contain
+set stat-reply "words letters smileys minutes"
+
+######
+#####
+###   HTML settings
+#####
+######
+
+# list the top x users
+set topnr 30
+
+# NOTE:
+# table-color and fade-table-color are hex numbers which represent a rgb color.
+# If you do not know how to use this format, either use the default or ask
+# someone who knows it. (it's the HTML-standard-format)
+
+# the background color of your table
+set table-color 0x3850B8
+
+# fade table color to another color
+set fade-table-to 0x000000
+
+# border width of the table (0 == no border, which is probably the best)
+set table-border 0
+
+# The following two settings only affect the display of the stats. The bot will
+# still log and save everything, no matter if it's displayed or not.
+#
+# Please don't use "slangified" types! For example, use always "idle" instead of
+# "idle-factor" or whatever you defined in the langfiles.
+
+# which stats should be shown?
+set topstats "words lines wpl actions smileys kicks modes topics idle minutes"
+
+# and which stats should be mentioned in the graphical output?
+set graphstats "words lines actions smileys kicks modes topics minutes"
+
+# mention the top x users in the graphical stats.
+set graphnr 15
+
+# which colour should the bars have?
+set graphcolor "blue"
+
+# display the average number of users in the chan?
+set display-average-users 1
+
+# display the users which are currently on channel in /chan/onchan/ ?
+set show-usersonchan 1
+
+# display a list of all users in /chan/users/ ?
+set show-userlist 1
+
+# display channels, which are +secret on the livestats index?
+set list-secret-chans 1
+
+# You can use a backgound to fill the bars to get a nice 3d look, for example.
+# WARNING: entering a non-existant filename might mess the output up in some
+# browsers
+# WARNING2: use a full URL if you're using livestats.
+# (this is now disabled by default, because 50% of the users made it wrong <g>)
+#set graphgif "http://your.shell.com/~you/bar.gif"
+
+##
+## Livestat settings
+##
+
+# listen on which port for livestat connection?
+# (to disable, either comment this out or use "off" or 0 as value)
+# (if you use the default, you can access the stats at
+#  http://your.shell.com:8033/ )
+livestats 8033
+
+# By default, livestats listens on every available vhost for incoming
+# connection. If you want to limit livestats to one vhost, then enter
+# its IP(_not_ the host) below
+#set livestats-ip ""
+
+# log access to livestats to a logfile? (set to "" to turn off)
+set livestats-log "livestats.log"
+
+# the log-level where you can monitor access to livestats on your console
+# (use .console +1 to activate it)
+set livestats-loglevel "1"
+
+# don't serve more than x livestats pages in y seconds (to prevent flooding)
+set max-livestats-access 5:10
+
+####
+## only for the frequently updated webfiles
+##
+## this feature is outdated, don't use it unless you really need it
+###
+
+# update webfiles every x minutes
+# (set this to 0 to disable)
+# (this is now disabled by default, use livestats instead!)
+set webupdate 0
+
+# directory where the files will be written to
+# don't use your standard webdir here, since index.html will be
+# overwritten and a lot of subdirs will be created
+set webdir "../public_html/stats"
+
+######
+#####
+###   other (rather unimportant) settings
+#####
+######
+
+## stats.mod saves new users in an internal database by default
+## for compatibility reasons, the old way of using the eggdrop
+## user file is still available, but I strongly recommend to let
+## it turned off!
+#set use-eggdrop-userfile 0
+#
+## don't add new hosts to users with the following flags
+## (only works for users in the eggdrop-userfile, use
+##  .schattr <user> -addhosts for users in the stats-userbase)
+#set anti-autoadd-flags "ofvb|ofv"
+#
+## exclude users with the following flags from the top10, top20...
+## (also only for eggdrop-userfile. Use .schattr <user> -list.
+#set anti-stats-flag "b|-"
+##
+## Please note that the last two settings don't affect the internal
+## user database of stats.mod at all! They only work if
+## use-eggdrop-userfile is set to 1
+## (maybe I'll change this for the next version)
+
+# defines what will be counted as a smiley
+# (don't add hundreds of smileys if you want a reasonable cpu usage)
+set smileys ":-) :) ;) ;-) ^_^ :-D :-P :P =) ;D"
+
+###############################################################################
+# end of config
+# just ignore everything below ^_^
+###############################################################################
+
+catch "unbind pub - !top10 *pub:!top10"
+catch "unbind pub - !ttop10 *pub:!ttop10"
+catch "unbind pub - !top20 *pub:!top20"
+catch "unbind pub - !ttop20 *pub:!ttop20"
+catch "unbind pub - !stat *pub:!stat"
+catch "unbind pub - !tstat *pub:!tstat"
+catch "unbind pub - !place *pub:!place"
+catch "unbind pub - !tplace *pub:!tplace"
+catch "unbind pub - !wordstats *pub:!wordstats"
+catch "unbind pub - !topwords *pub:!topwords"
+bind pub - ${cmdchar}top10 *pub:!top10
+bind pub - ${cmdchar}ttop10 *pub:!ttop10
+bind pub - ${cmdchar}top20 *pub:!top20
+bind pub - ${cmdchar}ttop20 *pub:!ttop20
+bind pub - ${cmdchar}place *pub:!place
+bind pub - ${cmdchar}tplace *pub:!tplace
+bind pub - ${cmdchar}stat *pub:!stat
+bind pub - ${cmdchar}tstat *pub:!tstat
+bind pub - ${cmdchar}wordstats *pub:!wordstats
+bind pub - ${cmdchar}topwords *pub:!topwords

+ 234 - 0
stats.ger.lang

@@ -0,0 +1,234 @@
+#######################################
+#
+# Language file for stats.mod v1.3.0
+# ----------------------------------
+#
+#
+# Ich denke, es erklärt sich von selbst, wie man das hier benutzt.
+# Wenn man mehr als einen Eintrag pro ID eingibt, dann wird jedesmal
+# bei der Ausgabe zufällig einer daraus ausgewählt.
+#
+# Es gibt einige Variable´n, die man benutzen kann und sollte, aber nicht jede
+# funktioniert an jeder Stelle. Beispielsweise wäre es blödsinnig, [user] 
+# für irgendeinen Teil der Index-Seite zu benutzen. In solchen Fällen ist die
+# Variable dann nicht initialisiert. Entweder steht dann dort nur Blödsinn,
+# oder der Bot stürzt schlimmstenfalls sogar ab.
+#
+# Man sollte außerdem beachten, daß, [user] nicht das selbe wie [nick] ist und
+# [value] nicht das selbe wie [int].
+#
+##########################################
+
+# Hier werden die einzelnen Stat-Typen übersetzt. Sollte kein Eintrag für
+# einen Typ vorhanden sein, wird einfach der Standard verwendet.
+T words Wörter
+T letters Buchstaben
+T lines Zeilen
+T smileys Smilies
+T minutes verschwendete Zeit
+T wpl Wörter/Zeile
+T quote Zitat
+T idle Idle-Faktor
+
+# Das hier kommt in den header der HTML-Dateien. In diesem Fall sind es CSS.
+1 <style type="text/css"><!--  A:link { text-decoration: none; }  A:visited { text-decoration: none;}  A:active { text-decoration: none;}  A:hover { text-decoration: underline;} --></style>
+
+# das bodytag (hier wird das allgemeine Farbschema für die Seiten festgelegt)
+5 <BODY BGCOLOR=#000000 TEXT=#1A9DFF LINK=#00D993 VLINK=#71C2FF ALINK=#2FFFBB>
+
+
+# Die nächsten beiden Elemente werden auf jeder Seite ganz oben und ganz
+# unten eingefügt. Das ist ganz praktisch um die Statistiken in ein bereits
+# existierendes Design mit Navigations-Leiste und ähnlichem einzufügen.
+
+# Beispiel1 (Header und Footer aus externen Dateien):
+#10 [source head.html]
+#11 [source footer.html]
+
+# Example2 (überhaupt kein Header oder Footer):
+10
+11
+
+# Titel der Index Seite
+100 [bot]'s Livestats
+
+# Dieser Text beschreibt den Link zu den Top-Quasslern.
+105 top[topnr]
+
+# Zeitspanne der Stats
+110 insgesamt
+111 heute
+112 wöchentlich
+113 monatlich
+
+# das selbe wie oben, nur für die User-Seiten
+114 insgesamt
+115 heute
+116 diese Woche
+117 diesen Monat
+
+# Link zu den User- und misc-stats Seiten
+118 Benutzerliste
+119 Wer ist grade da?
+120 Verschiedenes
+
+# titel der channel-seite
+130 [chan] statistics index
+
+# Titel der TopX-Seiten...
+200 [chan]'s Top [topnr] Quassler seit Anbegin der Zeit
+201 Die heute aktivesten Labertaschen auf [chan]
+202 Die Top-Fasler der Woche auf [chan]
+203 Die monatlichen [chan] Statistiken
+
+# ... und die Überschrift Selbiger
+210 <center><H1>[chan]'s Top [topnr] Quassler seit Anbegin der Zeit</H1></center>
+210 <center><H1>[chan]'s Extrem-Chatter</H1></center>
+210 <center><H1>[chan]'s Tastatur-Quäler</H1></center>
+211 <center><H1>Die heute aktivesten Labertaschen auf [chan]</H1></center>
+211 <center><H1>Die heutige Top[topnr]-Liste der Chatter mit den schnellsten Fingern</H1></center>
+212 <center><H1>Die Top-Fasler der Woche auf [chan]</H1></center>
+213 <center><H1>Die monatlichen [chan] Statistiken</H1></center>
+
+# selbsterklärend...
+214 <center>Sortiert nach "%s".
+215 Größte Ansammlung (hoffentlich) menschlicher Wesen: [peak]</center>
+216 Nr
+217 Insgesamt beobachtete Benutzer: [totalusers]
+217 Mindestens [totalusers] User haben bisher auf [chan] ihre Zeit mit chatten verschwendet
+217 Mindestens [totalusers] Benutzer haben sich bereits in dieses Statistiken verewigt.
+217 Meine Datenbank muss in [chan] insgesamt [totalusers] Benutzer ertragen.
+218 (seit [chanstarted])
+
+# eigentlich auch selbsterklärend...
+220 Balken-Diagramm
+221 andere Channel
+
+# Zeitangaben (Plural und Singular)
+250 Jahr
+251 Jahre
+252 Woche
+253 Wochen
+254 Tag
+255 Tage
+256 Stunde
+257 Stunden
+258 Minute
+259 Minuten
+260 Sekunde
+261 Sekunden
+
+300 Total top [graphnr] graphs on channel [chan]
+301 Daily top [graphnr] graphs on channel [chan]
+302 Weekly top [graphnr] graphs on channel [chan]
+303 Monthly top [graphnr] graphs on channel [chan]
+
+310 <center><h1>Total top [graphnr] graphs on channel [chan]</h1></center>
+311 <center><h1>Daily top [graphnr] graphs on channel [chan]</h1></center>
+312 <center><h1>Weekly top [graphnr] graphs on channel [chan]</h1></center>
+313 <center><h1>Monthly top [graphnr] graphs on channel [chan]</h1></center>
+
+320 <font size=+1>Sortiert nach <b>%s</b></font>
+321 <center><font size=-1>[[int] %s insgesamt]</font></center>
+322 [andere]
+323 Tabelle
+
+# /chan/users
+400 [chan]'s Benutzer
+
+# /chan/onchan/
+410 Wer ist gerade in [chan]?
+420 Jetzt im Channel:
+430 inaktiv seit
+435 verschollen
+436 <-- meinereiner ^_^
+
+450 EMail Adresse
+451 Homepage
+
+# /chan/users/user
+500 Statistiken für [user] in [chan]
+501 <H1>Statistiken für [user] in [chan]</H1>
+502 Platz
+510 Zitat: "%s"
+520 [user] gab [int] verschiedene Wörter von sich. Dies sind die meistgenutzten:
+530 andere Benutzer
+
+# misc stats
+600 Verschiedene Statistiken in [chan]
+610 <center><font size=+2>Verschiedene Statistiken in [chan]</font>
+620 Durchschnittliche Userzahl:
+630 Heutige Topics:
+631 (gesetzt von [nick] um %s)
+640 [int] zufällig ausgewählte URLs
+641 (last mentioned by [nick] at %s)
+650 Die häufigsten
+651 Provider
+652 Länder
+660 [int] zufällig ausgewählte kicks
+670 Registrierte Benutzer gaben %d unterschiedliche Wörter von sich.
+671 Dies sind die beliebtesten:
+
+# Hier lohnt es sich wieder ein paar Worte zu verlieren. ^_^
+# Das hier ist der Untere Teil der misc-stats-Seiten. Ein Eintrag muss
+# folgendermaßen aussehen:
+#
+# BN <Stat-Typ> <Platz> <Text>
+#
+# Naja, eigentlich erklärt sich auch das von selbst... ich sollte nurnoch
+# erwähnen, daß alle Plätze des selben Typs in eine Tabellenzeile gepackt
+# werden.
+# Selbstverständlich kann man auch hier doppelte Einträge benutzen, aus
+# denen dann einer zufällig ausgewählt wird.
+
+BN nicks 1 <a href="../users/[user]/">[user]</a> konnte sich für keinen nick entscheiden und wechselte selbigen [value] mal.
+BN nicks 1 <a href="../users/[user]/">[user]</a> hatte die größte Auswahl an nicks ([value]).
+BN nicks 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> hat auch [value] Versuche gebraucht um sich zu entscheiden.</div></font>
+BN kicks 1 <a href="../users/[user]/">[user]</a> tat was nötig war und beseitigte [value] Störenfriede.
+BN kicks 1 <a href="../users/[user]/">[user]</a> war heute leicht reizbar und beförderte [value] Opfer aus dem Channel.
+BN kicks 1 Mit <a href="../users/[user]/">[user]</a> war heute nicht zu scherzen... [value] Opfer erfuhren dies auf die harte Art.
+BN kicks 2 <font size=-2><div align="right">Der zweit-aktivste Bestrafer war <a href="../users/[user]/">[user]</a> mit [value] kicks.</div></font>
+BN kicks 2 <font size=-2><div align="right">Auch <a href="../users/[user]/">[user]</a> verlor in [value] Fällen die Geduld.</div></font>
+BN kicks 2 <font size=-2><div align="right">Auch <a href="../users/[user]/">[user]</a>'s Toleranz war in [value] Fällen erschöpft.</div></font>
+BN smileys 1 <a href="../users/[user]/">[user]</a> war heute gut gelaunt und grinste [value] mal.
+BN smileys 1 <a href="../users/[user]/">[user]</a> hatte einen guten Tag und grinste [value] mal.
+BN smileys 1 <a href="../users/[user]/">[user]</a> hatte [value] mal einen recht strahlenden Gesichtsausdruck.
+BN smileys 2 <font size=-2><br><div align="right"><a href="../users/[user]/">[user]</a> genoss den Besuch in [chan] mit [value] smileys offensichtlich auch.</font></div>
+BN questions 1 <a href="../users/[user]/">[user]</a> weiß das Wissen Macht bedeutet und stellte [value] fragen.
+BN questions 2 <font size=-2><div align="right">Auch <a href="../users/[user]/">[user]</a> war [value] mal auf der Suche nach Input.</div></font>
+BN joins 1 <a href="../users/[user]/">[user]</a> wusste nicht so recht, ob er/sie bleiben sollte oder nicht und betrat [chan] [value] mal.
+BN joins 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> gab sich fast genauso viel Mühe für mass-joining gekickt zu werden.</div></font>
+BN vocables 1 <a href="../users/[user]/">[user]</a> hat mit [value] verschiedenen Wörtern das größte Vokabular.
+BN vocables 1 <a href="../users/[user]/">[user]</a> drückte sich mit Hilfe von [value] verschiedenen Wörtern recht vielseitig aus.
+BN vocables 2 <font size=-2><div align="right">Auch <a href="../users/[user]/">[user]'s</a> Wortschatz ist nicht zu verachten. ([value] Wörter)</div></font>
+
+# Wird nur für die user-seiten der statischen Webfiles gebraucht... 
+# ... aber die will ja hoffentlich niemand benutzen, oder? ^_^
+900 Feature not available with static webfiles.
+901 <center><font size=+2>Sorry, but this feature is not available with static webfiles. <br><br></font>Beg your bot-admin to activate livestats. ^_^</center>
+
+## Hier noch die Einträge für die Channel-Befehle
+1000 No such stat type.
+# !top10
+1001 Top[int](%s):
+1002 Heutige Top[int](%s):
+1003 Top[int] dieser Woche(%s):
+1004 Top[int] dieses Monats(%s):
+# !top10 word foo
+1005 Top[int]("%s"):
+
+1010 [nick], tut mir leid, aber ich habe keinerlei Statistiken über dich.
+1011 Sorry, aber ich weiß nix über [nick] in [chan].
+
+# !place
+1012 [nick] ist auf Platz #[int] von [totalusers].
+1013 Heute ist [nick] auf Platz #[int] von [totalusers].
+1014 Diese Woche ist [nick] auf Platz #[int] von [totalusers].
+1015 Diesen Monat ist [nick] auf Platz #[int] von [totalusers].
+
+# !wordstats
+1016 Tut mir leid, aber ich habe heute noch keine Wörter von [nick] in [chan] aufgeschnappt
+1017 [user]s meistgenutzten Wörter:
+
+# !topwords
+1018 Die beliebtesten Wörter in [chan]:

+ 396 - 0
stats.h

@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+#define SAVESTATSLENGTH 5000
+#define MAXSLANGLENGTH 2000
+
+
+#define TYPES "words letters started minutes topics lines actions modes bans kicks nicks joins smileys questions"
+#define T_GSTARTED -1
+#define T_LSTARTED -2
+#define T_PEAK -3
+#define T_WPL -4
+#define T_WORD -5
+#define T_VOCABLES -6
+#define T_QUOTE -7
+#define T_IDLE -8
+#define T_ERROR -999
+#define T_WORDS 0
+#define T_LETTERS 1
+#define T_MINUTES 2
+#define T_TOPICS 3
+#define T_LINES 4
+#define T_ACTIONS 5
+#define T_MODES 6
+#define T_BANS 7
+#define T_KICKS 8
+#define T_NICKS 9
+#define T_JOINS 10
+#define T_SMILEYS 11
+#define T_QUESTIONS 12
+#define TOTAL_TYPES 13
+#define TOTAL_SPECIAL_TYPES 8
+
+#define S_TOTAL 0
+#define S_TODAY 1
+#define S_DAILY 1
+#define S_WEEKLY 2
+#define S_MONTHLY 3
+
+#define RANGESTR today ? ((today == S_DAILY) ? SLDAILY : ((today == S_WEEKLY) ? SLWEEKLY : SLMONTHLY)) : SLTOTAL
+#define RANGESTR_LONG today ? ((today == S_DAILY) ? SLLTODAY : ((today == S_WEEKLY) ? SLLWEEKLY : SLLMONTHLY)) : SLLTOTAL
+#define ISLINK(x) x ? "" : "!"
+#define ISTEXT(x, y) x ? y : "&nbsp;"
+
+#define S_USERSUM 0
+#define S_USERCOUNTS 1
+
+#define SL_PRIVMSG 0
+#define SL_KICK 1
+#define SL_MODE 2
+#define SL_NICK 3
+#define SL_PART 4
+#define SL_QUIT 5
+#define SL_JOIN 6
+
+
+typedef struct stats_hosts {
+  struct stats_hosts *next;
+  char *host;
+  int nr;
+} hoststr;
+
+typedef struct stats_words {
+  struct stats_words *next;
+  struct stats_words *left;
+  struct stats_words *right;
+  char *word;
+  int nr;
+} wordstats;
+
+typedef struct stats_quote {
+  struct stats_quote *next;
+  char *quote;
+} quotestr;
+
+typedef struct stats_topic {
+  struct stats_topic *next;
+  char *topic;
+  char *by;
+  time_t when;
+} topicstr;
+
+struct stats_url {
+  struct stats_url *next;
+  char *url;
+  char *by;
+  int shown;
+  time_t when;
+};
+
+struct stats_kick {
+  struct stats_kick *next;
+  quotestr *log;
+  int shown;
+};
+
+typedef struct stats_local {
+  struct stats_local *next;
+  struct stats_local *snext[4][TOTAL_TYPES + TOTAL_SPECIAL_TYPES];
+  char *user;
+  struct stats_userlist *u;
+  time_t started;
+  long int values[4][TOTAL_TYPES];
+  wordstats *words;
+  wordstats *tree;
+  wordstats *word;
+  int vocables;
+  quotestr *quotes;
+  int quotefr;
+  int flag;
+} locstats;
+
+typedef struct stats_global {
+  struct stats_global *next;
+  char *chan;
+  time_t started;
+  int peak[4];
+  int users[2][24];
+  struct stats_local *local;
+  struct stats_local *slocal[4][TOTAL_TYPES + TOTAL_SPECIAL_TYPES];
+  wordstats *words;
+  topicstr *topics;
+  hoststr *hosts;
+  struct stats_url *urls;
+  quotestr *log;
+  quotestr *lastlog;
+  int log_length;
+  struct stats_kick *kicks;
+} globstats;
+
+struct stats_clientinfo {
+  int traffic;
+  int code;
+  char *browser;
+  char *referer;
+  char *cmd;
+};
+
+#define stats_info_access(i) ((struct stats_clientinfo *) dcc[(i)].u.other)
+
+static void sortstats(struct stats_global *, int, int);
+static void countvocables(globstats *);
+static int localstats_expmem(struct stats_local *);
+static int wordstats_expmem(wordstats *);
+static void free_stats();
+static void free_localstats(struct stats_local *);
+static void free_wordstats(wordstats *);
+static void reset_tstats();
+static void free_urls(struct stats_url *);
+static void tell_top(char *, char *, char *, char *, int, int, int);
+static void tell_place(char *, char *, char *, char *, char *, int);
+static void tell_stat(char *, char *, char *, char *, char *, int);
+static int typetoi(char *);
+static int slangtypetoi(char *);
+static void incrstats(char *, char *, int, int, int);
+static int getstats(char *, char *, char *, int);
+static locstats *findlocstats(char *, char *);
+static globstats *findglobstats(char *);
+static int countwords(char *);
+static void write_stats();
+static void read_stats();
+static int countsmileys(char *);
+static int countstatmembers(globstats *);
+static int gettotal(globstats *, int, int);
+static void stats_autoadd(memberlist *, char *);
+static int matchattr(struct userrec *, char *, char *);
+static void deloldstatusers();
+static void purgestats();
+static void sensor_minutely();
+static int validchan(char *);
+static void start_listen_livestats(int);
+static void stop_listen_livestats();
+static void display_livestats_accept(int, char *);
+static void timeout_livestats(int);
+#ifndef OLDBOT
+static void outdone_livestats(int);
+#endif
+static void timeout_listen_livestats(int);
+static void livestats_accept(int, char *, int);
+static void eof_livestats(int);
+static void display_livestats(int, char *);
+static void livestats_activity(int, char *, int);
+static void send_livestats(int, char *);
+static char *splitpath(char **);
+static void sort_stats_alphabetically(globstats *);
+static void sensor_peak(char *);
+static int stat_flood();
+static void resetlocstats(locstats *);
+static int expmem_livestats(void *);
+static void kill_livestats(int, void *);
+static void out_livestats(int, char *, void *);
+static void calcwordstats(char *, globstats *, char *, locstats *ls);
+static void incrwordstats(locstats *, char *, int, int);
+static void strlower(char *);
+static void sortwordstats(locstats *, globstats *);
+static void filt(char *);
+static char *filt2(char *);
+static char *filtbrackets(char *);
+static void tell_wordstats(char *, char *, char *, char *, char *);
+static void tell_topwords(char *, char *, char *, char *);
+static void nincrwordstats(globstats *, char *, int);
+static void addquote(char *, globstats *, char *, locstats *);
+static void free_quotes(quotestr *);
+static int quotes_expmem(quotestr *);
+static void do_globwordstats(globstats *);
+static int topics_expmem(topicstr *);
+static int urls_expmem(struct stats_url *);
+static int hosts_expmem(hoststr *);
+static void free_topics(topicstr *);
+static void free_hosts(hoststr *);
+static void addtopic(char *, char *, char *);
+static void do_graphs(int, int, globstats *, char *);
+static void do_toptalkers(int, int, globstats *, char *, char *);
+static void do_miscstats(int, char *);
+static int countquestions(char *);
+static int inactivechan(char *);
+static int gethour();
+static int getmonth();
+static void sensor_countusers();
+static void reset_mwstats(int);
+static int secretchan(char *);
+static int nostats(char *);
+static void sorthosts(struct stats_global *);
+static void addhost(char *, globstats *);
+static void tell_top_word(char *, char *, char *, char *, int, int, globstats *);
+static int quietstats(char *);
+static int track_stat_user(char *, char *);
+static void check_for_url(char *, char *, char *);
+static void free_kicks(struct stats_kick *);
+static int kicks_expmem(struct stats_kick *);
+#if EGG_IS_MIN_VER(10500)
+static void write_new_webstats();
+void stats_setsock(int, int);
+#endif
+static char *stats_duration(int);
+static int livestats_flood();
+static void display_users_on_chan(int, char *, struct chanset_t *);
+
+/* eggdrop-userfile-independent user management */
+struct stats_hostlist {
+  struct stats_hostlist *next;
+  char *mask;
+  time_t lastused;
+};
+
+struct stats_userlist {
+  struct stats_userlist *next;
+  char *user;
+  int list;
+  int addhosts;
+  struct stats_hostlist *hosts;
+  char *email;
+  char *homepage;
+};
+
+struct stats_memberlist {
+  struct stats_memberlist *next;
+  char *nick;
+  char *uhost;
+  time_t joined;
+  struct stats_userlist *user;
+  locstats *stats;
+};
+
+struct stats_chanset {
+  struct stats_chanset *next;
+  char *chan;
+  struct stats_memberlist *members;
+};
+
+static int suserlist_expmem(struct stats_userlist *);
+static int hostlist_expmem(struct stats_hostlist *);
+static int chanlist_expmem(struct stats_chanset *);
+static int memberlist_expmem(struct stats_memberlist *);
+static void free_suserlist(struct stats_userlist *);
+static void free_hostlist(struct stats_hostlist *);
+static void free_chanlist(struct stats_chanset *);
+static void free_memberlist(struct stats_memberlist *);
+static void free_one_chan(char *);
+
+static struct stats_memberlist *nick2suser(char *, char *);
+static struct stats_chanset *findschan(char *);
+static struct stats_chanset *initchan(char *);
+static void saddmember(char *, char *, char *, char *);
+static void strackmember(char *, char *, char *);
+static void skillmember(char *, char *);
+static struct stats_userlist *findsuser(char *);
+static struct stats_userlist *findsuser_by_name(char *);
+static struct stats_userlist *addsuser(char *, int, int);
+static void delsuser(char *user);
+static void saddhost(struct stats_userlist *, char *, time_t);
+static int sdelhost(struct stats_userlist *, char *);
+static void stats_autosadd(struct stats_memberlist *, struct stats_chanset *);
+static void nincrstats(locstats *, int, int);
+static locstats *initstats(char *, char *);
+static int listsuser(locstats *, char *);
+static int countsusers();
+static int counthosts();
+static void weed_userlink_from_chanset(struct stats_userlist *);
+static void weed_statlink_from_chanset(locstats *);
+static void weed_userlink_from_locstats(struct stats_userlist *);
+static int countmembers(struct stats_memberlist *);
+static void check_desynch();
+static void update_schannel_members();
+static void dump_suser(int, struct stats_userlist *);
+static void setemail(struct stats_userlist *, char *);
+static void sethomepage(struct stats_userlist *, char *);
+static void long_dprintf(int, char *);
+
+/* language system */
+
+struct slang_texts {
+  struct slang_texts *next;
+  char *text;
+  int dynamic;
+};
+
+struct slang_ids {
+  struct slang_ids *next;
+  int id;
+  int entries;
+  struct slang_texts *texts;
+};
+
+struct slang_types {
+  struct slang_types *next;
+  char *type;
+  int entries;
+  struct slang_texts *texts;
+};
+
+struct slang_lang {
+  struct slang_lang *next;
+  char *lang;
+  struct slang_ids *ids;
+  struct slang_types *types;
+  struct slang_bntypes *bignumbers;
+  char *durs[10];
+};
+
+struct slang_chan {
+  struct slang_chan *next;
+  char *chan;
+  char *lang;
+};
+
+struct slang_bnplaces {
+  struct slang_bnplaces *next;
+  int place;
+  int entries;
+  struct slang_texts *texts;
+};
+
+struct slang_bntypes {
+  struct slang_bntypes *next;
+  char *type;
+  struct slang_bnplaces *places;
+};
+
+static int slang_expmem();
+static void free_slang();
+static int loadslang(char *, char *);
+static void addslangitem(struct slang_lang *, int, char *);
+static void addslangtype(struct slang_lang *, char *);
+static void addslangbn(struct slang_lang *, char *);
+static char *getslang(int idnr);
+static char *getslangtype(char *);
+static char *dynamicslang(char *);
+static int isdynamicslang(char *);
+static char *dynamicslang(char *);
+static char *chanlang(char *);
+static void setchanlang(char *, char *);
+static void setslglobs(char *, int, int, time_t);
+static void setslnick(char *);
+static char *getdur(int);
+
+static void sortstats_wpl(struct stats_global *, int);
+static void sortstats_vocables(struct stats_global *, int);
+static void sortstats_word(struct stats_global *, int);
+static void sortstats_idle(struct stats_global *, int);
+static void maskstricthost(const char *, char *);

+ 243 - 0
stats.lang

@@ -0,0 +1,243 @@
+#######################################
+#
+# Language file for stats.mod v1.3.0
+# ----------------------------------
+#
+#
+# Usage is pretty self-explaining.
+# If you add more than one line per item, a random one will be selected
+# each time when it's used.
+#
+# You can use some variables, but not every var in every place.
+# (using [user] on the index page would be nonsense, for example.
+#  In such cases, the var is unitialized (which might even cause a crash))
+#
+# Also be only aware of the fact, that [user] is not the same as [nick] and
+# [value] not the same as [int]. (which has something to do with the internal
+# processing of those vars.)
+#
+##########################################
+
+# rename the names of the different stat types, if you don't like them.
+# if no name is defined, the standard will be used.
+T minutes time wasted
+T wpl words/line
+T smileys smilies
+T idle idle-factor
+
+# things you want to put into the header of each html file (CSS for example)
+1 <style type="text/css"><!--  A:link { text-decoration: none; }  A:visited { text-decoration: none;}  A:active { text-decoration: none;}  A:hover { text-decoration: underline;} --></style>
+
+# the bodytag (use this to change the colors of the pages)
+5 <BODY BGCOLOR=#000000 TEXT=#1A9DFF LINK=#00D993 VLINK=#71C2FF ALINK=#2FFFBB>
+
+# These two items get inserted at the top and bottom of every page,
+# so if you want to insert livestats into an existing page design,
+# you might add menu bars and similar stuff here.
+# If not, just add an empty item.
+
+# Example1 (header and footer from external files):
+#10 [source head.html]
+#11 [source footer.html]
+
+# Example2 (no header or footer at all):
+10
+11
+
+# title of the index page
+100 [bot]'s Livestats
+100 Livestats of [bot]
+100 Livestats
+100 Livestats - Channel Overview
+
+# text that describes the link to the top talkers
+105 top[topnr]
+
+# time range of the stats
+110 total
+111 daily
+112 weekly
+113 monthly
+
+# same as above, but for user pages
+114 total
+115 today
+116 this week
+117 this month
+
+# link to user- and misc-stats pages
+118 userlist
+119 Who's on now?
+120 misc stats
+
+# title of channel-page
+130 [chan] statistics index
+
+# <title>s of toptalker pages
+200 Total top [topnr] talkers on channel [chan]
+201 Daily top [topnr] talkers on channel [chan]
+202 Weekly top [topnr] talkers on channel [chan]
+203 Monthly top [topnr] talkers on channel [chan]
+
+# heads of toptalker pages
+210 <center><H1>Total top [topnr] talkers on channel [chan]</H1></center>
+211 <center><H1>Daily top [topnr] talkers on channel [chan]</H1></center>
+212 <center><H1>Weekly top [topnr] talkers on channel [chan]</H1></center>
+213 <center><H1>Monthly top [topnr] talkers on channel [chan]</H1></center>
+
+# self-explaining...
+214 <center>Ordered by "%s" (to change it click corresponding table field).
+215 User peak: [peak]</center>
+216 No
+217 Total users: [totalusers]
+218 (stats logged since [chanstarted])
+
+# link to graphs
+220 graphs
+
+# link back to the index
+221 other channels
+
+# time string (singular and plural)
+250 year
+251 years
+252 week
+253 weeks
+254 day
+255 days
+256 hour
+257 hours
+258 minute
+259 minutes
+
+# <title>s of graph pages
+300 Total top [graphnr] graphs on channel [chan]
+301 Daily top [graphnr] graphs on channel [chan]
+302 Weekly top [graphnr] graphs on channel [chan]
+303 Monthly top [graphnr] graphs on channel [chan]
+
+# heads of graph pages
+310 <center><h1>Total top [graphnr] graphs on channel [chan]</h1></center>
+311 <center><h1>Daily top [graphnr] graphs on channel [chan]</h1></center>
+312 <center><h1>Weekly top [graphnr] graphs on channel [chan]</h1></center>
+313 <center><h1>Monthly top [graphnr] graphs on channel [chan]</h1></center>
+
+# self-explaining
+320 <font size=+1>Ordered by <b>%s</b></font>
+321 <center><font size=-1>[[int] %s total]</font></center>
+322 [others]
+323 table
+
+# /chan/users/
+400 users of [chan]
+
+# /chan/onchan
+410 Users who are currently on [chan]
+420 Currently on channel:
+430 idle time
+435 netsplitted
+436 <-- it's me ^_^
+
+450 email address
+451 homepage
+
+# userpage title
+500 Stats for [user] in [chan]
+# userpage head
+501 <H1>Stats for [user] in [chan]</H1>
+502 Place
+# self-explaining...
+510 random quote: "%s"
+520 [user] spoke [int] different words today. These are the most used ones:
+530 other users
+
+## Misc Stats
+# title
+600 Misc Stats in [chan]
+# head
+610 <center><font size=+2>Misc Stats of Today in [chan]</font>
+# self-explaining...
+620 Average users:
+630 Today's Topics:
+631 (set by [nick] at %s)
+640 [int] random URLs
+641 (last mentioned by [nick] at %s)
+650 Most used
+651 ISPs
+652 TLDs
+660 [int] random kicks
+670 Registered users spoke %d different words
+671 These are the most used ones:
+
+# The last part of the misc stats, big numbers. That's how you use it:
+# BN <type> <place> <text>
+# All places of the same type will be in one table row.
+BN nicks 1 <a href="../users/[user]/">[user]</a> couldn't decide which nick is the best and changed it [value] times.
+BN nicks 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> was also curious which of his [value] nicks is perfect.</div></font>
+BN kicks 1 <a href="../users/[user]/">[user]</a> did what was necessary and kicked [value] people.
+BN kicks 2 <font size=-2><div align="right">The second most active punisher was <a href="../users/[user]/">[user]</a>([value] kicks).</div></font>
+BN smileys 1 <a href="../users/[user]/">[user]</a> had a happy day and smiled [value] times.
+BN smileys 2 <font size=-2><br><div align="right"><a href="../users/[user]/">[user]</a> also enjoyed his/her visit with [value] smileys</font></div>
+BN questions 1 <a href="../users/[user]/">[user]</a> knows that wisdom is the real wealth and asked [value] questions.
+BN questions 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> was also seeking for input [value] times.</div></font>
+BN joins 1 <a href="../users/[user]/">[user]</a> wasn't too sure whether he/she should stay or not and visited [chan] [value] times
+BN joins 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> is the second one who tried to get kicked for mass-joining ([value] joins)</div></font>
+BN wpl 1 <a href="../users/[user]/">[user]</a> likes long speeches and wrote an average of [value] words per line.
+BN wpl 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> is another fan of long, expressive sentences with [value] words/line.</div></font>
+BN vocables 1 <a href="../users/[user]/">[user]</a> has the largest vocabulary with [value] different words.
+BN vocables 2 <font size=-2><div align="right"><a href="../users/[user]/">[user]</a> used almost as much vocables ([value])</div></font>
+
+## This is for the file that explains that there are no stats for 
+## every single user in the static webfiles.
+## I hope you don't need this! ^_^
+# title
+900 Feature not available with static webfiles.
+# body
+901 <center><font size=+2>Sorry, but this feature is not available with static webfiles. <br><br></font>Beg your bot-admin to activate livestats. ^_^</center>
+
+## public commands
+
+1000 No such stat type.
+
+# !top10
+1001 Top[int](%s):
+1002 Today's Top[int](%s):
+1003 Weekly Top[int](%s):
+1004 Monthly Top[int](%s):
+# !top10 word foo
+1005 Top [int]("%s"):
+
+1010 [nick], sorry, I don't have any stats about you.
+1011 Sorry, I don't have any stats about [nick] in [chan].
+
+# !place
+1012 [nick] is on place #[int] out of [totalusers].
+1013 Today, [nick] is on place #[int] out of [totalusers].
+1014 This week, [nick] is on place #[int] out of [totalusers].
+1015 This week, [nick] is on place #[int] out of [totalusers].
+
+# !wordstats
+1016 Sorry, I don't have any wordstats about [nick] in [chan]
+1017 words most used by [user]:
+
+# !topwords
+1018 Most used words in [chan]:
+
+## /msg <bot> SETEMAIL|SETHOMEPAGE
+1020 Sorry, I don't recognize you.
+1021 Usage: /msg <bot> SETEMAIL <emailadress>
+1022 Successfully set new email address for you.
+1023 Usage: /msg <bot> SETHOMEPAGE <homepage>
+1024 Successfully set homepage address for you.
+
+## /msg <bot> top10
+2030 Sorry, that is not a valid channel. Usage: top10 <chan>
+2031 Sorry, that is not a valid channel. Usage: ttop10 <chan>
+2032 Sorry, that is not a valid channel. Usage: top20 <chan>
+2033 Sorry, that is not a valid channel. Usage: ttop20 <chan>
+2034 Sorry, that is not a valid channel. Usage: place <chan>
+2035 Sorry, that is not a valid channel. Usage: tplace <chan>
+2036 Sorry, that is not a valid channel. Usage: stat <chan> [user]
+2037 Sorry, that is not a valid channel. Usage: tstat <chan> [user]
+2038 Sorry, that is not a valid channel. Usage: wordstats <chan> [user]
+2039 Sorry, that is not a valid channel. Usage: topwords <chan>

+ 187 - 0
tclstats.c

@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int tcl_incrstats STDVAR
+{
+  int set, type;
+
+  Context;
+  BADARGS(5, 6, " user chan type value ?set?");
+  if (argc == 6)
+    set = atoi(argv[5]);
+  else
+    set = 0;
+  type = typetoi(argv[3]);
+  if (type == -3) {
+    Tcl_AppendResult(irp, "invalid type", NULL);
+    return TCL_ERROR;
+  }
+  incrstats(argv[1], argv[2], type, atoi(argv[4]), set);
+  Context;
+  return TCL_OK;
+}
+
+static int tcl_getstats STDVAR
+{
+  char s[30];
+  int today = 0;
+
+  Context;
+  BADARGS(4, 5, " user chan type ?today?");
+  if (typetoi(argv[3]) < 0) {
+    Tcl_AppendResult(irp, "invalid type", NULL);
+    return TCL_ERROR;
+  }
+  if (argc == 5)
+    today = atoi(argv[4]);
+  if ((today != 1) && (today != 0)) {
+    Tcl_AppendResult(irp, "invalid today parameter. Must be 0 or 1.", NULL);
+    return TCL_ERROR;
+  }
+  sprintf(s, "%d", getstats(argv[1], argv[2], argv[3], today));
+  Tcl_AppendResult(irp, s, NULL);
+  Context;
+  return TCL_OK;
+}
+
+static int tcl_livestats STDVAR
+{
+  int port;
+
+  Context;
+  BADARGS(2, 2, " port");
+  if (!strcasecmp(argv[1], "off") || !strcasecmp(argv[1], "0")) {
+    stop_listen_livestats();
+    return TCL_OK;
+  }
+  port = atoi(argv[1]);
+  if (port < 1) {
+    Tcl_AppendResult(irp, "invalid port", NULL);
+    return TCL_ERROR;
+  }
+  start_listen_livestats(port);
+  return TCL_OK;
+}
+
+static int tcl_resetuser STDVAR
+{
+  char *user, *chan;
+  locstats *ls;
+
+  Context;
+  BADARGS(3, 3, " user channel");
+  user = argv[1];
+  chan = argv[2];
+  ls = findlocstats(chan, user);
+  if (!ls) {
+    Tcl_AppendResult(irp, "couldn't find stats for user", NULL);
+    return TCL_ERROR;
+  }
+  resetlocstats(ls);
+  return TCL_OK;
+}
+
+static int tcl_loadslang STDVAR
+{
+  int ret;
+
+  Context;
+  BADARGS(2, 3, " [lang] slangfile");
+  if (argc == 3)
+    ret = loadslang(argv[1], argv[2]);
+  else
+    ret = loadslang(NULL, argv[1]);
+  if (!ret) {
+    Tcl_AppendResult(irp, "Couldn't open slang file!!!", NULL);
+    return TCL_ERROR;
+  }
+  return TCL_OK;
+}
+
+static int tcl_resetslang STDVAR
+{
+  Context;
+  free_slang();
+  slangs = NULL;
+  slangchans = NULL;
+  return TCL_OK;
+}
+
+static int tcl_getslang STDVAR
+{
+  Context;
+  BADARGS(2, 2, " ID");
+  Tcl_AppendResult(irp, getslang(atoi(argv[1])), NULL);
+  return TCL_OK;
+}
+
+static int tcl_nick2suser STDVAR
+{
+  struct stats_memberlist *m;
+  Context;
+  BADARGS(3, 3, " nick channel");
+  m = nick2suser(argv[1], argv[2]);
+  if (m && m->user)
+    Tcl_AppendResult(irp, m->user->user, NULL);
+  else
+    Tcl_AppendResult(irp, "*", NULL);
+  return TCL_OK;
+}
+
+static int tcl_setchanslang STDVAR
+{
+  Context;
+  BADARGS(3, 3, " channel language");
+  setchanlang(argv[1], argv[2]);
+  return TCL_OK;
+}
+
+static int tcl_findsuser STDVAR
+{
+  struct stats_userlist *u;
+  struct userrec *ou;
+
+  Context;
+  BADARGS(2, 2, " nick!user@host");
+  ou = get_user_by_host(argv[1]);
+  if (ou)
+	Tcl_AppendResult(irp, ou->handle, NULL);
+  else {
+    u = findsuser(argv[1]);
+    if (u)
+      Tcl_AppendResult(irp, u->user, NULL);
+    else
+      Tcl_AppendResult(irp, "*", NULL);
+  }
+  return TCL_OK;
+}
+
+static tcl_cmds mytcls[] =
+{
+  {"incrstats", tcl_incrstats},
+  {"getstats", tcl_getstats},
+  {"livestats", tcl_livestats},
+  {"resetuser", tcl_resetuser},
+  {"nick2suser", tcl_nick2suser},
+  {"loadslang", tcl_loadslang},
+  {"resetslang", tcl_resetslang},
+  {"getslang", tcl_getslang},
+  {"setchanslang", tcl_setchanslang},
+  {"findsuser", tcl_findsuser},
+  {0, 0}
+};

+ 302 - 0
user.c

@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+static int stats_checkhand (char *oldnick, char *newnick)
+{
+
+  Context;
+  if (track_stat_user(oldnick, newnick))
+    putlog(LOG_MISC, "*", "Stats.mod: Transferred stats from %s to %s", oldnick, newnick);
+  Context;
+  return 1;
+}
+
+static cmd_t stats_nkch[] =
+{
+  {"*", "", (Function) stats_checkhand, "stat:nkch"},
+  {0, 0, 0, 0}
+};
+
+static void stats_autoadd(memberlist *m, char *chan)
+{
+  struct userrec *u;
+  char s[121], s2[121], host[121];
+  struct xtra_key *xk;
+  time_t tt;
+
+  Context;
+  if (!use_userfile)
+    return;
+  if (autoadd < 0)
+    return;
+  if (!m->nick[0])
+    return;
+  if ((now - m->joined) < (autoadd * 60))
+    return;
+  if (match_my_nick(m->nick))
+    return;
+  if ((strlen(m->nick) + strlen(m->userhost)) >= 120)
+    return;
+  sprintf(s, "%s!%s", m->nick, m->userhost);
+  if (strchr(s, '*') || strchr(s, '?'))
+    return;     /* don't add users with wildcards in their hostmask */
+  maskhost(s, host);
+  u = get_user_by_host(s);
+  if (u)
+    return;
+  u = get_user_by_handle(userlist, m->nick);
+  if (u) {
+    if (!matchattr(u, badflags, chan)) {
+      addhost_by_handle(m->nick, host);
+      putlog(LOG_MISC, "*", "Stats.mod: Added hostmask %s to %s.", host, m->nick);
+      return;
+    }
+  } else {
+    userlist = adduser(userlist, m->nick, host, "-", USER_DEFAULT);
+    u = get_user_by_handle(userlist, m->nick);
+    xk = user_malloc(sizeof(struct xtra_key));
+    xk->key = user_malloc(7);
+    strcpy(xk->key, "AADDED");
+    xk->data = user_malloc(10);
+    sprintf(xk->data, "%09lu", now);
+    set_user(&USERENTRY_XTRA, u, xk);
+    tt = now;
+    strftime(s2, 120, "Added by Stats.mod on %d.%m.%Y %H:%M.", localtime(&tt));
+    /* sprintf(s2, "Added by Stats.mod on %s", ctime(&tt)); */
+    set_user(&USERENTRY_COMMENT, u, (void *) s2);
+    set_handle_laston(chan, u, now);
+    putlog(LOG_MISC, "*", "Stats.mod: Added %s to userlist.", m->nick);
+  }
+  Context;
+}
+
+static void deloldstatusers()
+{
+  struct laston_info *li;
+  struct xtra_key *xk;
+  struct userrec *u, *lu;
+  int changed, del;
+  struct chanset_t *chan;
+  struct stats_userlist *su, *lsu;
+  struct stats_hostlist *h, *lh;
+
+  if (stat_expire_user < 1)
+    return;
+  u = userlist;
+  while (u) {
+    changed = 0;
+    del = 1;
+    li = get_user(&USERENTRY_LASTON, u);
+    if (li) {
+      if ((now - li->laston) > (stat_expire_user * 86400)) {
+        for (xk = get_user(&USERENTRY_XTRA, u); (xk && del); xk = xk->next) {
+          if (!strcasecmp(xk->key, "AADDED")) {
+            if(!get_user(&USERENTRY_PASS, u)) {
+	      for (chan = chanset; chan; chan = chan->next) {
+#if EGG_IS_MIN_VER(10500)
+                if (matchattr(u, badflags, chan->dname)) {
+#else
+                if (matchattr(u, badflags, chan->name)) {
+#endif
+		  del = 0;
+		  break;
+		}
+	      }
+	      if (!del)
+	        break;
+              putlog(LOG_MISC, "*", "Stats.mod: %s wasn't seen for over %d days. Deleted from the eggdrop userfile.", u->handle, stat_expire_user);
+              lu = u->next;
+              deluser(u->handle);
+              u = lu;
+              changed = 1;
+              break;
+            }
+          }
+        }
+      }
+    }
+    if (!changed)
+      u = u->next;
+  }
+  su = suserlist;
+  lsu = NULL;
+  while (su) {
+    h = su->hosts;
+    lh = NULL;
+    while (h) {
+      if ((now - h->lastused) > (stat_expire_user * 86400)) {
+	putlog(LOG_MISC, "*", "Stats.Mod: %s didn't use the hostmask %s during the last %d days. Removing from hostlist...",
+	       su->user, h->mask, stat_expire_user);
+	nfree(h->mask);
+	if (lh)
+	  lh->next = h->next;
+	else
+	  su->hosts = h->next;
+	nfree(h);
+	if (lh)
+	  h = lh->next;
+	else
+	  h = su->hosts;
+      } else {
+	lh = h;
+	h = h->next;
+      }
+    }
+    if (!su->hosts) {
+      u = get_user_by_handle(userlist, su->user);
+      if (u) {
+        lsu = su;
+        su = su->next;
+        continue;
+      }
+      putlog(LOG_MISC, "*", "Stats.Mod: All of %s's hosts expired. Deleting stat user...", su->user);
+      nfree(su->user);
+      if (lsu)
+        lsu->next = su->next;
+      else
+        suserlist = su->next;
+      weed_userlink_from_chanset(su);
+      weed_userlink_from_locstats(su);
+      nfree(su);
+      if (lsu)
+        su = lsu->next;
+      else
+        su = suserlist;
+    } else {
+      lsu = su;
+      su = su->next;
+    }
+  }
+}
+
+static void purgestats()
+{
+  globstats *gs, *gs2;
+  locstats *ls, *ls2;
+  locstats *sl, *sl2;
+  int i, ii, kill;
+  struct stats_userlist *u;
+  struct userrec *u2;
+  struct chanset_t *chan;
+
+  Context;
+  gs = sdata;
+  gs2 = NULL;
+  while (gs) {
+#ifndef OLDBOT
+    chan = findchan_by_dname(gs->chan);
+#else
+    chan = findchan(gs->chan);
+#endif
+    if (chan && gs->local) {
+      ls = gs->local;
+      ls2 = NULL;
+      while (ls) {
+        kill = 1;
+        u2 = get_user_by_handle(userlist, ls->user);
+        u = findsuser_by_name(ls->user);
+        if (u2 || u) {
+          for (i = 0; i < TOTAL_TYPES; i++) {
+            if (ls->values[S_TOTAL][i] != 0) {
+              kill = 0;
+              break;
+            }
+          }
+          if (!kill) {
+	    if (use_userfile && u2) {
+	      if (strcmp(ls->user, u2->handle)) {
+	        debug2("Stats.mod: Transferred stats from %s to %s", ls->user, u2->handle);
+                nfree(ls->user);
+                ls->user = nmalloc(strlen(u2->handle) + 1);
+                strcpy(ls->user, u2->handle);
+	      }
+	    } else if (!use_userfile && u) {
+	      if (strcmp(ls->user, u->user)) {
+	        debug2("Stats.mod: Transferred stats from %s to %s", ls->user, u->user);
+                nfree(ls->user);
+                ls->user = nmalloc(strlen(u->user) + 1);
+                strcpy(ls->user, u->user);
+	      }
+	    }
+          }
+        }
+        if (kill) {
+          putlog(LOG_MISC, "*", "Stats.mod: Deleting stats for %s in %s(empty data or no such user)", ls->user, gs->chan);
+          for (i = 0; i < 4; i++) {
+            for (ii = 0; ii < (TOTAL_TYPES + TOTAL_SPECIAL_TYPES); ii++) {
+              sl = gs->slocal[i][ii];
+              sl2 = NULL;
+              while (sl) {
+                if (!rfc_casecmp(sl->user, ls->user))
+                  break;
+                sl2 = sl;
+                sl = sl->snext[i][ii];
+              }
+              if (sl) {
+                if (sl2)
+                  sl2->snext[i][ii] = sl->snext[i][ii];
+                else
+                  gs->slocal[i][ii] = sl->snext[i][ii];
+              } else
+                putlog(LOG_MISC, "*", "WARNING!!! %s not found in sorted list ([%d][%d])! Corrupted data?", ls->user, i, ii);
+            }
+          }
+          if (ls2)
+            ls2->next = ls->next;
+          else
+            gs->local = ls->next;
+          nfree(ls->user);
+          free_wordstats(ls->words);
+          free_quotes(ls->quotes);
+          weed_statlink_from_chanset(ls);
+          nfree(ls);
+          if (ls2)
+            ls = ls2->next;
+          else
+            ls = gs->local;
+        } else {
+          ls2 = ls;
+          ls = ls->next;
+        }
+      }
+      gs2 = gs;
+      gs = gs->next;
+    } else {
+      putlog(LOG_MISC, "*", "Stats.mod: Deleting stats for %s. (empty data or no such channel)", gs->chan);
+      free_one_chan(gs->chan);
+      if (gs2)
+        gs2->next = gs->next;
+      else
+        sdata = gs->next;
+      free_localstats(gs->local);
+      free_wordstats(gs->words);
+      free_topics(gs->topics);
+      free_urls(gs->urls);
+      free_quotes(gs->log);
+      free_hosts(gs->hosts);
+      free_kicks(gs->kicks);
+      nfree(gs->chan);
+      nfree(gs);
+      if (gs2)
+        gs = gs2->next;
+      else
+        gs = sdata;
+    }
+  }
+  Context;
+}

+ 627 - 0
userrec.c

@@ -0,0 +1,627 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+
+/* nick2suser():
+ * searches the channel list for a nick and returns a pointer
+ * to his/her user-struct, if found.
+ */
+static struct stats_memberlist *nick2suser(char *nick, char *channel)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m, *lm;
+
+  Context;
+  chan = findschan(channel);
+  if (!chan)
+    chan = initchan(channel);
+  if (chan) {
+    lm = NULL;
+    m = chan->members;
+    while (m) {
+      if (!rfc_casecmp(m->nick, nick)) {
+        if (lm) {
+          // move this entry to the top of the list
+          // since chatting means writing several sentences,
+          // we'll probably need this item again pretty soon.
+          // => should be faster
+          lm->next = m->next;
+          m->next = chan->members;
+          chan->members = m;
+        }
+        if (!m->user)
+          return NULL;
+        else
+          return m;
+      }
+      lm = m;
+      m = m->next;
+    }
+
+
+  }
+  return NULL;
+}
+
+static struct stats_chanset *findschan(char *channel)
+{
+  struct stats_chanset *chan;
+
+  for (chan = schans; chan; chan = chan->next)
+    if (!strcasecmp(chan->chan, channel))
+      return chan;
+  return NULL;
+}
+
+static struct stats_chanset *initchan(char *channel)
+{
+  struct chanset_t *ch;
+  memberlist *m;
+  struct stats_chanset *chan, *nchan;
+  char *host;
+
+  Context;
+  debug1("Stats.mod: Initing chanset for %s", channel);
+  ch = findchan_by_dname(channel);
+  if (!ch) {
+    debug1("initchan: no such chan %s", channel);
+    return NULL;
+  }
+  if (ch->channel.members < 1) {
+    debug2("initchan: %s members == %d", channel, ch->channel.members);
+    return NULL;
+  }
+  chan = schans;
+  while (chan && chan->next)
+    chan = chan->next;
+  nchan = nmalloc(sizeof(struct stats_chanset));
+  nchan->chan = nmalloc(strlen(channel) + 1);
+  strcpy(nchan->chan, channel);
+  nchan->next = NULL;
+  nchan->members = NULL;
+  if (chan)
+    chan->next = nchan;
+  else
+    schans = nchan;
+  for (m = ch->channel.member; m; m = m->next) {
+    if (!m->userhost) {
+      debug2("Stats.Mod: No host for %s in %s available, yet.",
+             m->nick, channel);
+      continue;
+    }
+    if (m->user == NULL) {
+      host = nmalloc(strlen(m->nick) + 1 + strlen(m->userhost) + 1);
+      sprintf(host, "%s!%s", m->nick, m->userhost);
+      m->user = get_user_by_host(host);
+      nfree(host);
+    }
+    saddmember(m->nick, m->userhost, m->user ? m->user->handle : "*", channel);
+  }
+  return nchan;
+}
+
+static void saddmember(char *nick, char *uhost, char *hand, char *channel)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m, *nm;
+  char *host;
+
+  Context;
+  if (!nick) {
+    debug0("kein nick");
+    return;
+  }
+  Context;
+  if (!nick[0]) {
+    debug0("kein nick0");
+    return;
+  }
+  debug3("saddmember(%s, %s, %s)", channel, nick, uhost);
+  chan = findschan(channel);
+  if (!chan)
+    return;
+  m = chan->members;
+  while (m && m->next)
+    m = m->next;
+  nm = nmalloc(sizeof(struct stats_memberlist));
+  nm->nick = nmalloc(strlen(nick) + 1);
+  strcpy(nm->nick, nick);
+  nm->uhost = nmalloc(strlen(uhost) + 1);
+  strcpy(nm->uhost, uhost);
+  if (hand[0] != '*') {
+    nm->user = findsuser_by_name(hand);
+    if (!nm->user) {
+      nm->user = addsuser(hand, 1, 1);
+      debug1("Stats.Mod: Created suserrec for %s.", hand);
+    }
+  } else {
+    host = nmalloc(strlen(nick) + 1 + strlen(uhost) + 1);
+    sprintf(host, "%s!%s", nick, uhost);
+    nm->user = findsuser(host);
+    nfree(host);
+  }
+  if (nm->user)
+    nm->stats = findlocstats(channel, nm->user->user);
+  else
+    nm->stats = NULL;
+  nm->next = NULL;
+  nm->joined = now;
+  if (m)
+    m->next = nm;
+  else
+    chan->members = nm;
+}
+
+static void strackmember(char *channel, char *oldnick, char *newnick)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m;
+
+  Context;
+  chan = findschan(channel);
+  if (!chan)
+    return;
+  for (m = chan->members; m; m = m->next) {
+    if (!rfc_casecmp(m->nick, oldnick)) {
+      nfree(m->nick);
+      m->nick = nmalloc(strlen(newnick) + 1);
+      strcpy(m->nick, newnick);
+      return;
+    }
+  }
+}
+
+static void skillmember(char *channel, char *nick)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m, *mm;
+
+  Context;
+  chan = findschan(channel);
+  if (!chan)
+    return;
+  m = chan->members;
+  mm = NULL;
+  while (m) {
+    if (!rfc_casecmp(m->nick, nick)) {
+      nfree(m->nick);
+      nfree(m->uhost);
+      if (mm)
+        mm->next = m->next;
+      else
+        chan->members = m->next;
+      nfree(m);
+      return;
+    }
+    mm = m;
+    m = m->next;
+  }
+  putlog(LOG_MISC, "*", "Stats.mod: skillmember(%s, %s) failed!!!", channel, nick);
+}
+
+
+/* Find the stats-user that belongs to a hostmask
+*/
+static struct stats_userlist *findsuser(char *host)
+{
+  struct stats_userlist *user, *u;
+  struct stats_hostlist *h, *h2;
+  int len = 0;
+
+  Context;
+  u = NULL;
+  h2 = NULL;
+  for (user = suserlist; user; user = user->next) {
+    for (h = user->hosts; h; h = h->next) {
+      /* the longest hostmask gives the best match */
+      if (!len || (strlen(h->mask) > len)) {
+        if (wild_match(h->mask, host)) {
+          u = user;
+          h2 = h;
+          len = strlen(h->mask);
+        }
+      }
+    }
+  }
+  if (u) {
+    h2->lastused = now;
+    return u;
+  }
+  return NULL;
+}
+
+static struct stats_userlist *findsuser_by_name(char *user)
+{
+  struct stats_userlist *u;
+
+  Context;
+  for (u = suserlist; u; u = u->next)
+    if (!rfc_casecmp(u->user, user))
+      return u;
+  return NULL;
+}
+
+static struct stats_userlist *addsuser(char *user, int list, int addhosts)
+{
+  struct stats_userlist *u, *nu;
+
+  Context;
+  for (u = suserlist; u; u = u->next)
+    if (!rfc_casecmp(u->user, user))
+      return u;
+  u = suserlist;
+  while (u && u->next)
+    u = u->next;
+  nu = nmalloc(sizeof(struct stats_userlist));
+  nu->user = nmalloc(strlen(user) + 1);
+  strcpy(nu->user, user);
+  nu->list = list;
+  nu->addhosts = addhosts;
+  nu->next = NULL;
+  nu->hosts = NULL;
+  nu->email = NULL;
+  nu->homepage = NULL;
+  if (u)
+    u->next = nu;
+  else
+    suserlist = nu;
+  return nu;
+}
+
+static void delsuser(char *user)
+{
+  struct stats_userlist *u, *lu;
+
+  Context;
+  debug1("Deleting %s...", user);
+  u = suserlist;
+  lu = NULL;
+  while (u) {
+    if (!rfc_casecmp(u->user, user)) {
+      if (lu)
+        lu->next = u->next;
+      else
+        suserlist = u->next;
+      free_hostlist(u->hosts);
+      if (u->email)
+        nfree(u->email);
+      if (u->homepage)
+        nfree(u->homepage);
+      nfree(u->user);
+      weed_userlink_from_chanset(u);
+      weed_userlink_from_locstats(u);
+      nfree(u);
+      if (lu)
+        u = lu->next;
+      else
+        u = suserlist;
+    } else {
+      lu = u;
+      u = u->next;
+    }
+  }
+}
+
+static void saddhost(struct stats_userlist *u, char *host, time_t lastused)
+{
+  struct stats_hostlist *h, *nh;
+
+  Context;
+  for (h = u->hosts; h; h = h->next)
+    if (!rfc_casecmp(h->mask, host))
+      return;
+  h = u->hosts;
+  while (h && h->next)
+    h = h->next;
+  nh = nmalloc(sizeof(struct stats_hostlist));
+  nh->mask = nmalloc(strlen(host) + 1);
+  strcpy(nh->mask, host);
+  nh->lastused = lastused;
+  nh->next = NULL;
+  if (h)
+    h->next = nh;
+  else
+    u->hosts = nh;
+}
+
+static int sdelhost(struct stats_userlist *u, char *host)
+{
+  struct stats_hostlist *h, *lh;
+
+  Context;
+  h = u->hosts;
+  lh = NULL;
+  while (h) {
+    if (!rfc_casecmp(h->mask, host)) {
+      nfree(h->mask);
+      if (lh)
+        lh->next = h->next;
+      else
+        u->hosts = h->next;
+      nfree(h);
+      return 1;
+    }
+    lh = h;
+    h = h->next;
+  }
+  return 0;
+}
+
+static void stats_autosadd(struct stats_memberlist *m, struct stats_chanset *chan)
+{
+  struct stats_userlist *u;
+  struct userrec *uu;
+  char *mhost, *host, *egghost;
+  struct chanset_t *eggchan;
+  memberlist *eggmember;
+
+  Context;
+  if (autoadd < 0)
+    return;
+  if ((now - m->joined) < (autoadd * 60))
+    return;
+  if (m->user) {
+    debug3("Stats.Mod: stats_autosadd called for %s in %s, but m->user already belongs to %s",
+           m->nick, chan->chan, m->user->user);
+    return;
+  }
+#ifndef OLDBOT
+  eggchan = findchan_by_dname(chan->chan);
+#else
+  eggchan = findchan(chan->chan);
+#endif
+  if (!eggchan) {
+    putlog(LOG_MISC, "*", "Stats.Mod: Couldn't find eggdrop channel data while autoadding in %s.", chan->chan);
+    return;
+  }
+  eggmember = ismember(eggchan, m->nick);
+  if (!eggmember) {
+    putlog(LOG_MISC, "*", "Stats.Mod: Couldn't find eggdrop member data while autoadding %s in %s.", m->nick, chan->chan);
+    return;
+  }
+  egghost = eggmember->userhost;
+  if (!egghost) {
+    debug2("Stats.Mod: Couldn't autoadd %s in %s because there isn't an validated host, yet.", m->nick, chan->chan);
+    return;
+  }
+  u = findsuser_by_name(m->nick);
+  host = nmalloc(strlen(egghost) + strlen(m->nick) + 2);
+  sprintf(host, "%s!%s", m->nick, egghost);
+  mhost = nmalloc(strlen(host) + 10); /* better a few bytes too much than too little */
+  // I use maskstricthost() here, because stats.mod shouldn't strip
+  // a host anywhere at all. (strict-hosts 0 sucks...)
+  maskstricthost(host, mhost);
+//  mhost = nrealloc(mhost, strlen(mhost) + strlen(nick) + 1);sprintf(mhost, "%s%s", m->nick, mhost + 1);
+  if (u) {
+    if (u->addhosts) {
+      saddhost(u, mhost, now);
+      m->user = u;
+      putlog(LOG_MISC, "*", "Stats.Mod: Added stats-hostmask %s to %s.", mhost, u->user);
+    }
+  } else {
+    uu = get_user_by_host(host);
+    if (!uu && (autoadd == 0)) {
+      nfree(mhost);
+      nfree(host);
+      return;
+    }
+    if (uu)
+      u = addsuser(uu->handle, 1, 1);
+    else
+      u = addsuser(m->nick, 1, 1);
+    saddhost(u, mhost, now);
+    if (uu)
+      putlog(LOG_MISC, "*", "Stats.Mod: %s matched %s(in the \"common\" userfile), added %s to userbase.", host, uu->handle, u->user);
+    else
+      putlog(LOG_MISC, "*", "Stats.Mod: Added %s(%s) to userbase.", u->user, mhost);
+    m->user = u;
+  }
+  if (m->user)
+    m->stats = findlocstats(chan->chan, m->user->user);
+  else
+    m->stats = NULL;
+  nfree(mhost);
+  nfree(host);
+}
+
+static int listsuser(locstats *ls, char *chan)
+{
+  struct userrec *ou;
+
+  if (!use_userfile) {
+    if (!ls->u)
+      ls->u = findsuser_by_name(ls->user);
+    if (ls->u && !ls->u->list)
+      return 0;
+  } else {
+    ou = get_user_by_handle(userlist, ls->user);
+    if (matchattr(ou, nostatsflags, chan))
+      return 0;
+  }
+  return 1;
+}
+
+static int countsusers()
+{
+  static struct stats_userlist *u;
+  int users = 0;
+
+  Context;
+  for (u = suserlist; u; u = u->next)
+    users++;
+  return users;
+}
+
+static int counthosts()
+{
+  static struct stats_userlist *u;
+  static struct stats_hostlist *h;
+  int hosts = 0;
+
+  Context;
+  for (u = suserlist; u; u = u->next)
+    for (h = u->hosts; h; h = h->next)
+      hosts++;
+  return hosts;
+}
+
+static void weed_userlink_from_chanset(struct stats_userlist *u)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m;
+
+  Context;
+  for (chan = schans; chan; chan = chan->next) {
+    for (m = chan->members; m; m = m->next) {
+      if (m->user == u) {
+        m->user = NULL;
+        m->stats = NULL;
+      }
+    }
+  }
+}
+
+static void weed_statlink_from_chanset(locstats *ls)
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m;
+
+  Context;
+  for (chan = schans; chan; chan = chan->next) {
+    for (m = chan->members; m; m = m->next) {
+      if (m->stats == ls) {
+        m->stats = NULL;
+      }
+    }
+  }
+}
+
+/* weed_userlink_from_locstats():
+ * removes all references to a userstruct from the stat-structs
+ * (mostly used if the user got deleted)
+ */
+static void weed_userlink_from_locstats(struct stats_userlist *u)
+{
+  globstats *gs;
+  locstats *ls;
+
+  Context;
+  for (gs = sdata; gs; gs = gs->next)
+    for (ls = gs->local; ls; ls = ls->next)
+      if (ls->u == u)
+        ls->u = NULL;
+  Context;
+}
+
+static int countmembers(struct stats_memberlist *m)
+{
+  int nr = 0;
+
+  Context;
+  while (m) {
+    nr++;
+    m = m->next;
+  }
+  return nr;
+}
+
+static void check_desynch()
+{
+  struct stats_chanset *chan;
+  struct chanset_t *ch;
+
+  Context;
+  for (ch = chanset; ch; ch = ch->next) {
+    if (ch->status & CHAN_INACTIVE)
+      continue;
+#if EGG_IS_MIN_VER(10500)
+    chan = findschan(ch->dname);
+#else
+    chan = findschan(ch->name);
+#endif
+    if (chan)
+      if (ch->channel.members == countmembers(chan->members))
+        continue;
+#if EGG_IS_MIN_VER(10500)
+    putlog(LOG_DEBUG, "*", "Stats.Mod: Channel list for %s desynched!!! Resetting...", ch->dname);
+#else
+    putlog(LOG_DEBUG, "*", "Stats.Mod: Channel list for %s desynched!!! Resetting...", ch->name);
+#endif
+    if (chan)
+      free_one_chan(chan->chan);
+#if EGG_IS_MIN_VER(10500)
+    initchan(ch->dname);
+#else
+    initchan(ch->name);
+#endif
+  }
+}
+
+/* used when shosts/susers changed */
+static void update_schannel_members()
+{
+  struct stats_chanset *chan;
+  struct stats_memberlist *m;
+  struct userrec *u;
+  char *host;
+
+  Context;
+  for (chan = schans; chan; chan = chan->next) {
+    for (m = chan->members; m; m = m->next) {
+      m->user = NULL;
+      host = nmalloc(strlen(m->nick) + 1 + strlen(m->uhost) + 1);
+      sprintf(host, "%s!%s", m->nick, m->uhost);
+      u = get_user_by_host(host);
+      if (u) {
+        m->user = findsuser_by_name(u->handle);
+        if (!m->user) {
+          m->user = addsuser(u->handle, 1, 0);
+          debug1("Stats.Mod: Created suserrec for %s.", u->handle);
+        }
+      } else
+        m->user = findsuser(host);
+      nfree(host);
+      m->stats = NULL;
+    }
+  }
+  debug0("update_schannel_members()");
+}
+
+static void setemail(struct stats_userlist *u, char *email)
+{
+  if (!u) {
+    putlog(LOG_MISC, "*", "ERROR! Tried to set email for NULL!");
+    return;
+  }
+  if (u->email)
+    nfree(u->email);
+  u->email = nmalloc(strlen(email) + 1);
+  strcpy(u->email, email);
+}
+
+static void sethomepage(struct stats_userlist *u, char *homepage)
+{
+  if (!u) {
+    putlog(LOG_MISC, "*", "ERROR! Tried to set homepage for NULL!");
+    return;
+  }
+  if (u->homepage)
+    nfree(u->homepage);
+  u->homepage = nmalloc(strlen(homepage) + 1);
+  strcpy(u->homepage, homepage);
+}

+ 234 - 0
webfiles.c

@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2000,2001  Florian Sander
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+void stats_setsock(int sock, int options) /* fabian's code */
+{
+  (int) allocsock(sock, options);
+  (int) fcntl(sock, F_SETFL, O_NONBLOCK);
+}
+
+static void write_new_webstats()
+{
+  char *url, *file, *dir, *dir2, *buf, *buf2, *type, *today;
+  char *bak, *bak2;
+  globstats *gs;
+  locstats *ls;
+  FILE *f;
+  int fd;
+
+  Context;
+  putlog(LOG_MISC, "*", "Writing static webfiles to %s...", webdir);
+  file = nmalloc(strlen(webdir) + strlen("/index.html") + 1);
+  sprintf(file, "%s/index.html", webdir);
+  fd = creat(file, 0644);
+  if (fd == -1) {
+    putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+    nfree(file);
+    return;
+  }
+  nfree(file);
+  stats_setsock(fd, SOCK_NONSOCK);
+  send_livestats(-fd, "/");
+  killsock(fd);
+  for (gs = sdata; gs; gs = gs->next) {
+    putlog(LOG_MISC, "*", "... writing stats for %s ...", gs->chan);
+    if (strchr(gs->chan, '[') || strchr(gs->chan, ']') || strchr(gs->chan, '^')) {
+      putlog(LOG_MISC, "*", "... illegal character ('[', ']', or '^') in channame, skipping...");
+      continue;
+    }
+    dir = nmalloc(strlen(webdir) + strlen(gs->chan) + 1 + 1);
+    sprintf(dir, "%s/%s", webdir, (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    mkdir(dir, 0755);
+    file = nmalloc(strlen(dir) + 11 + 1);
+    sprintf(file, "%s/index.html", dir);
+    fd = creat(file, 0644);
+    if (fd == -1) {
+      putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+      nfree(dir);
+      nfree(file);
+      return;
+    }
+    nfree(file);
+    url = nmalloc(strlen(gs->chan) + 2 + 1);
+    sprintf(url, "/%s/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    stats_setsock(fd, SOCK_NONSOCK);
+    send_livestats(-fd, url);
+    nfree(url);
+    killsock(fd);
+    dir2 = nmalloc(strlen(dir) + 5 + 1);
+    sprintf(dir2, "%s/misc", dir);
+    mkdir(dir2, 0755);
+    file = nmalloc(strlen(dir2) + 11 + 1);
+    sprintf(file, "%s/index.html", dir2);
+    fd = creat(file, 0644);
+    if (fd == -1) {
+      putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+      nfree(dir);
+      nfree(file);
+      nfree(dir2);
+      return;
+    }
+    nfree(file);
+    url = nmalloc(9 + strlen(gs->chan) + 1);
+    sprintf(url, "/%s/misc/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    stats_setsock(fd, SOCK_NONSOCK);
+    send_livestats(-fd, url);
+    nfree(url);
+    killsock(fd);
+    nfree(dir2);
+    dir2 = nmalloc(6 + strlen(dir) + 1);
+    sprintf(dir2, "%s/top", dir);
+    mkdir(dir2, 0755);
+    file = nmalloc(strlen(dir2) + 11 + 1);
+    sprintf(file, "%s/index.html", dir2);
+    fd = creat(file, 0644);
+    if (fd == -1) {
+      putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+      nfree(dir);
+      nfree(file);
+      nfree(dir2);
+      return;
+    }
+    nfree(file);
+    url = nmalloc(8 + strlen(gs->chan) + 1);
+    sprintf(url, "/%s/top/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    stats_setsock(fd, SOCK_NONSOCK);
+    send_livestats(-fd, url);
+    nfree(url);
+    killsock(fd);
+    nfree(dir2);
+    buf = nmalloc(26 + 1);
+    bak = buf;
+    strcpy(buf, "total daily weekly monthly");
+    while (buf[0]) {
+      today = newsplit(&buf);
+      dir2 = nmalloc(9 + strlen(dir) + strlen(today) + 1);
+      sprintf(dir2, "%s/top/%s", dir, today);
+      mkdir(dir2, 0755);
+      nfree(dir2);
+      buf2 = nmalloc(9 + strlen(webstats) + 1);
+      sprintf(buf2, "%s graphs", webstats);
+      bak2 = buf2;
+      while (buf2[0]) {
+        type = newsplit(&buf2);
+        dir2 = nmalloc(12 + strlen(dir) + strlen(today) + strlen(type) + 1);
+        sprintf(dir2, "%s/top/%s/%s", dir, today, type);
+        mkdir(dir2, 0755);
+        file = nmalloc(13 + strlen(dir2) + 1);
+        sprintf(file, "%s/index.html", dir2);
+        fd = creat(file, 0644);
+        if (fd == -1) {
+          putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+          nfree(dir);
+          nfree(file);
+          nfree(dir2);
+          nfree(bak);
+          nfree(bak2);
+          return;
+        }
+        nfree(file);
+        url = nmalloc(14 + strlen(gs->chan) + strlen(today) + strlen(type) + 1);
+        sprintf(url, "/%s/top/%s/%s/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan, today, type);
+        stats_setsock(fd, SOCK_NONSOCK);
+        send_livestats(-fd, url);
+        killsock(fd);
+        nfree(url);
+        nfree(dir2);
+      }
+      nfree(bak2);
+    }
+    nfree(bak);
+    dir2 = nmalloc(8 + strlen(dir) + 1);
+    sprintf(dir2, "%s/users", dir);
+    mkdir(dir2, 0755);
+    file = nmalloc(13 + strlen(dir2) + 1);
+    sprintf(file, "%s/index.html", dir2);
+    fd = creat(file, 0644);
+    if (fd == -1) {
+      putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+      nfree(dir);
+      nfree(file);
+      nfree(dir2);
+      return;
+    }
+    nfree(file);
+    url = nmalloc(10 + strlen(gs->chan) + 1 );
+    sprintf(url, "/%s/users/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    stats_setsock(fd, SOCK_NONSOCK);
+    send_livestats(-fd, url);
+    killsock(fd);
+    nfree(url);
+    nfree(dir2);
+
+    dir2 = nmalloc(9 + strlen(dir) + 1);
+    sprintf(dir2, "%s/onchan", dir);
+    mkdir(dir2, 0755);
+    file = nmalloc(13 + strlen(dir2) + 1);
+    sprintf(file, "%s/index.html", dir2);
+    fd = creat(file, 0644);
+    if (fd == -1) {
+      putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+      nfree(dir);
+      nfree(file);
+      nfree(dir2);
+      return;
+    }
+    nfree(file);
+    url = nmalloc(11 + strlen(gs->chan) + 1 );
+    sprintf(url, "/%s/onchan/", (gs->chan[0] == '#') ? gs->chan + 1 : gs->chan);
+    stats_setsock(fd, SOCK_NONSOCK);
+    send_livestats(-fd, url);
+    killsock(fd);
+    nfree(url);
+    nfree(dir2);
+
+    setslglobs(gs->chan, gs->peak[S_TOTAL], countstatmembers(gs), gs->started);
+    for (ls = gs->local; ls; ls = ls->next) { 	/* WARNING! DANGEROUS LOOP! If the sorting changes, you can get trapped in an endless loop! */
+      slgloblocstats = ls;
+      dir2 = nmalloc(11 + strlen(dir) + strlen(ls->user) + 1);
+      sprintf(dir2, "%s/users/%s", dir, ls->user);
+      mkdir(dir2, 0755);
+      file = nmalloc(13 + strlen(dir2) + 1);
+      sprintf(file, "%s/index.html", dir2);
+      f = fopen(file, "w");
+      chmod(file, 0644);
+      if (!f) {
+        putlog(LOG_MISC, "*", "ERROR writing webfiles! (%s)", file);
+        nfree(dir);
+        nfree(file);
+        nfree(dir2);
+        return;
+      }
+      nfree(file);
+      fprintf(f, "<html>\n<head>\n<title>%s</title>\n%s\n</head>\n%s\n", SLSTATICTITLE, SLCSS, bodytag);
+      fprintf(f, "%s\n", SLSTATICBODY);
+      fprintf(f, "<br><br><hr>\n");
+      fprintf(f, "<table width=100%% border=0>\n");
+      fprintf(f, "<tr><td align=center><table width=100%% border=0><tr>\n");
+      fprintf(f, "<td width=25%% align=center><font size=-1><a href=\"../\">%s</a></font></td>\n", SLOTHERUSERS);
+      fprintf(f, "<td width=25%% align=center><font size=-1><a href=\"../../top/total/words/\">%s</a></font></td>\n", SLTOP);
+      fprintf(f, "<td width=25%% align=center><font size=-1><a href=\"../../misc/\">%s</a></font></td>\n", SLMISCSTATS);
+      fprintf(f, "<td width=25%% align=center><font size=-1><a href=\"../../../\">%s</a></font></td>\n", SLOTHERCHANS);
+      fprintf(f, "</tr></table></td></tr>\n");
+      nfree(dir2);
+      fclose(f);
+    }
+    nfree(dir);
+  }
+  putlog(LOG_MISC, "*", "... done.");
+}