diff --git a/dr_botzo/dr_botzo/settings.py b/dr_botzo/dr_botzo/settings.py index a89939e..4108d45 100644 --- a/dr_botzo/dr_botzo/settings.py +++ b/dr_botzo/dr_botzo/settings.py @@ -37,6 +37,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'django_extensions', 'ircbot', + 'karma', 'markov', 'races', 'seen', diff --git a/dr_botzo/karma/__init__.py b/dr_botzo/karma/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dr_botzo/karma/admin.py b/dr_botzo/karma/admin.py new file mode 100644 index 0000000..4f8eede --- /dev/null +++ b/dr_botzo/karma/admin.py @@ -0,0 +1,9 @@ +"""Manage karma models in the admin interface.""" + +from django.contrib import admin + +from karma.models import KarmaKey, KarmaLogEntry + + +admin.site.register(KarmaKey) +admin.site.register(KarmaLogEntry) diff --git a/dr_botzo/karma/ircplugin.py b/dr_botzo/karma/ircplugin.py new file mode 100644 index 0000000..87ac983 --- /dev/null +++ b/dr_botzo/karma/ircplugin.py @@ -0,0 +1,139 @@ +"""Karma hooks for the IRC bot.""" + +import logging +import re + +import irc.client + +from ircbot.lib import Plugin +from karma.models import KarmaKey, KarmaLogEntry + +log = logging.getLogger('karma.ircplugin') + + +class Karma(Plugin): + + """Track karma and report on it.""" + + def start(self): + """Set up the handlers.""" + + self.connection.add_global_handler('pubmsg', self.handle_chatter, -20) + self.connection.add_global_handler('privmsg', self.handle_chatter, -20) + + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!karma\s+rank\s+(.*)$', + self.handle_rank, -20) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + (r'^!karma\s+report\s+(highest|lowest|positive|negative' + r'|top|opinionated)'), + self.handle_report, -20) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], + r'^!karma\s+stats\s+([\S]+)', + self.handle_stats, -20) + + super(Karma, self).start() + + def stop(self): + """Tear down handlers.""" + + self.connection.remove_global_handler('pubmsg', self.handle_chatter) + self.connection.remove_global_handler('privmsg', self.handle_chatter) + + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_rank) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_report) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stats) + + super(Karma, self).stop() + + @staticmethod + def handle_chatter(connection, event): + """Watch karma from IRC chatter.""" + + what = event.arguments[0].lower() + karma_pattern = r'(?:\((.+?)\)|(\S+))(\+\+|--|\+-|-\+)(\s+|$)' + + # check the line for karma + log.debug(u"searching '%s' for karma", what) + matches = re.findall(karma_pattern, what, re.IGNORECASE) + for match in matches: + key = match[0] if match[0] else match[1] + value = match[2] + + karma_key, c = KarmaKey.objects.get_or_create(key=key) + + if value == '++': + KarmaLogEntry.objects.create(key=karma_key, delta=1, nickmask=event.source) + elif value == '--': + KarmaLogEntry.objects.create(key=karma_key, delta=-1, nickmask=event.source) + elif value == '+-': + KarmaLogEntry.objects.create(key=karma_key, delta=1, nickmask=event.source) + KarmaLogEntry.objects.create(key=karma_key, delta=-1, nickmask=event.source) + elif value == '-+': + KarmaLogEntry.objects.create(key=karma_key, delta=-1, nickmask=event.source) + KarmaLogEntry.objects.create(key=karma_key, delta=1, nickmask=event.source) + + def handle_rank(self, connection, event, match): + """Report on the rank of a karma item.""" + + key = match.group(1).lower().rstrip() + try: + karma_key = KarmaKey.objects.get(key=key) + return self.bot.reply(event, "{0:s} has {1:d} points of karma (rank {2:d})".format(karma_key.key, + karma_key.score(), + karma_key.rank())) + except KarmaKey.DoesNotExist: + return self.bot.reply(event, "i have not seen any karma for {0:s}".format(match.group(1))) + + def handle_report(self, connection, event, match): + """Provide some karma reports.""" + + report = match.group(1).lower() + if report == 'highest': + sorted_keys = KarmaKey.objects.ranked_scored_order() + msg = "top 5 recipients: {0:s}".format(", ".join([str(x) for x in sorted_keys[:5]])) + return self.bot.reply(event, msg) + elif report == 'lowest': + sorted_keys = KarmaKey.objects.ranked_scored_reverse_order() + msg = "bottom 5 recipients: {0:s}".format(", ".join([str(x) for x in sorted_keys[:5]])) + return self.bot.reply(event, msg) + elif report == 'positive': + karmaers = KarmaLogEntry.objects.optimists() + karmaer_list = ", ".join(["{0:s} ({1:d})".format(irc.client.NickMask(x['nickmask']).nick, + x['karma_outlook']) for x in karmaers]) + msg = "top 5 optimists: {0:s}".format(karmaer_list) + return self.bot.reply(event, msg) + elif report == 'negative': + karmaers = KarmaLogEntry.objects.pessimists() + karmaer_list = ", ".join(["{0:s} ({1:d})".format(irc.client.NickMask(x['nickmask']).nick, + x['karma_outlook']) for x in karmaers]) + msg = "top 5 pessimists: {0:s}".format(karmaer_list) + return self.bot.reply(event, msg) + elif report == 'top' or report == 'opinionated': + karmaers = KarmaLogEntry.objects.most_opinionated() + karmaer_list = ", ".join(["{0:s} ({1:d})".format(irc.client.NickMask(x['nickmask']).nick, + x['karma_count']) for x in karmaers]) + msg = "top 5 opinionated users: {0:s}".format(karmaer_list) + return self.bot.reply(event, msg) + + def handle_stats(self, connection, event, match): + """Provide stats on a karma user.""" + + karmaer = match.group(1) + log_entries = KarmaLogEntry.objects.filter(nickmask=karmaer) + if len(log_entries) == 0: + # fallback, try to match the nick part of nickmask + log_entries = KarmaLogEntry.objects.filter(nickmask__startswith='{0:s}!'.format(karmaer)) + + if len(log_entries) == 0: + return self.bot.reply(event, "karma user {0:s} not found".format(karmaer)) + + total_karma = log_entries.count() + positive_karma = log_entries.filter(delta__gt=0).count() + negative_karma = log_entries.filter(delta__lt=0).count() + msg = ("{0:s} has given {1:d} postive karma and {2:d} negative karma, for a total of {3:d} karma" + "".format(karmaer, positive_karma, negative_karma, total_karma)) + return self.bot.reply(event, msg) + + +plugin = Karma diff --git a/dr_botzo/karma/migrations/0001_initial.py b/dr_botzo/karma/migrations/0001_initial.py new file mode 100644 index 0000000..f433d29 --- /dev/null +++ b/dr_botzo/karma/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='KarmaKey', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('key', models.CharField(unique=True, max_length=200)), + ], + ), + migrations.CreateModel( + name='KarmaLogEntry', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('delta', models.SmallIntegerField()), + ('nickmask', models.CharField(default='', max_length=200, blank=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('key', models.ForeignKey(to='karma.KarmaKey')), + ], + ), + ] diff --git a/dr_botzo/karma/migrations/0002_auto_20150519_2156.py b/dr_botzo/karma/migrations/0002_auto_20150519_2156.py new file mode 100644 index 0000000..5908192 --- /dev/null +++ b/dr_botzo/karma/migrations/0002_auto_20150519_2156.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('karma', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='karmalogentry', + options={'verbose_name_plural': 'karma log entries'}, + ), + ] diff --git a/dr_botzo/karma/migrations/__init__.py b/dr_botzo/karma/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dr_botzo/karma/models.py b/dr_botzo/karma/models.py new file mode 100644 index 0000000..07302eb --- /dev/null +++ b/dr_botzo/karma/models.py @@ -0,0 +1,95 @@ +"""Karma logging models.""" + +from __future__ import unicode_literals + +import logging +import pytz + +from irc.client import NickMask + +from django.conf import settings +from django.db import models + + +log = logging.getLogger('karma.models') + + +class KarmaKeyManager(models.Manager): + + """Manage handy queries for KarmaKey.""" + + def ranked_scored_order(self): + keys = self.annotate(karma_score=models.Sum('karmalogentry__delta')).order_by('-karma_score') + return keys + + def ranked_scored_reverse_order(self): + keys = self.annotate(karma_score=models.Sum('karmalogentry__delta')).order_by('karma_score') + return keys + + +class KarmaKey(models.Model): + + """Track a thing being karmaed.""" + + key = models.CharField(max_length=200, unique=True) + + objects = KarmaKeyManager() + + def __unicode__(self): + """String representation.""" + + return "{0:s} ({1:d})".format(self.key, self.score()) + + def score(self): + """Determine the score for this karma entry.""" + + return KarmaLogEntry.objects.filter(key=self).aggregate(models.Sum('delta'))['delta__sum'] + + def rank(self): + """Determine the rank of this karma entry relative to the others.""" + + sorted_keys = KarmaKey.objects.ranked_scored_order() + for rank, key in enumerate(sorted_keys): + if key == self: + return rank+1 + return None + + +class KarmaLogEntryManager(models.Manager): + + """Manage handy queries for KarmaLogEntry.""" + + def optimists(self): + karmaers = self.values('nickmask').annotate(karma_outlook=models.Sum('delta')).order_by('-karma_outlook') + return karmaers + + def pessimists(self): + karmaers = self.values('nickmask').annotate(karma_outlook=models.Sum('delta')).order_by('karma_outlook') + return karmaers + + def most_opinionated(self): + karmaers = self.values('nickmask').annotate(karma_count=models.Count('delta')).order_by('-karma_count') + return karmaers + + +class KarmaLogEntry(models.Model): + + """Track each karma increment/decrement.""" + + key = models.ForeignKey('KarmaKey') + delta = models.SmallIntegerField() + nickmask = models.CharField(max_length=200, default='', blank=True) + created = models.DateTimeField(auto_now_add=True) + + objects = KarmaLogEntryManager() + + class Meta: + verbose_name_plural = 'karma log entries' + + def __unicode__(self): + """String representation.""" + + tz = pytz.timezone(settings.TIME_ZONE) + return "{0:s}{1:s} @ {2:s} by {3:s}".format(self.key.key, '++' if self.delta > 0 else '--', + self.created.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S %Z'), + NickMask(self.nickmask).nick)