# -*- coding: utf-8 -*-
###
# Copyright (c) 2010 by Elián Hanisch <lambdae2@gmail.com>
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
###
###
# Shows highest and lowest user count for joined channels,
# and an average (for a period of a month)
#
#
# Commands:
# * /chanstat
# Prints current channel stats, see /help chanstat
#
#
# Settings:
# * plugins.var.python.chanstat.path:
# path where to store stat files, deault '%h/chanstat'
#
# * plugins.var.python.chanstat.averge_period:
# Period of time for calculate the average stats. This means the avegare will be calculated with
# the users present in the last x days. Default is 30 days.
#
# * plugins.var.python.chanstat.show_peaks:
# If 'on' it will display a message when there's a user peak in any channel.
# Valid values: on, off
#
# * plugins.var.python.chanstat.show_lows:
# If 'on' it will display a message when there's a user low in any channel.
# Valid values: on, off
#
#
# History:
# 2010-06-08
# version 0.1: initial release.
#
###
SCRIPT_NAME = "chanstat"
SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
SCRIPT_VERSION = "0.1"
SCRIPT_LICENSE = "GPL3"
SCRIPT_DESC = "Channel statistics"
try:
import weechat
WEECHAT_RC_OK = weechat.WEECHAT_RC_OK
import_ok = True
except ImportError:
print "This script must be run under WeeChat."
print "Get WeeChat now at: http://weechat.flashtux.org/"
import_ok = False
import time
now = lambda : int(time.time())
time_hour = 3600
time_day = 86400
time_year = 31536000
### messages
def debug(s, args=(), prefix='', name_suffix='debug', level=1):
"""Debug msg"""
l = weechat.config_get_plugin('debug')
if not (l and int(l) >= level): return
buffer_name = '%s_%s' %(SCRIPT_NAME, name_suffix)
buffer = weechat.buffer_search('python', buffer_name)
if not buffer:
buffer = weechat.buffer_new(buffer_name, '', '', '', '')
weechat.buffer_set(buffer, 'nicklist', '0')
weechat.buffer_set(buffer, 'localvar_set_no_log', '1')
weechat.prnt(buffer, '%s\t%s' %(prefix, s %args))
def error(s, prefix=SCRIPT_NAME, buffer=''):
"""Error msg"""
prefix = prefix or script_nick
weechat.prnt(buffer, '%s%s %s' %(weechat.prefix('error'), prefix, s))
def say(s, prefix=None, buffer=''):
"""Normal msg"""
prefix = prefix or script_nick
weechat.prnt(buffer, '%s\t%s' %(prefix, s))
### config and value validation
boolDict = {'on':True, 'off':False}
def get_config_boolean(config):
value = weechat.config_get_plugin(config)
try:
return boolDict[value]
except KeyError:
default = settings[config]
error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
error("'%s' is invalid, allowed: 'on', 'off'" %value)
return boolDict[default]
def get_config_int(config):
value = weechat.config_get_plugin(config)
try:
return int(value)
except ValueError:
default = settings[config]
error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
error("'%s' is not a number." %value)
return int(default)
def get_dir(filename):
import os
basedir = weechat.config_get_plugin('path').replace('%h', weechat.info_get('weechat_dir', ''))
if not os.path.isdir(basedir):
os.makedirs(basedir)
return os.path.join(basedir, filename.lower())
class CaseInsensibleString(str):
def __init__(self, s=''):
self.lowered = s.lower()
def __eq__(self, s):
try:
return self.lowered == s.lower()
except:
return False
def __ne__(self, s):
return not self == s
def __hash__(self):
return hash(self.lowered)
def caseInsensibleKey(k):
if isinstance(k, str):
return CaseInsensibleString(k)
elif isinstance(k, tuple):
return tuple([ caseInsensibleKey(v) for v in k ])
return k
class CaseInsensibleDict(dict):
key = staticmethod(caseInsensibleKey)
def __setitem__(self, k, v):
dict.__setitem__(self, self.key(k), v)
def __getitem__(self, k):
return dict.__getitem__(self, self.key(k))
def __delitem__(self, k):
dict.__delitem__(self, self.key(k))
def __contains__(self, k):
return dict.__contains__(self, self.key(k))
class Channel(object):
def __init__(self, max=None, min=None, max_date=None, min_date=None, avrg_date=None,
avrg_period=None, average=None, count=0):
if not max:
max = count
if not min:
min = 0
if not average:
average = float(count)
if not max_date:
max_date = now()
if not min_date:
min_date = now()
if not avrg_date:
avrg_date = now()
if not avrg_period:
avrg_period = 0
self.max = max
self.min = min
self.max_date = max_date
self.min_date = min_date
self.avrg_date = avrg_date
self.avrg_period = avrg_period
self.average = average
def __iter__(self):
return iter((self.max, self.min, self.max_date, self.min_date, self.avrg_date,
self.avrg_period, self.average))
def __str__(self):
return 'Channel(max=%s, average=%s, min_delta=%s)' %(self.max, self.average, self.min)
class StatLog(object):
def __init__(self):
self.writers = CaseInsensibleDict()
@staticmethod
def make_log(key):
return get_dir('%s_%s.cvs' %key)
def log(self, key, *args):
if key in self.writers:
writer = self.writers[key]
else:
import csv
filename = self.make_log(key)
writer = csv.writer(open(filename, 'ab'))
self.writers[key] = writer
writer.writerow(args)
def get_reader(self, key):
if key in self.writers:
del self.writers[key]
import csv
return csv.reader(open(self.make_log(key)))
def close(self):
self.writers = {}
class ChanStatDB(CaseInsensibleDict):
def __init__(self):
self.logger = StatLog()
def __setitem__(self, key, value):
if not value:
return
debug(' ** stats update for %s', args=key[1])
_now = now()
avrg = 0
if key in self:
chan = self[key]
if value > chan.max:
debug('PEAK, %s: %s', args=(key[1], value))
chan.max = value
new_channel_peak(key, value, chan.max_date)
chan.max_date = _now
elif (chan.max - value) > chan.min:
# we save the difference between max and min rather the min absolute value, because
# the minimum min value for a channel would be 1, and that's isn't interesting.
min_delta = chan.max - value
debug('LOW, %s: %s', args=(key[1], value))
chan.min = min_delta
new_channel_low(key, value, chan.min_date)
chan.min_date = _now
# calculate average aproximation
diff = _now - chan.avrg_date
#period = 30 * time_day
period = get_config_int('average_period') * time_day
if not period:
period = time_day
avrg_period = chan.avrg_period
avrg_period += diff
if avrg_period > period:
avrg_period = period
if diff > avrg_period // 1000 and diff > 600:
# calc average after 1000th part of the period (10 min minimum)
max = period // 100
if diff > max:
# too much time have passed since last check, average will be skewed by current
# user count, so we reduce the average period.
excess = diff - max
debug('avrg period correction %s e:%.4f%%', args=(key[1],
excess*100.0/avrg_period))
diff = max
avrg_period -= excess
if avrg_period < diff:
avrg_period = diff
avrg = chan.average
avrg = (avrg * (avrg_period - diff) + value * diff) / avrg_period
chan.avrg_date = _now
chan.avrg_period = avrg_period
# make sure avrg is between max and 1
if avrg > chan.max:
avrg = chan.max
elif avrg < 1:
avrg = 1
debug('avrg %s %.2f → %.2f (%.4f%% %.4f)', args=(key[1], chan.average, avrg,
diff*100.0/avrg_period, avrg - chan.average))
chan.average = avrg
else:
CaseInsensibleDict.__setitem__(self, key, Channel(count=value))
#if avrg:
# self.logger.log(key, _now, value, avrg)
#else:
# self.logger.log(key, _now, value)
def initchan(self, key, *args):
CaseInsensibleDict.__setitem__(self, key, Channel(*args))
def iterchan(self):
def generator():
for key in self.keys():
chan = self[key]
row = list(key)
row.extend(chan)
yield row
return generator()
def keys(self):
"""Returns keys sorted"""
L = dict.keys(self)
L.sort()
return L
def close(self):
self.logger.close()
channel_stats = ChanStatDB()
def write_database():
import csv
filename = get_dir('peak_data.csv')
try:
writer = csv.writer(open(filename, 'wb'))
writer.writerows(channel_stats.iterchan())
except IOError:
error('Failed to write chanstat database in %s' %file)
def load_database():
import csv
filename = get_dir('peak_data.csv')
try:
reader = csv.reader(open(filename, 'rb'))
except IOError:
return
channel_stats.clear()
for row in reader:
key = tuple(row[0:2])
values = row[2:-1]
values = map(int, values)
average = row[-1]
average = float(average)
values.append(average)
channel_stats.initchan(key, *values)
def update_user_count(server=None, channel=None):
if isinstance(channel, str):
channel = set((channel, ))
elif channel:
channel = set(channel)
def update_channel(server, channel=None):
channel_infolist = weechat.infolist_get('irc_channel', '', server)
while weechat.infolist_next(channel_infolist):
_channel = weechat.infolist_string(channel_infolist, 'name')
if channel:
_channel = caseInsensibleKey(_channel)
if _channel not in channel:
continue
channel_stats[server, _channel] = weechat.infolist_integer(channel_infolist, 'nicks_count')
weechat.infolist_free(channel_infolist)
if not server:
server_infolist = weechat.infolist_get('irc_server', '', '')
while weechat.infolist_next(server_infolist):
server = weechat.infolist_string(server_infolist, 'name')
update_channel(server)
weechat.infolist_free(server_infolist)
else:
update_channel(server, channel)
def time_elapsed(elapsed, ret=None, level=2):
if ret is None:
ret = []
if not elapsed:
return ''
if elapsed > time_year:
years, elapsed = elapsed // time_year, elapsed % time_year
ret.append('%s%s' %(years, 'y'))
elif elapsed > time_day:
days, elapsed = elapsed // time_day, elapsed % time_day
ret.append('%s%s' %(days, 'd'))
elif elapsed > time_hour:
hours, elapsed = elapsed // time_hour, elapsed % time_hour
ret.append('%s%s' %(hours, 'h'))
elif elapsed > 60:
mins, elapsed = elapsed // 60, elapsed % 60
ret.append('%s%s' %(mins, 'm'))
else:
secs, elapsed = elapsed, 0
ret.append('%s%s' %(secs, 's'))
if len(ret) >= level or not elapsed:
return ' '.join(ret)
ret = time_elapsed(elapsed, ret, level)
return ret
channel_peak_hooks = CaseInsensibleDict()
msg_queue_timeout = 15
def new_channel_peak(key, count, time=0):
if not get_config_boolean('show_peaks'):
return
if key in channel_peak_hooks:
weechat.unhook(channel_peak_hooks[key][0])
time = channel_peak_hooks[key][1]
else:
time -= 60 * msg_queue_timeout # add delay in showing the msg
if time:
elapsed = time_elapsed(now() - time)
if elapsed:
elapsed = '(last peak was %s ago)' %elapsed
else:
elapsed = ''
# hook it for show msg 10 min later
channel_peak_hooks[key] = (weechat.hook_timer(60000 * msg_queue_timeout, 0, 1, 'new_channel_peak_cb',
'%s,%s,New user peak: %s users %s' %(key[0], key[1], count, elapsed)), time)
def new_channel_peak_cb(data, count):
debug(data)
server, channel, s = data.split(',', 2)
buffer = weechat.info_get('irc_buffer', '%s,%s' %(server, channel))
if buffer:
say('%s%s' %(color_peak, s), buffer=buffer)
else:
debug('XX falied to get buffer: %s.%s', args=(server, channel))
del channel_peak_hooks[server, channel]
return WEECHAT_RC_OK
channel_low_hooks = CaseInsensibleDict()
def new_channel_low(key, count, time=0):
if not get_config_boolean('show_lows'):
return
if key in channel_low_hooks:
weechat.unhook(channel_low_hooks[key][0])
time = channel_low_hooks[key][1]
else:
time -= 60 * msg_queue_timeout # add delay in showing the msg
if time:
elapsed = time_elapsed(now() - time)
if elapsed:
elapsed = '(last low was %s ago)' %elapsed
else:
elapsed = ''
# hook it for show msg 10 min later
channel_low_hooks[key] = (weechat.hook_timer(60000 * msg_queue_timeout, 0, 1, 'new_channel_low_cb',
'%s,%s,New user low: %s %s' %(key[0], key[1], count, elapsed)), time)
def new_channel_low_cb(data, count):
debug(data)
server, channel, s = data.split(',', 2)
buffer = weechat.buffer_search('irc', '%s.%s' %(server, channel))
if buffer:
say('%s%s' %(color_low, s), buffer=buffer)
else:
debug('XX falied to get buffer: %s.%s', args=(server, channel))
del channel_low_hooks[server, channel]
return WEECHAT_RC_OK
# chanstat command
def chanstat_cmd(data, buffer, args):
if args == '--save':
write_database()
channel_stats.close()
say('Channel statistics saved.')
return WEECHAT_RC_OK
elif args == '--load':
load_database()
say('Channel statistics loaded.')
return WEECHAT_RC_OK
# elif args == '--print':
# prnt = weechat.command
channel = weechat.buffer_get_string(buffer, 'localvar_channel')
server = weechat.buffer_get_string(buffer, 'localvar_server')
key = (server, channel)
update_user_count(server, channel)
# clear any update in queue
if key in update_channel_hook:
weechat.unhook(update_channel_hook[key][0])
del update_channel_hook[key]
try:
chan = channel_stats[server, channel]
_now = now()
peak_time = time_elapsed(_now - chan.max_date)
low_time = time_elapsed(_now - chan.min_date)
if peak_time:
peak_time = ' (%s ago)' %peak_time
if low_time:
low_time = ' (%s ago)' %low_time
if chan.avrg_period > time_hour:
average = ' average: %s%.2f%s users (%s period)' %(color_avg, chan.average,
color_reset, time_elapsed(chan.avrg_period, level=1))
else:
average = ' (no average yet)'
say('%s%s%s user peak: %s%s%s%s lowest: %s%s%s%s%s' %(
color_bold, channel, color_reset,
color_peak, chan.max, color_reset,
peak_time,
color_low, chan.max - chan.min, color_reset,
low_time, average), buffer=buffer)
# clear any new peak or low msg in queue
if key in channel_peak_hooks:
weechat.unhook(channel_peak_hooks[key][0])
del channel_peak_hooks[key]
if key in channel_low_hooks:
weechat.unhook(channel_low_hooks[key][0])
del channel_low_hooks[key]
except KeyError:
say('No statistics available.', buffer=buffer)
return WEECHAT_RC_OK
def cmd_debug(data, buffer, args):
dbg = lambda s, a: debug(s, args=a, name_suffix='dump')
dbg('\nStats DB: %s', len(channel_stats))
for key in channel_stats.keys():
dbg('%s %s - %s', (key[0], key[1], channel_stats[key]))
dbg('\nUsers: %s', len(domain_list))
return WEECHAT_RC_OK
class Queue(dict):
"""User queue, for ignore many joins from same host (clones)"""
def __contains__(self, key):
self.clear()
return dict.__contains__(self, key)
def clear(self):
_now = now()
for key, time in self.items():
if (_now - time) > 60:
#debug('clearing domain %s from list (count: %s)' %(key, len(self)))
del self[key]
domain_list = Queue()
# signal callbacks
def join_cb(data, signal, signal_data):
#debug('%s %s\n%s' %(data, signal, signal_data), 'SIGNAL')
global netsplit
if netsplit:
debug('ignoring, netsplit')
if (now() - netsplit) > 30*60: # wait 30 min
netsplit = 0
return WEECHAT_RC_OK
server = signal[:signal.find(',')]
signal_data = signal_data.split()
channel = signal_data[2].strip(':')
host = signal_data[0].strip(':')
domain = '%s,%s' %(channel, host[host.find('@')+1:])
if domain in domain_list:
#debug('ignoring %s', args=domain)
return WEECHAT_RC_OK
else:
domain_list[domain] = now()
debug(' -- ping %s (%s)', args=(channel,signal[-4:]))
add_update_user_hook(server, channel)
return WEECHAT_RC_OK
netsplit = 0
def quit_cb(data, signal, signal_data):
#debug('%s %s\n%s' %(data, signal, signal_data), 'SIGNAL')
global netsplit
if netsplit:
return WEECHAT_RC_OK
quit_msg = signal_data[signal_data.rfind(':')+1:]
if quit_msg_is_split(quit_msg):
netsplit = now()
for hook, when in update_channel_hook.itervalues():
weechat.unhook(hook)
update_channel_hook.clear()
debug('NETSPLIT')
return WEECHAT_RC_OK
def quit_msg_is_split(s):
#if 'peer' in s: return True
if s.count(' ') == 1:
sp = s.find(' ')
d1 = s.find('.')
d2 = s.rfind('.')
if 0 < d1 and 4 < d2 and d1 < sp < d2 and d2 + 1 < len(s):
return True
return False
update_channel_hook = CaseInsensibleDict()
update_queue_timeout = 120
def add_update_user_hook(server, channel):
key = (server, channel)
if key in update_channel_hook:
hook, when = update_channel_hook[key]
if (now() - when) > update_queue_timeout//2:
debug(' vv rescheduling %s', args=key[1])
weechat.unhook(hook)
else:
return
else:
debug(' >> scheduling %s', args=key[1])
# we schedule the channel check for later so we can filter quick joins/parts and netsplits
update_channel_hook[key] = (weechat.hook_timer(update_queue_timeout * 1000, 0, 1, 'update_user_count_cb',
','.join(key)), now())
def update_user_count_cb(data, count):
server, channel = data.split(',', 1)
channels = [ chan for serv, chan in update_channel_hook if server == serv ]
if channels:
update_user_count(server, channels)
for chan in channels:
hook, when = update_channel_hook[server, chan]
weechat.unhook(hook)
del update_channel_hook[server, chan]
return WEECHAT_RC_OK
def script_load():
load_database()
update_user_count()
def script_unload():
write_database()
channel_stats.close()
return WEECHAT_RC_OK
# default settings
settings = {
'path' :'%h/chanstat',
'average_period':'30',
'show_peaks' :'on',
'show_lows' :'on',
}
if __name__ == '__main__' and import_ok and \
weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
SCRIPT_DESC, 'script_unload', ''):
# colors
color_delimiter = weechat.color('chat_delimiters')
color_chat_nick = weechat.color('chat_nick')
color_reset = weechat.color('reset')
color_peak = weechat.color('green')
color_low = weechat.color('red')
color_avg = weechat.color('brown')
color_bold = weechat.color('white')
# pretty [chanop]
script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_chat_nick, SCRIPT_NAME, color_delimiter,
color_reset)
for opt, val in settings.iteritems():
if not weechat.config_is_set_plugin(opt):
weechat.config_set_plugin(opt, val)
script_load()
weechat.hook_signal('*,irc_in2_join', 'join_cb', '')
weechat.hook_signal('*,irc_in2_part', 'join_cb', '')
weechat.hook_signal('*,irc_in2_quit', 'quit_cb', '')
weechat.hook_command('chanstat', "Display channel's statistics.", '[--save | --load]',
"Displays channel peak, lowest and average users for current channel.\n"
" --save: forces saving the stats database.\n"
" --load: forces loading the stats database (Overwriting actual values).\n",
#" --print: sends /chanstat output to the current channel.",
'--save|--load', 'chanstat_cmd', '')
weechat.hook_command('chanstat_debug', '', '', '', '', 'cmd_debug', '')
# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: