collapsing all of dr_botzo one directory
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
doc-warnings: true
|
||||
strictness: medium
|
||||
ignore-paths:
|
||||
- migrations
|
||||
ignore-patterns:
|
||||
- \.log$
|
||||
- localsettings.py$
|
||||
- parsetab.py$
|
||||
pylint:
|
||||
enable:
|
||||
- relative-import
|
||||
options:
|
||||
max-line-length: 120
|
||||
good-names: log
|
||||
pep8:
|
||||
options:
|
||||
max-line-length: 120
|
||||
@@ -1,349 +0,0 @@
|
||||
"""An acromania style game for IRC."""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
|
||||
from irc.client import NickMask, is_channel
|
||||
|
||||
from ircbot.lib import Plugin
|
||||
|
||||
|
||||
log = logging.getLogger('acro.ircplugin')
|
||||
|
||||
|
||||
class Acro(Plugin):
|
||||
|
||||
"""Play a game where users come up with silly definitions for randomly-generated acronyms."""
|
||||
|
||||
class AcroGame(object):
|
||||
|
||||
"""Track game details."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize basic stuff."""
|
||||
|
||||
# running state
|
||||
self.state = 0
|
||||
self.quit = False
|
||||
self.scores = dict()
|
||||
self.rounds = []
|
||||
self.channel = ''
|
||||
|
||||
class AcroRound(object):
|
||||
|
||||
"""Track a particular round of a game."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize basic stuff."""
|
||||
|
||||
self.acro = ""
|
||||
self.submissions = dict()
|
||||
self.sub_shuffle = []
|
||||
self.votes = dict()
|
||||
|
||||
# default options
|
||||
self.seconds_to_submit = 60
|
||||
self.seconds_to_vote = 45
|
||||
self.seconds_to_submit_step = 10
|
||||
self.seconds_to_pause = 5
|
||||
|
||||
def __init__(self, bot, connection, event):
|
||||
"""Set up the game tracking and such."""
|
||||
|
||||
# game state
|
||||
self.game = self.AcroGame()
|
||||
|
||||
super(Acro, self).__init__(bot, connection, event)
|
||||
|
||||
def start(self):
|
||||
"""Set up handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+status$',
|
||||
self.handle_status, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+start$',
|
||||
self.handle_start, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+submit\s+(.*)$',
|
||||
self.handle_submit, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+vote\s+(\d+)$',
|
||||
self.handle_vote, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+quit$',
|
||||
self.handle_quit, -20)
|
||||
|
||||
super(Acro, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_status)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_submit)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_vote)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_quit)
|
||||
|
||||
super(Acro, self).stop()
|
||||
|
||||
def handle_status(self, connection, event, match):
|
||||
"""Return the status of the currently-running game, if there is one."""
|
||||
|
||||
if self.game.state == 0:
|
||||
return self.bot.reply(event, "there currently isn't a game running.")
|
||||
else:
|
||||
return self.bot.reply(event, "the game is running.")
|
||||
|
||||
def handle_start(self, connection, event, match):
|
||||
"""Start the game, notify the channel, and begin timers."""
|
||||
|
||||
if self.game.state != 0:
|
||||
return self.bot.reply(event, "the game is already running.")
|
||||
else:
|
||||
if is_channel(event.target):
|
||||
self._start_new_game(event.target)
|
||||
else:
|
||||
return self.bot.reply("you must start the game from a channel.")
|
||||
|
||||
def handle_submit(self, connection, event, match):
|
||||
"""Take a submission for an acro."""
|
||||
|
||||
nick = NickMask(event.source).nick
|
||||
submission = match.group(1)
|
||||
|
||||
return self.bot.reply(event, self._take_acro_submission(nick, submission))
|
||||
|
||||
def handle_vote(self, connection, event, match):
|
||||
"""Take a vote for an acro."""
|
||||
|
||||
nick = NickMask(event.source).nick
|
||||
vote = match.group(1)
|
||||
|
||||
return self.bot.reply(event, self._take_acro_vote(nick, vote))
|
||||
|
||||
def handle_quit(self, connection, event, match):
|
||||
"""Quit the game after the current round ends."""
|
||||
|
||||
if self.game.state != 0:
|
||||
self.game.quit = True
|
||||
return self.bot.reply(event, "the game will end after the current round.")
|
||||
|
||||
def thread_do_process_submissions(self, sleep_time):
|
||||
"""Wait for players to provide acro submissions, and then kick off voting."""
|
||||
|
||||
time.sleep(sleep_time)
|
||||
self._start_voting()
|
||||
|
||||
def thread_do_process_votes(self):
|
||||
"""Wait for players to provide votes, and then continue or quit."""
|
||||
|
||||
time.sleep(self.game.rounds[-1].seconds_to_vote)
|
||||
self._end_current_round()
|
||||
|
||||
def _start_new_game(self, channel):
|
||||
"""Begin a new game, which will have multiple rounds."""
|
||||
|
||||
self.game.state = 1
|
||||
self.game.channel = channel
|
||||
self.bot.privmsg(self.game.channel, "starting a new game of acro. it will run until you tell it to quit.")
|
||||
|
||||
self._start_new_round()
|
||||
|
||||
def _start_new_round(self):
|
||||
"""Start a new round for play."""
|
||||
|
||||
self.game.state = 2
|
||||
self.game.rounds.append(self.AcroRound())
|
||||
acro = self._generate_acro()
|
||||
self.game.rounds[-1].acro = acro
|
||||
sleep_time = self.game.rounds[-1].seconds_to_submit + (self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3))
|
||||
|
||||
self.bot.privmsg(self.game.channel, "the round has started! your acronym is '{0:s}'. "
|
||||
"submit within {1:d} seconds via !acro submit [meaning]".format(acro, sleep_time))
|
||||
|
||||
t = threading.Thread(target=self.thread_do_process_submissions, args=(sleep_time,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
@staticmethod
|
||||
def _generate_acro():
|
||||
"""Generate an acro to play with.
|
||||
|
||||
Letter frequencies pinched from
|
||||
http://www.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html
|
||||
"""
|
||||
|
||||
acro = []
|
||||
# generate acro 3-8 characters long
|
||||
for i in range(1, random.randint(4, 9)):
|
||||
letter = random.randint(1, 182303)
|
||||
if letter <= 21912:
|
||||
acro.append('E')
|
||||
elif letter <= 38499:
|
||||
acro.append('T')
|
||||
elif letter <= 53309:
|
||||
acro.append('A')
|
||||
elif letter <= 67312:
|
||||
acro.append('O')
|
||||
elif letter <= 80630:
|
||||
acro.append('I')
|
||||
elif letter <= 93296:
|
||||
acro.append('N')
|
||||
elif letter <= 104746:
|
||||
acro.append('S')
|
||||
elif letter <= 115723:
|
||||
acro.append('R')
|
||||
elif letter <= 126518:
|
||||
acro.append('H')
|
||||
elif letter <= 134392:
|
||||
acro.append('D')
|
||||
elif letter <= 141645:
|
||||
acro.append('L')
|
||||
elif letter <= 146891:
|
||||
acro.append('U')
|
||||
elif letter <= 151834:
|
||||
acro.append('C')
|
||||
elif letter <= 156595:
|
||||
acro.append('M')
|
||||
elif letter <= 160795:
|
||||
acro.append('F')
|
||||
elif letter <= 164648:
|
||||
acro.append('Y')
|
||||
elif letter <= 168467:
|
||||
acro.append('W')
|
||||
elif letter <= 172160:
|
||||
acro.append('G')
|
||||
elif letter <= 175476:
|
||||
acro.append('P')
|
||||
elif letter <= 178191:
|
||||
acro.append('B')
|
||||
elif letter <= 180210:
|
||||
acro.append('V')
|
||||
elif letter <= 181467:
|
||||
acro.append('K')
|
||||
elif letter <= 181782:
|
||||
acro.append('X')
|
||||
elif letter <= 181987:
|
||||
acro.append('Q')
|
||||
elif letter <= 182175:
|
||||
acro.append('J')
|
||||
elif letter <= 182303:
|
||||
acro.append('Z')
|
||||
|
||||
return "".join(acro)
|
||||
|
||||
def _take_acro_submission(self, nick, submission):
|
||||
"""Take an acro submission and record it."""
|
||||
|
||||
if self.game.state == 2:
|
||||
sub_acro = self._turn_text_into_acro(submission)
|
||||
|
||||
if sub_acro == self.game.rounds[-1].acro:
|
||||
self.game.rounds[-1].submissions[nick] = submission
|
||||
return "your submission has been recorded. it replaced any old submission for this round."
|
||||
else:
|
||||
return "the current acro is '{0:s}', not '{1:s}'".format(self.game.rounds[-1].acro, sub_acro)
|
||||
|
||||
@staticmethod
|
||||
def _turn_text_into_acro(text):
|
||||
"""Turn text into an acronym."""
|
||||
|
||||
words = text.split()
|
||||
acro = []
|
||||
for w in words:
|
||||
acro.append(w[0].upper())
|
||||
return "".join(acro)
|
||||
|
||||
def _start_voting(self):
|
||||
"""Begin the voting period."""
|
||||
|
||||
self.game.state = 3
|
||||
self.game.rounds[-1].sub_shuffle = list(self.game.rounds[-1].submissions.keys())
|
||||
random.shuffle(self.game.rounds[-1].sub_shuffle)
|
||||
self.bot.privmsg(self.game.channel, "here are the results. vote with !acro vote [number]")
|
||||
self._print_round_acros()
|
||||
|
||||
t = threading.Thread(target=self.thread_do_process_votes, args=())
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _print_round_acros(self):
|
||||
"""Take the current round's acros and spit them to the channel."""
|
||||
|
||||
i = 0
|
||||
for s in self.game.rounds[-1].sub_shuffle:
|
||||
log.debug("%s is %s", str(i), s)
|
||||
self.bot.privmsg(self.game.channel, " {0:d}: {1:s}".format(i+1, self.game.rounds[-1].submissions[s]))
|
||||
i += 1
|
||||
|
||||
def _take_acro_vote(self, nick, vote):
|
||||
"""Take an acro vote and record it."""
|
||||
|
||||
if self.game.state == 3:
|
||||
acros = self.game.rounds[-1].submissions
|
||||
if int(vote) > 0 and int(vote) <= len(acros):
|
||||
log.debug("%s is %s", vote, self.game.rounds[-1].sub_shuffle[int(vote)-1])
|
||||
key = self.game.rounds[-1].sub_shuffle[int(vote)-1]
|
||||
|
||||
if key != nick:
|
||||
self.game.rounds[-1].votes[nick] = key
|
||||
return "your vote has been recorded. it replaced any old vote for this round."
|
||||
else:
|
||||
return "you can't vote for yourself!"
|
||||
else:
|
||||
return "you must vote for 1-{0:d}".format(len(acros))
|
||||
|
||||
def _end_current_round(self):
|
||||
"""Clean up and output for ending the current round."""
|
||||
|
||||
self.game.state = 4
|
||||
self.bot.privmsg(self.game.channel, "voting's over! here are the scores for the round:")
|
||||
self._print_round_scores()
|
||||
self._add_round_scores_to_game_scores()
|
||||
|
||||
# delay a bit
|
||||
time.sleep(self.game.rounds[-1].seconds_to_pause)
|
||||
self._continue_or_quit()
|
||||
|
||||
def _print_round_scores(self):
|
||||
"""For the acros in the round, find the votes for them."""
|
||||
|
||||
i = 0
|
||||
for s in list(self.game.rounds[-1].submissions.keys()):
|
||||
votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s]
|
||||
self.bot.privmsg(self.game.channel, " {0:d} ({1:s}): {2:d}".format(i+1, s, len(votes)))
|
||||
i += 1
|
||||
|
||||
def _add_round_scores_to_game_scores(self):
|
||||
"""Apply the final round scores to the totall scores for the game."""
|
||||
|
||||
for s in list(self.game.rounds[-1].votes.values()):
|
||||
votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s]
|
||||
if s in list(self.game.scores.keys()):
|
||||
self.game.scores[s] += len(votes)
|
||||
else:
|
||||
self.game.scores[s] = len(votes)
|
||||
|
||||
def _continue_or_quit(self):
|
||||
"""Decide whether the game should continue or quit."""
|
||||
|
||||
if self.game.state == 4:
|
||||
if self.game.quit:
|
||||
self._end_game()
|
||||
else:
|
||||
self._start_new_round()
|
||||
|
||||
def _end_game(self):
|
||||
"""Clean up the entire game."""
|
||||
|
||||
self.game.state = 0
|
||||
self.game.quit = False
|
||||
self.bot.privmsg(self.game.channel, "game's over! here are the final scores:")
|
||||
self._print_game_scores()
|
||||
|
||||
def _print_game_scores(self):
|
||||
"""Print the final calculated scores."""
|
||||
|
||||
for s in list(self.game.scores.keys()):
|
||||
self.bot.privmsg(self.game.channel, " {0:s}: {1:d}".format(s, self.game.scores[s]))
|
||||
|
||||
|
||||
plugin = Acro
|
||||
@@ -1,7 +0,0 @@
|
||||
"""Manage choices models."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from choices.models import ChoiceSet
|
||||
|
||||
admin.site.register(ChoiceSet)
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChoiceSet',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||
('name', models.CharField(max_length=20)),
|
||||
('choices', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Define choice set models"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ChoiceSet(models.Model):
|
||||
|
||||
"""Define collections of possible choices."""
|
||||
|
||||
name = models.CharField(max_length=20)
|
||||
choices = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} - {1:s}".format(self.name, self.choices)
|
||||
|
||||
def choices_list(self):
|
||||
"""Return choices as a list."""
|
||||
return self.choices.split(',')
|
||||
@@ -1,9 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}choice set: {{ choiceset.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{{ choiceset.name }}</h3>
|
||||
<p><strong>Choices:</strong> {{ choiceset.choices_list|join:", " }}</p>
|
||||
<p><strong>Random Choice:</strong> {{ choiceset.choices_list|random }}</p>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}choices{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for choiceset in choicesets %}
|
||||
<li><a href="{% url 'choices_choiceset_detail' choiceset.name %}">{{ choiceset.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -1,8 +0,0 @@
|
||||
"""URL patterns for choices."""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns('choices.views',
|
||||
url(r'^$', 'index', name='choices_index'),
|
||||
url(r'^(?P<set_name>.+)/$', 'choiceset_detail', name='choices_choiceset_detail'),
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Display choice sets."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from choices.models import ChoiceSet
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Display nothing, for the moment."""
|
||||
|
||||
choicesets = ChoiceSet.objects.all()
|
||||
|
||||
return render(request, 'choices/index.html', {'choicesets': choicesets})
|
||||
|
||||
|
||||
def choiceset_detail(request, set_name):
|
||||
"""Display info, and a random choice, for the requested choice set."""
|
||||
|
||||
choiceset = get_object_or_404(ChoiceSet, name=set_name)
|
||||
|
||||
return render(request, 'choices/choiceset_detail.html', {'choiceset': choiceset})
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Manage countdown models in the admin interface."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from countdown.models import CountdownItem
|
||||
|
||||
|
||||
admin.site.register(CountdownItem)
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Access to countdown items through bot commands."""
|
||||
|
||||
import logging
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from ircbot.lib import Plugin
|
||||
from countdown.models import CountdownItem
|
||||
|
||||
|
||||
log = logging.getLogger('countdown.ircplugin')
|
||||
|
||||
|
||||
class Countdown(Plugin):
|
||||
|
||||
"""Report on countdown items."""
|
||||
|
||||
def start(self):
|
||||
"""Set up handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
|
||||
self.handle_item_list, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$',
|
||||
self.handle_item_detail, -20)
|
||||
|
||||
super(Countdown, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_item_list)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_item_detail)
|
||||
|
||||
super(Countdown, self).stop()
|
||||
|
||||
def handle_item_detail(self, connection, event, match):
|
||||
"""Provide the details of one countdown item."""
|
||||
|
||||
name = match.group(1)
|
||||
|
||||
if name != 'list':
|
||||
try:
|
||||
item = CountdownItem.objects.get(name=name)
|
||||
rdelta = relativedelta(item.at_time, timezone.now())
|
||||
relstr = "{0:s} will occur in ".format(name)
|
||||
if rdelta.years != 0:
|
||||
relstr += "{0:s} year{1:s} ".format(str(rdelta.years), "s" if abs(rdelta.years) != 1 else "")
|
||||
if rdelta.months != 0:
|
||||
relstr += "{0:s} month{1:s}, ".format(str(rdelta.months), "s" if abs(rdelta.months) != 1 else "")
|
||||
if rdelta.days != 0:
|
||||
relstr += "{0:s} day{1:s}, ".format(str(rdelta.days), "s" if abs(rdelta.days) != 1 else "")
|
||||
if rdelta.hours != 0:
|
||||
relstr += "{0:s} hour{1:s}, ".format(str(rdelta.hours), "s" if abs(rdelta.hours) != 1 else "")
|
||||
if rdelta.minutes != 0:
|
||||
relstr += "{0:s} minute{1:s}, ".format(str(rdelta.minutes), "s" if abs(rdelta.minutes) != 1 else "")
|
||||
if rdelta.seconds != 0:
|
||||
relstr += "{0:s} second{1:s}, ".format(str(rdelta.seconds), "s" if abs(rdelta.seconds) != 1 else "")
|
||||
# remove trailing comma from output
|
||||
reply = relstr[0:-2]
|
||||
return self.bot.reply(event, reply)
|
||||
except CountdownItem.DoesNotExist:
|
||||
return self.bot.reply(event, "countdown item '{0:s}' not found".format(name))
|
||||
|
||||
def handle_item_list(self, connection, event, match):
|
||||
"""List all countdown items."""
|
||||
|
||||
items = CountdownItem.objects.all()
|
||||
if len(items) > 0:
|
||||
reply = "countdown items: {0:s}".format(", ".join([x.name for x in items]))
|
||||
return self.bot.reply(event, reply)
|
||||
else:
|
||||
return self.bot.reply(event, "no countdown items are configured")
|
||||
|
||||
|
||||
plugin = Countdown
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CountdownItem',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(default='', max_length=64)),
|
||||
('source', models.CharField(default='', max_length=200)),
|
||||
('at_time', models.DateTimeField()),
|
||||
('created_time', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('countdown', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='countdownitem',
|
||||
name='source',
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Countdown item models."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
log = logging.getLogger('countdown.models')
|
||||
|
||||
|
||||
class CountdownItem(models.Model):
|
||||
|
||||
"""Track points in time."""
|
||||
|
||||
name = models.CharField(max_length=64, default='')
|
||||
at_time = models.DateTimeField()
|
||||
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))
|
||||
@@ -1,423 +0,0 @@
|
||||
"""Roll dice when asked, intended for RPGs."""
|
||||
|
||||
# this breaks yacc, but ply might be happy in py3
|
||||
#from __future__ import unicode_literals
|
||||
|
||||
import math
|
||||
import re
|
||||
import random
|
||||
|
||||
from irc.client import NickMask
|
||||
|
||||
import ply.lex as lex
|
||||
import ply.yacc as yacc
|
||||
|
||||
from ircbot.lib import Plugin
|
||||
|
||||
class Dice(Plugin):
|
||||
|
||||
"""Roll simple or complex dice strings."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.roller = DiceRoller()
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$',
|
||||
self.handle_roll, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!ctech\s+(.*)$',
|
||||
self.handle_ctech, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$',
|
||||
self.handle_random, -20)
|
||||
|
||||
super(Dice, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_roll)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_ctech)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random)
|
||||
|
||||
super(Dice, self).stop()
|
||||
|
||||
def handle_random(self, connection, event, match):
|
||||
"""Handle the !random command which picks an item from a list."""
|
||||
|
||||
nick = NickMask(event.source).nick
|
||||
choices = match.group(1)
|
||||
|
||||
choices_list = choices.split(' ')
|
||||
choice = random.choice(choices_list)
|
||||
|
||||
reply = "{0:s}: {1:s}".format(nick, choice)
|
||||
return self.bot.reply(event, reply)
|
||||
|
||||
def handle_roll(self, connection, event, match):
|
||||
"""Handle the !roll command which covers most common dice stuff."""
|
||||
|
||||
nick = NickMask(event.source).nick
|
||||
dicestr = match.group(1)
|
||||
|
||||
reply = "{0:s}: {1:s}".format(nick, self.roller.do_roll(dicestr))
|
||||
return self.bot.reply(event, re.sub(r'(\d+)(.*?\s+)(\(.*?\))', r'\1\214\3', reply))
|
||||
|
||||
def handle_ctech(self, connection, event, match):
|
||||
"""Handle cthulhutech dice rolls."""
|
||||
|
||||
nick = NickMask(event.source).nick
|
||||
rollitrs = re.split(';\s*', match.group(1))
|
||||
reply = ""
|
||||
for count, roll in enumerate(rollitrs):
|
||||
pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?'
|
||||
regex = re.compile(pattern)
|
||||
matches = regex.search(roll)
|
||||
if matches is not None:
|
||||
dice = int(matches.group(1))
|
||||
modifier = 0
|
||||
|
||||
if matches.group(2) is not None and matches.group(3) is not None:
|
||||
if str(matches.group(2)) == '-':
|
||||
modifier = -1 * int(matches.group(3))
|
||||
else:
|
||||
modifier = int(matches.group(3))
|
||||
|
||||
result = roll + ': '
|
||||
|
||||
rolls = []
|
||||
for d in range(dice):
|
||||
rolls.append(random.randint(1, 10))
|
||||
rolls.sort()
|
||||
rolls.reverse()
|
||||
|
||||
# highest single die method
|
||||
method1 = rolls[0]
|
||||
|
||||
# highest set method
|
||||
method2 = 0
|
||||
rolling_sum = 0
|
||||
for i, r in enumerate(rolls):
|
||||
# if next roll is same as current, sum and continue, else see if sum is best so far
|
||||
if i+1 < len(rolls) and rolls[i+1] == r:
|
||||
if rolling_sum == 0:
|
||||
rolling_sum = r
|
||||
rolling_sum += r
|
||||
else:
|
||||
if rolling_sum > method2:
|
||||
method2 = rolling_sum
|
||||
rolling_sum = 0
|
||||
# check for set in progress (e.g. lots of 1s)
|
||||
if rolling_sum > method2:
|
||||
method2 = rolling_sum
|
||||
|
||||
# straight method
|
||||
method3 = 0
|
||||
rolling_sum = 0
|
||||
count = 0
|
||||
for i, r in enumerate(rolls):
|
||||
# if next roll is one less as current, sum and continue, else check len and see if sum is best so far
|
||||
if i+1 < len(rolls) and rolls[i+1] == r-1:
|
||||
if rolling_sum == 0:
|
||||
rolling_sum = r
|
||||
count += 1
|
||||
rolling_sum += r-1
|
||||
count += 1
|
||||
else:
|
||||
if count >= 3 and rolling_sum > method3:
|
||||
method3 = rolling_sum
|
||||
rolling_sum = 0
|
||||
# check for straight in progress (e.g. straight ending in 1)
|
||||
if count >= 3 and rolling_sum > method3:
|
||||
method3 = rolling_sum
|
||||
|
||||
# get best roll
|
||||
best = max([method1, method2, method3])
|
||||
|
||||
# check for critical failure
|
||||
botch = False
|
||||
ones = 0
|
||||
for r in rolls:
|
||||
if r == 1:
|
||||
ones += 1
|
||||
if ones >= math.ceil(float(len(rolls))/2):
|
||||
botch = True
|
||||
|
||||
if botch:
|
||||
result += 'BOTCH'
|
||||
else:
|
||||
result += str(best + modifier)
|
||||
rollres = ''
|
||||
for i,r in enumerate(rolls):
|
||||
rollres += str(r)
|
||||
if i is not len(rolls)-1:
|
||||
rollres += ','
|
||||
result += ' [' + rollres
|
||||
if modifier != 0:
|
||||
if modifier > 0:
|
||||
result += ' +' + str(modifier)
|
||||
else:
|
||||
result += ' -' + str(modifier * -1)
|
||||
result += ']'
|
||||
|
||||
reply += result
|
||||
if count is not len(rollitrs)-1:
|
||||
reply += "; "
|
||||
if reply is not "":
|
||||
msg = "{0:s}: {1:s}".format(nick, reply)
|
||||
return self.bot.reply(event, msg)
|
||||
|
||||
|
||||
class DiceRoller(object):
|
||||
|
||||
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
|
||||
literals = ['#', '/', '+', '-', 'd']
|
||||
|
||||
t_TEXT = r'\s+[^;]+'
|
||||
t_ROLLSEP = r';\s*'
|
||||
|
||||
def build(self):
|
||||
lex.lex(module=self)
|
||||
yacc.yacc(module=self)
|
||||
|
||||
def t_NUMBER(self, t):
|
||||
r'\d+'
|
||||
t.value = int(t.value)
|
||||
return t
|
||||
|
||||
def t_error(self, t):
|
||||
t.lexer.skip(1)
|
||||
|
||||
precedence = (
|
||||
('left', 'ROLLSEP'),
|
||||
('left', '+', '-'),
|
||||
('right', 'd'),
|
||||
('left', '#'),
|
||||
('left', '/')
|
||||
)
|
||||
|
||||
output = ""
|
||||
|
||||
def roll_dice(self, keep, dice, size):
|
||||
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
|
||||
the actual roll. Returns a string representing the result.
|
||||
"""
|
||||
|
||||
a = list(range(dice))
|
||||
for i in range(dice):
|
||||
a[i] = random.randint(1, size)
|
||||
if keep != dice:
|
||||
b = sorted(a, reverse=True)
|
||||
b = b[0:keep]
|
||||
else:
|
||||
b = a
|
||||
total = sum(b)
|
||||
outstr = "[" + ",".join(str(i) for i in a) + "]"
|
||||
|
||||
return (total, outstr)
|
||||
|
||||
def process_roll(self, trials, mods, comment):
|
||||
"""Processes rolls coming from the parser.
|
||||
|
||||
This generates the inputs for the roll_dice() command, and returns
|
||||
the full string representing the whole current dice string (the part
|
||||
up to a semicolon or end of line).
|
||||
"""
|
||||
|
||||
output = ""
|
||||
repeat = 1
|
||||
if trials != None:
|
||||
repeat = trials
|
||||
for i in range(repeat):
|
||||
mode = 1
|
||||
total = 0
|
||||
curr_str = ""
|
||||
if i > 0:
|
||||
output += ", "
|
||||
for m in mods:
|
||||
keep = 0
|
||||
dice = 1
|
||||
res = 0
|
||||
# if m is a tuple, then it is a die roll
|
||||
# m[0] = (keep, num dice)
|
||||
# m[1] = num faces on the die
|
||||
if type(m) == tuple:
|
||||
if m[0] != None:
|
||||
if m[0][0] != None:
|
||||
keep = m[0][0]
|
||||
dice = m[0][1]
|
||||
size = m[1]
|
||||
if keep > dice or keep == 0:
|
||||
keep = dice
|
||||
if size < 1:
|
||||
output = "# of sides for die is incorrect: %d" % size
|
||||
return output
|
||||
if dice < 1:
|
||||
output = "# of dice is incorrect: %d" % dice
|
||||
return output
|
||||
res = self.roll_dice(keep, dice, size)
|
||||
curr_str += "%d%s" % (res[0], res[1])
|
||||
res = res[0]
|
||||
elif m == "+":
|
||||
mode = 1
|
||||
curr_str += "+"
|
||||
elif m == "-":
|
||||
mode = -1
|
||||
curr_str += "-"
|
||||
else:
|
||||
res = m
|
||||
curr_str += str(m)
|
||||
total += mode * res
|
||||
if repeat == 1:
|
||||
if comment != None:
|
||||
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
|
||||
else:
|
||||
output = "%d (%s)" % (total, curr_str)
|
||||
else:
|
||||
output += "%d (%s)" % (total, curr_str)
|
||||
if i == repeat - 1:
|
||||
if comment != None:
|
||||
output += " (%s)" % (comment.strip())
|
||||
return output
|
||||
|
||||
def p_roll_r(self, p):
|
||||
# Chain rolls together.
|
||||
|
||||
# General idea I had when creating this grammar: A roll string is a chain
|
||||
# of modifiers, which may be repeated for a certain number of trials. It can
|
||||
# have a comment that describes the roll
|
||||
# Multiple roll strings can be chained with semicolon
|
||||
|
||||
'roll : roll ROLLSEP roll'
|
||||
global output
|
||||
p[0] = p[1] + "; " + p[3]
|
||||
output = p[0]
|
||||
|
||||
def p_roll(self, p):
|
||||
# Parse a basic roll string.
|
||||
|
||||
'roll : trial modifier comment'
|
||||
global output
|
||||
mods = []
|
||||
if type(p[2]) == list:
|
||||
mods = p[2]
|
||||
else:
|
||||
mods = [p[2]]
|
||||
p[0] = self.process_roll(p[1], mods, p[3])
|
||||
output = p[0]
|
||||
|
||||
def p_roll_no_trials(self, p):
|
||||
# Parse a roll string without trials.
|
||||
|
||||
'roll : modifier comment'
|
||||
global output
|
||||
mods = []
|
||||
if type(p[1]) == list:
|
||||
mods = p[1]
|
||||
else:
|
||||
mods = [p[1]]
|
||||
p[0] = self.process_roll(None, mods, p[2])
|
||||
output = p[0]
|
||||
|
||||
def p_comment(self, p):
|
||||
# Parse a comment.
|
||||
|
||||
'''comment : TEXT
|
||||
|'''
|
||||
if len(p) == 2:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_modifier(self, p):
|
||||
# Parse a modifier on a roll string.
|
||||
|
||||
'''modifier : modifier "+" modifier
|
||||
| modifier "-" modifier'''
|
||||
# Use append to prevent nested lists (makes dealing with this easier)
|
||||
if type(p[1]) == list:
|
||||
p[1].append(p[2])
|
||||
p[1].append(p[3])
|
||||
p[0] = p[1]
|
||||
elif type(p[3]) == list:
|
||||
p[3].insert(0, p[2])
|
||||
p[3].insert(0, p[1])
|
||||
p[0] = p[3]
|
||||
else:
|
||||
p[0] = [p[1], p[2], p[3]]
|
||||
|
||||
def p_die(self, p):
|
||||
# Return the left side before the "d", and the number of faces.
|
||||
|
||||
'modifier : left NUMBER'
|
||||
p[0] = (p[1], p[2])
|
||||
|
||||
def p_die_num(self, p):
|
||||
'modifier : NUMBER'
|
||||
p[0] = p[1]
|
||||
|
||||
def p_left(self, p):
|
||||
# Parse the number of dice we are rolling, and how many we are keeping.
|
||||
'left : keep dice'
|
||||
if p[1] == None:
|
||||
p[0] = [None, p[2]]
|
||||
else:
|
||||
p[0] = [p[1], p[2]]
|
||||
|
||||
def p_left_all(self, p):
|
||||
'left : dice'
|
||||
p[0] = [None, p[1]]
|
||||
|
||||
def p_left_e(self, p):
|
||||
'left :'
|
||||
p[0] = None
|
||||
|
||||
def p_total(self, p):
|
||||
'trial : NUMBER "#"'
|
||||
if len(p) > 1:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_keep(self, p):
|
||||
'keep : NUMBER "/"'
|
||||
if p[1] != None:
|
||||
p[0] = p[1]
|
||||
else:
|
||||
p[0] = None
|
||||
|
||||
def p_dice(self, p):
|
||||
'dice : NUMBER "d"'
|
||||
p[0] = p[1]
|
||||
|
||||
def p_dice_one(self, p):
|
||||
'dice : "d"'
|
||||
p[0] = 1
|
||||
|
||||
def p_error(self, p):
|
||||
# Provide the user with something (albeit not much) when the roll can't be parsed.
|
||||
global output
|
||||
output = "Unable to parse roll"
|
||||
|
||||
def get_result(self):
|
||||
global output
|
||||
return output
|
||||
|
||||
def do_roll(self, dicestr):
|
||||
"""
|
||||
Roll some dice and get the result (with broken out rolls).
|
||||
|
||||
Keyword arguments:
|
||||
dicestr - format:
|
||||
N#X/YdS+M label
|
||||
N#: do the following roll N times (optional)
|
||||
X/: take the top X rolls of the Y times rolled (optional)
|
||||
Y : roll the die specified Y times (optional, defaults to 1)
|
||||
dS: roll a S-sided die
|
||||
+M: add M to the result (-M for subtraction) (optional)
|
||||
"""
|
||||
self.build()
|
||||
yacc.parse(dicestr)
|
||||
return self.get_result()
|
||||
|
||||
|
||||
plugin = Dice
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Manage dispatch models in the admin interface."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from dispatch.models import Dispatcher, DispatcherAction
|
||||
|
||||
|
||||
admin.site.register(Dispatcher)
|
||||
admin.site.register(DispatcherAction)
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Dispatcher',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('key', models.CharField(unique=True, max_length=16)),
|
||||
('type', models.CharField(max_length=16, choices=[(b'privmsg', b'IRC privmsg'), (b'file', b'Write to file')])),
|
||||
('destination', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatch', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DispatcherAction',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('type', models.CharField(max_length=16, choices=[(b'privmsg', b'IRC privmsg'), (b'file', b'Write to file')])),
|
||||
('destination', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='dispatcher',
|
||||
name='destination',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='dispatcher',
|
||||
name='type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='dispatcheraction',
|
||||
name='dispatcher',
|
||||
field=models.ForeignKey(to='dispatch.Dispatcher'),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatch', '0002_auto_20150619_1124'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='dispatcher',
|
||||
options={'permissions': (('send_message', 'Can send messages to dispatchers'),)},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dispatcheraction',
|
||||
name='dispatcher',
|
||||
field=models.ForeignKey(related_name='actions', to='dispatch.Dispatcher'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatch', '0003_auto_20150619_1637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dispatcheraction',
|
||||
name='include_key',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatch', '0004_dispatcheraction_include_key'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dispatcheraction',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('privmsg', 'IRC privmsg'), ('file', 'Write to file')], max_length=16),
|
||||
),
|
||||
]
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Track dispatcher configurations."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
log = logging.getLogger('dispatch.models')
|
||||
|
||||
|
||||
class Dispatcher(models.Model):
|
||||
|
||||
"""Organize dispatchers by key."""
|
||||
|
||||
key = models.CharField(max_length=16, unique=True)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('send_message', "Can send messages to dispatchers"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s}".format(self.key)
|
||||
|
||||
|
||||
class DispatcherAction(models.Model):
|
||||
|
||||
"""Handle requests to dispatchers and do something with them."""
|
||||
|
||||
PRIVMSG_TYPE = 'privmsg'
|
||||
FILE_TYPE = 'file'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(PRIVMSG_TYPE, "IRC privmsg"),
|
||||
(FILE_TYPE, "Write to file"),
|
||||
)
|
||||
|
||||
dispatcher = models.ForeignKey('Dispatcher', related_name='actions')
|
||||
type = models.CharField(max_length=16, choices=TYPE_CHOICES)
|
||||
destination = models.CharField(max_length=200)
|
||||
include_key = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination)
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Serializers for the dispatcher API objects."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from dispatch.models import Dispatcher, DispatcherAction
|
||||
|
||||
|
||||
class DispatcherActionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DispatcherAction
|
||||
fields = ('id', 'dispatcher', 'type', 'destination')
|
||||
|
||||
|
||||
class DispatcherSerializer(serializers.ModelSerializer):
|
||||
|
||||
actions = DispatcherActionSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Dispatcher
|
||||
fields = ('id', 'key', 'actions')
|
||||
|
||||
|
||||
class DispatchMessageSerializer(serializers.Serializer):
|
||||
|
||||
message = serializers.CharField()
|
||||
status = serializers.CharField(read_only=True)
|
||||
@@ -1,20 +0,0 @@
|
||||
"""URL patterns for the dispatcher API."""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from dispatch.views import (DispatchMessage, DispatchMessageByKey, DispatcherList, DispatcherDetail,
|
||||
DispatcherDetailByKey, DispatcherActionList, DispatcherActionDetail)
|
||||
|
||||
|
||||
urlpatterns = patterns('dispatch.views',
|
||||
url(r'^api/dispatchers/$', DispatcherList.as_view(), name='dispatch_api_dispatchers'),
|
||||
url(r'^api/dispatchers/(?P<pk>[0-9]+)/$', DispatcherDetail.as_view(), name='dispatch_api_dispatcher_detail'),
|
||||
url(r'^api/dispatchers/(?P<pk>[0-9]+)/message$', DispatchMessage.as_view(), name='dispatch_api_dispatch_message'),
|
||||
url(r'^api/dispatchers/(?P<key>[A-Za-z-]+)/$', DispatcherDetailByKey.as_view(),
|
||||
name='dispatch_api_dispatcher_detail'),
|
||||
url(r'^api/dispatchers/(?P<key>[A-Za-z-]+)/message$', DispatchMessageByKey.as_view(),
|
||||
name='dispatch_api_dispatch_message'),
|
||||
|
||||
url(r'^api/actions/$', DispatcherActionList.as_view(), name='dispatch_api_actions'),
|
||||
url(r'^api/actions/(?P<pk>[0-9]+)/$', DispatcherActionDetail.as_view(), name='dispatch_api_action_detail'),
|
||||
)
|
||||
@@ -1,121 +0,0 @@
|
||||
"""Handle dispatcher API requests."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import xmlrpc.client
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from dispatch.models import Dispatcher, DispatcherAction
|
||||
from dispatch.serializers import DispatchMessageSerializer, DispatcherSerializer, DispatcherActionSerializer
|
||||
|
||||
|
||||
log = logging.getLogger('dispatch.views')
|
||||
|
||||
|
||||
class HasSendMessagePermission(IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.user.has_perm('dispatch.send_message'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DispatcherList(generics.ListAPIView):
|
||||
|
||||
"""List all dispatchers."""
|
||||
|
||||
queryset = Dispatcher.objects.all()
|
||||
serializer_class = DispatcherSerializer
|
||||
|
||||
|
||||
class DispatcherDetail(generics.RetrieveAPIView):
|
||||
|
||||
"""Detail the given dispatcher."""
|
||||
|
||||
queryset = Dispatcher.objects.all()
|
||||
serializer_class = DispatcherSerializer
|
||||
|
||||
|
||||
class DispatcherDetailByKey(DispatcherDetail):
|
||||
|
||||
lookup_field = 'key'
|
||||
|
||||
|
||||
class DispatchMessage(generics.GenericAPIView):
|
||||
|
||||
"""Send a message to the given dispatcher."""
|
||||
|
||||
permission_classes = (HasSendMessagePermission,)
|
||||
|
||||
queryset = Dispatcher.objects.all()
|
||||
serializer_class = DispatchMessageSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
dispatcher = self.get_object()
|
||||
data = {'message': "", 'status': "READY"}
|
||||
message = self.serializer_class(data=data)
|
||||
return Response(message.initial_data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
dispatcher = self.get_object()
|
||||
message = self.serializer_class(data=request.data)
|
||||
if message.is_valid():
|
||||
for action in dispatcher.actions.all():
|
||||
# manipulate the message if desired
|
||||
if action.include_key:
|
||||
text = "[{0:s}] {1:s}".format(action.dispatcher.key, message.data['message'])
|
||||
else:
|
||||
text = message.data['message']
|
||||
|
||||
if action.type == DispatcherAction.PRIVMSG_TYPE:
|
||||
# connect over XML-RPC and send
|
||||
try:
|
||||
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT)
|
||||
bot = xmlrpc.client.ServerProxy(bot_url)
|
||||
log.debug("sending '%s' to channel %s", text, action.destination)
|
||||
bot.privmsg(action.destination, text)
|
||||
except Exception as e:
|
||||
new_data = copy.deepcopy(message.data)
|
||||
new_data['status'] = "FAILED - {0:s}".format(str(e))
|
||||
new_message = self.serializer_class(data=new_data)
|
||||
return Response(new_message.initial_data)
|
||||
elif action.type == DispatcherAction.FILE_TYPE:
|
||||
# write to file
|
||||
filename = os.path.abspath(action.destination)
|
||||
log.debug("sending '%s' to file %s", text, filename)
|
||||
with open(filename, 'w') as f:
|
||||
f.write(text + '\n')
|
||||
|
||||
new_data = copy.deepcopy(message.data)
|
||||
new_data['status'] = "OK"
|
||||
new_message = self.serializer_class(data=new_data)
|
||||
return Response(new_message.initial_data)
|
||||
|
||||
return Response(message.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class DispatchMessageByKey(DispatchMessage):
|
||||
|
||||
lookup_field = 'key'
|
||||
|
||||
|
||||
class DispatcherActionList(generics.ListAPIView):
|
||||
|
||||
"""List all dispatchers."""
|
||||
|
||||
queryset = DispatcherAction.objects.all()
|
||||
serializer_class = DispatcherActionSerializer
|
||||
|
||||
|
||||
class DispatcherActionDetail(generics.RetrieveAPIView):
|
||||
|
||||
"""Detail the given dispatcher."""
|
||||
|
||||
queryset = DispatcherAction.objects.all()
|
||||
serializer_class = DispatcherActionSerializer
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from facts.models import Fact, FactCategory
|
||||
|
||||
|
||||
admin.site.register(Fact)
|
||||
admin.site.register(FactCategory)
|
||||
@@ -1,68 +0,0 @@
|
||||
"""IRC plugin for retrieval of facts."""
|
||||
|
||||
import logging
|
||||
|
||||
from irc.client import NickMask
|
||||
|
||||
from ircbot.lib import Plugin, has_permission
|
||||
from facts.models import Fact, FactCategory
|
||||
|
||||
|
||||
log = logging.getLogger('facts.ircplugin')
|
||||
|
||||
|
||||
class Facts(Plugin):
|
||||
|
||||
"""Present facts to IRC."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!facts\s+add\s+(\S+)\s+(.*)$',
|
||||
self.handle_add_fact, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!facts\s+(\S+)(\s+(.*)$|$)',
|
||||
self.handle_facts, -20)
|
||||
|
||||
super(Facts, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_add_fact)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_facts)
|
||||
|
||||
super(Facts, self).stop()
|
||||
|
||||
def handle_facts(self, connection, event, match):
|
||||
"""Respond to the facts command with desired fact."""
|
||||
|
||||
category = match.group(1)
|
||||
regex = None
|
||||
if match.group(2) != '':
|
||||
regex = match.group(3)
|
||||
|
||||
fact = Fact.objects.random_fact(category, regex)
|
||||
if fact:
|
||||
if fact.category.show_source:
|
||||
nick = NickMask(fact.nickmask).nick
|
||||
msg = "{0:s} ({1:s})".format(fact.fact, nick)
|
||||
else:
|
||||
msg = fact.fact
|
||||
|
||||
return self.bot.reply(event, msg)
|
||||
|
||||
def handle_add_fact(self, connection, event, match):
|
||||
"""Add a new fact to the database."""
|
||||
|
||||
category_name = match.group(1)
|
||||
fact_text = match.group(2)
|
||||
|
||||
if has_permission(event.source, 'facts.add_fact'):
|
||||
# create the category
|
||||
category, created = FactCategory.objects.get_or_create(name=category_name)
|
||||
fact = Fact.objects.create(fact=fact_text, category=category, nickmask=event.source)
|
||||
if fact:
|
||||
return self.bot.reply(event, "fact added to {0:s}".format(category.name))
|
||||
|
||||
|
||||
plugin = Facts
|
||||
@@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fact',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('fact', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FactCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fact',
|
||||
name='category',
|
||||
field=models.ForeignKey(to='facts.FactCategory'),
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('facts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='factcategory',
|
||||
options={'verbose_name_plural': 'fact categories'},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('facts', '0002_auto_20150521_2224'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='fact',
|
||||
name='nickmask',
|
||||
field=models.CharField(default='', max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='factcategory',
|
||||
name='show_source',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
from django.utils.timezone import utc
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('facts', '0003_auto_20150620_1018'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='fact',
|
||||
name='time',
|
||||
field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 15, 22, 20, 481856, tzinfo=utc), auto_now_add=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('facts', '0004_fact_time'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='fact',
|
||||
name='nickmask',
|
||||
field=models.CharField(default='', max_length=200, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Store "facts"."""
|
||||
|
||||
import logging
|
||||
import random
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
log = logging.getLogger('facts.models')
|
||||
|
||||
|
||||
class FactCategory(models.Model):
|
||||
|
||||
"""Define categories for facts."""
|
||||
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
show_source = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'fact categories'
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s}".format(self.name)
|
||||
|
||||
|
||||
class FactManager(models.Manager):
|
||||
|
||||
"""Queries against Fact."""
|
||||
|
||||
def random_fact(self, category, regex=None):
|
||||
"""Get a random fact from the database."""
|
||||
|
||||
try:
|
||||
fact_category = FactCategory.objects.get(name=category)
|
||||
except FactCategory.DoesNotExist:
|
||||
return None
|
||||
|
||||
facts = Fact.objects.filter(category=fact_category)
|
||||
if regex:
|
||||
facts = facts.filter(fact__iregex=regex)
|
||||
|
||||
if len(facts) > 0:
|
||||
return random.choice(facts)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Fact(models.Model):
|
||||
|
||||
"""Define facts."""
|
||||
|
||||
fact = models.TextField()
|
||||
category = models.ForeignKey(FactCategory)
|
||||
nickmask = models.CharField(max_length=200, default='', blank=True)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
objects = FactManager()
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} - {1:s}".format(self.category.name, self.fact)
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Admin stuff for GitLab bot models."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from gitlab_bot.models import GitlabConfig, GitlabProjectConfig
|
||||
|
||||
admin.site.register(GitlabConfig)
|
||||
admin.site.register(GitlabProjectConfig)
|
||||
@@ -1,253 +0,0 @@
|
||||
"""Wrapped client for the configured GitLab bot."""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
|
||||
import gitlab
|
||||
|
||||
from gitlab_bot.models import GitlabConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GitlabBot(object):
|
||||
|
||||
"""Bot for doing GitLab stuff. Might be used by the IRC bot, or daemons, or whatever."""
|
||||
|
||||
REVIEWS_START_FORMAT = "Starting review process."
|
||||
REVIEWS_RESET_FORMAT = "Review process reset."
|
||||
REVIEWS_REVIEW_REGISTERED_FORMAT = "Review identified."
|
||||
REVIEWS_PROGRESS_FORMAT = "{0:d} reviews of {1:d} necessary to proceed."
|
||||
REVIEWS_REVIEWS_COMPLETE = "Reviews complete."
|
||||
NEW_REVIEWER_FORMAT = "Assigning to {0:s} to review merge request."
|
||||
NEW_ACCEPTER_FORMAT = "Assigning to {0:s} to accept merge request."
|
||||
|
||||
NOTE_COMMENT = [
|
||||
"The computer is your friend.",
|
||||
"Trust the computer.",
|
||||
"Quality is mandatory.",
|
||||
"Errors show your disloyalty to the computer.",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the actual GitLab client."""
|
||||
config = GitlabConfig.objects.first()
|
||||
self.client = gitlab.Gitlab(config.url, private_token=config.token)
|
||||
self.client.auth()
|
||||
|
||||
def random_reviews_start_message(self):
|
||||
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_START_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
|
||||
random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def random_reviews_reset_message(self):
|
||||
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_RESET_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
|
||||
random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def random_review_progress_message(self):
|
||||
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_REVIEW_REGISTERED_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
|
||||
random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def random_reviews_complete_message(self):
|
||||
return "{0:s} {1:s}".format(self.REVIEWS_REVIEWS_COMPLETE, random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def random_new_reviewer_message(self):
|
||||
return "{0:s} {1:s}".format(self.NEW_REVIEWER_FORMAT, random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def random_reviews_done_message(self):
|
||||
return "{0:s} {1:s}".format(self.NEW_ACCEPTER_FORMAT, random.choice(self.NOTE_COMMENT))
|
||||
|
||||
def scan_project_for_reviews(self, project, merge_request_ids=None):
|
||||
project_obj = self.client.projects.get(project.project_id)
|
||||
if not project_obj:
|
||||
return
|
||||
|
||||
if merge_request_ids:
|
||||
merge_requests = []
|
||||
for merge_request_id in merge_request_ids:
|
||||
merge_requests.append(project_obj.mergerequests.get(id=merge_request_id))
|
||||
else:
|
||||
merge_requests = project_obj.mergerequests.list(state='opened')
|
||||
|
||||
for merge_request in merge_requests:
|
||||
log.debug("scanning merge request '%s'", merge_request.title)
|
||||
|
||||
if merge_request.state in ['merged', 'closed']:
|
||||
log.info("merge request '%s' is already %s, doing nothing", merge_request.title,
|
||||
merge_request.state)
|
||||
continue
|
||||
|
||||
request_state = _MergeRequestScanningState()
|
||||
notes = sorted(self.client.project_mergerequest_notes.list(project_id=merge_request.project_id,
|
||||
merge_request_id=merge_request.id,
|
||||
all=True),
|
||||
key=lambda x: x.id)
|
||||
for note in notes:
|
||||
if not note.system:
|
||||
log.debug("merge request '%s', note '%s' is a normal message", merge_request.title, note.id)
|
||||
# note that we can't ignore note.system = True + merge_request.author, that might have been
|
||||
# a new push or something, so we only ignore normal notes from the author
|
||||
if note.author == merge_request.author:
|
||||
log.debug("skipping note from the merge request author")
|
||||
elif note.author.username == self.client.user.username:
|
||||
log.debug("saw a message from myself, i might have already sent a notice i'm sitting on")
|
||||
if note.body.find(self.REVIEWS_RESET_FORMAT) >= 0 and request_state.unlogged_approval_reset:
|
||||
log.debug("saw a reset message, unsetting the flag")
|
||||
request_state.unlogged_approval_reset = False
|
||||
elif note.body.find(self.REVIEWS_START_FORMAT) >= 0 and request_state.unlogged_review_start:
|
||||
log.debug("saw a start message, unsetting the flag")
|
||||
request_state.unlogged_review_start = False
|
||||
elif note.body.find(self.REVIEWS_REVIEW_REGISTERED_FORMAT) >= 0 and request_state.unlogged_approval:
|
||||
log.debug("saw a review log message, unsetting the flag")
|
||||
request_state.unlogged_approval = False
|
||||
elif note.body.find(self.REVIEWS_REVIEWS_COMPLETE) >= 0 and request_state.unlogged_review_complete:
|
||||
log.debug("saw a review complete message, unsetting the flag")
|
||||
request_state.unlogged_review_complete = False
|
||||
else:
|
||||
log.debug("nothing in particular relevant in '%s'", note.body)
|
||||
else:
|
||||
if note.body.find("LGTM") >= 0:
|
||||
log.debug("merge request '%s', note '%s' has a LGTM", merge_request.title, note.id)
|
||||
request_state.unlogged_approval = True
|
||||
request_state.approver_list.append(note.author.username)
|
||||
log.debug("approvers: %s", request_state.approver_list)
|
||||
if len(request_state.approver_list) < project.code_reviews_necessary:
|
||||
log.debug("not enough code reviews yet, setting needs_reviewer")
|
||||
request_state.needs_reviewer = True
|
||||
request_state.needs_accepter = False
|
||||
else:
|
||||
log.debug("enough code reviews, setting needs_accepter")
|
||||
request_state.needs_accepter = True
|
||||
request_state.needs_reviewer = False
|
||||
request_state.unlogged_review_complete = True
|
||||
else:
|
||||
log.debug("merge request '%s', note '%s' does not have a LGTM", merge_request.title,
|
||||
note.id)
|
||||
else:
|
||||
log.debug("merge request '%s', note '%s' is a system message", merge_request.title, note.id)
|
||||
if re.match(r'Added \d+ commit', note.body):
|
||||
log.debug("resetting approval list, '%s' looks like a push!", note.body)
|
||||
|
||||
# only set the unlogged approval reset flag if there's some kind of progress
|
||||
if len(request_state.approver_list) > 0:
|
||||
request_state.unlogged_approval_reset = True
|
||||
request_state.needs_reviewer = True
|
||||
request_state.approver_list.clear()
|
||||
else:
|
||||
log.debug("leaving the approval list as it is, i don't think '%s' is a push", note.body)
|
||||
|
||||
# do some cleanup
|
||||
excluded_review_candidates = request_state.approver_list + [merge_request.author.username]
|
||||
review_candidates = [x for x in project.code_reviewers.split(',') if x not in excluded_review_candidates]
|
||||
if merge_request.assignee:
|
||||
if request_state.needs_reviewer and merge_request.assignee.username in review_candidates:
|
||||
log.debug("unsetting the needs_reviewer flag, the request is already assigned to one")
|
||||
request_state.needs_reviewer = False
|
||||
elif request_state.needs_reviewer and merge_request.assignee.username == merge_request.author.username:
|
||||
log.info("unsetting the needs_reviewer flag, the request is assigned to the author")
|
||||
log.info("in this case we are assuming that the author has work to do, and will re/unassign")
|
||||
request_state.needs_reviewer = False
|
||||
|
||||
excluded_accept_candidates = [merge_request.author.username]
|
||||
accept_candidates = [x for x in project.code_review_final_merge_assignees.split(',') if x not in excluded_accept_candidates]
|
||||
if merge_request.assignee:
|
||||
if request_state.needs_accepter and merge_request.assignee.username in accept_candidates:
|
||||
log.debug("unsetting the needs_accepter flag, the request is already assigned to one")
|
||||
request_state.needs_accepter = False
|
||||
elif request_state.needs_accepter and merge_request.assignee.username == merge_request.author.username:
|
||||
log.info("unsetting the needs_accepter flag, the request is assigned to the author")
|
||||
log.info("in this case we are assuming that the author has work to do, and will re/unassign")
|
||||
request_state.needs_accepter = False
|
||||
|
||||
log.debug("%s", request_state.__dict__)
|
||||
|
||||
# status message stuff
|
||||
if request_state.unlogged_review_start:
|
||||
log.info("sending message for start of reviews")
|
||||
msg = {'body': self.random_reviews_start_message().format(len(request_state.approver_list),
|
||||
project.code_reviews_necessary)}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
if request_state.unlogged_approval_reset:
|
||||
log.info("sending message for review reset")
|
||||
msg = {'body': self.random_reviews_reset_message().format(len(request_state.approver_list),
|
||||
project.code_reviews_necessary)}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
if request_state.unlogged_approval:
|
||||
log.info("sending message for code review progress")
|
||||
msg = {'body': self.random_review_progress_message().format(len(request_state.approver_list),
|
||||
project.code_reviews_necessary)}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
if request_state.unlogged_review_complete:
|
||||
log.info("sending message for code review complete")
|
||||
msg = {'body': self.random_reviews_complete_message()}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
|
||||
# if there's a reviewer necessary, assign the merge request
|
||||
if len(request_state.approver_list) < project.code_reviews_necessary and request_state.needs_reviewer:
|
||||
log.debug("%s needs a code review", merge_request.title)
|
||||
if merge_request.assignee is not None:
|
||||
log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)
|
||||
|
||||
if merge_request.assignee is None or merge_request.assignee.username not in review_candidates:
|
||||
if len(review_candidates) > 0:
|
||||
new_reviewer = review_candidates[merge_request.iid % len(review_candidates)]
|
||||
log.debug("%s is the new reviewer", new_reviewer)
|
||||
|
||||
# get the user object for the new reviewer
|
||||
new_reviewer_obj = self.client.users.get_by_username(new_reviewer)
|
||||
|
||||
# create note for the update
|
||||
msg = {'body': self.random_new_reviewer_message().format(new_reviewer_obj.name)}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
|
||||
# assign the merge request to the new reviewer
|
||||
self.client.update(merge_request, assignee_id=new_reviewer_obj.id)
|
||||
else:
|
||||
log.warning("no reviewers left to review %s, doing nothing", merge_request.title)
|
||||
else:
|
||||
log.debug("needs_reviewer set but the request is assigned to a reviewer, doing nothing")
|
||||
|
||||
# if there's an accepter necessary, assign the merge request
|
||||
if len(request_state.approver_list) >= project.code_reviews_necessary and request_state.needs_accepter:
|
||||
log.debug("%s needs an accepter", merge_request.title)
|
||||
if merge_request.assignee is not None:
|
||||
log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)
|
||||
|
||||
if merge_request.assignee is None or merge_request.assignee.username not in accept_candidates:
|
||||
if len(accept_candidates) > 0:
|
||||
new_accepter = accept_candidates[merge_request.iid % len(accept_candidates)]
|
||||
log.debug("%s is the new accepter", new_accepter)
|
||||
|
||||
# get the user object for the new accepter
|
||||
new_accepter_obj = self.client.users.get_by_username(new_accepter)
|
||||
|
||||
# create note for the update
|
||||
msg = {'body': self.random_reviews_done_message().format(new_accepter_obj.name)}
|
||||
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
|
||||
merge_request_id=merge_request.id)
|
||||
|
||||
# assign the merge request to the new reviewer
|
||||
self.client.update(merge_request, assignee_id=new_accepter_obj.id)
|
||||
else:
|
||||
log.warning("no accepters left to accept %s, doing nothing", merge_request.title)
|
||||
|
||||
|
||||
class _MergeRequestScanningState(object):
|
||||
|
||||
"""Track the state of a merge request as it is scanned and appropriate action identified."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set default flags/values."""
|
||||
self.approver_list = []
|
||||
|
||||
self.unlogged_review_start = True
|
||||
self.unlogged_approval_reset = False
|
||||
self.unlogged_approval = False
|
||||
self.unlogged_review_complete = False
|
||||
self.needs_reviewer = True
|
||||
self.needs_accepter = False
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Find merge requests that need code reviewers."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from gitlab_bot.lib import GitlabBot
|
||||
from gitlab_bot.models import GitlabProjectConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Find merge requests needing code reviewers/accepters"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
bot = GitlabBot()
|
||||
projects = GitlabProjectConfig.objects.filter(manage_merge_request_code_reviews=True)
|
||||
for project in projects:
|
||||
bot.scan_project_for_reviews(project)
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Run the code review process on a specific merge request."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from gitlab_bot.lib import GitlabBot
|
||||
from gitlab_bot.models import GitlabProjectConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Assign code reviewers/accepters for a specific merge request"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('project_id', type=int)
|
||||
parser.add_argument('merge_request_id', type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
project = GitlabProjectConfig.objects.get(pk=options['project_id'])
|
||||
merge_request_ids = [options['merge_request_id'], ]
|
||||
|
||||
bot = GitlabBot()
|
||||
bot.scan_project_for_reviews(project, merge_request_ids=merge_request_ids)
|
||||
@@ -1,33 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GitlabConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('url', models.URLField()),
|
||||
('token', models.CharField(max_length=64)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GitlabProjectConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('project_id', models.CharField(max_length=64)),
|
||||
('manage_merge_request_code_reviews', models.BooleanField(default=False)),
|
||||
('code_reviews_necessary', models.PositiveSmallIntegerField(default=0)),
|
||||
('code_reviewers', models.TextField(blank=True, default='')),
|
||||
('code_review_final_merge_assignees', models.TextField(blank=True, default='')),
|
||||
('gitlab_config', models.ForeignKey(to='gitlab_bot.GitlabConfig', null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gitlab_bot', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='gitlabprojectconfig',
|
||||
name='project_id',
|
||||
field=models.CharField(unique=True, max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Bot/daemons for doing stuff with GitLab."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GitlabConfig(models.Model):
|
||||
|
||||
"""Maintain bot-wide settings (URL, auth key, etc.)."""
|
||||
|
||||
url = models.URLField()
|
||||
token = models.CharField(max_length=64)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
return "bot @ {0:s}".format(self.url)
|
||||
|
||||
|
||||
class GitlabProjectConfig(models.Model):
|
||||
|
||||
"""Maintain settings for a particular project in GitLab."""
|
||||
|
||||
gitlab_config = models.ForeignKey('GitlabConfig', null=True)
|
||||
project_id = models.CharField(max_length=64, unique=True)
|
||||
|
||||
manage_merge_request_code_reviews = models.BooleanField(default=False)
|
||||
code_reviews_necessary = models.PositiveSmallIntegerField(default=0)
|
||||
code_reviewers = models.TextField(default='', blank=True)
|
||||
code_review_final_merge_assignees = models.TextField(default='', blank=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
return "configuration for {0:s} @ {1:s}".format(self.project_id, self.gitlab_config.url)
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Manage ircbot models and admin actions in the admin interface."""
|
||||
|
||||
import logging
|
||||
import xmlrpc.client
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.shortcuts import render
|
||||
|
||||
from ircbot.forms import PrivmsgForm
|
||||
from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.admin')
|
||||
|
||||
|
||||
admin.site.register(Alias)
|
||||
admin.site.register(BotUser)
|
||||
admin.site.register(IrcChannel)
|
||||
admin.site.register(IrcPlugin)
|
||||
|
||||
|
||||
def send_privmsg(request):
|
||||
"""Send a privmsg over XML-RPC to the IRC bot."""
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PrivmsgForm(request.POST)
|
||||
if form.is_valid():
|
||||
target = form.cleaned_data['target']
|
||||
message = form.cleaned_data['message']
|
||||
|
||||
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT)
|
||||
bot = xmlrpc.client.ServerProxy(bot_url)
|
||||
bot.privmsg(target, message)
|
||||
form = PrivmsgForm()
|
||||
else:
|
||||
form = PrivmsgForm()
|
||||
|
||||
return render(request, 'privmsg.html', {'form': form})
|
||||
|
||||
admin.site.register_view('ircbot/privmsg/', "Ircbot - privmsg", view=send_privmsg, urlname='ircbot_privmsg')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
"""Forms for doing ircbot stuff."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.forms import Form, CharField
|
||||
|
||||
log = logging.getLogger('markov.forms')
|
||||
|
||||
|
||||
class PrivmsgForm(Form):
|
||||
|
||||
"""Accept a privmsg to send to the ircbot."""
|
||||
|
||||
target = CharField()
|
||||
message = CharField()
|
||||
@@ -1,34 +0,0 @@
|
||||
import logging
|
||||
|
||||
from ircbot.lib import Plugin, reply_destination_for_event
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.lib')
|
||||
|
||||
|
||||
class Echo(Plugin):
|
||||
|
||||
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!echo\s+(.*)$',
|
||||
self.handle_echo, -20)
|
||||
|
||||
super(Echo, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_echo)
|
||||
|
||||
super(Echo, self).stop()
|
||||
|
||||
def handle_echo(self, connection, event, match):
|
||||
"""Handle the echo command... by echoing."""
|
||||
|
||||
return self.bot.reply(event, match.group(1))
|
||||
|
||||
|
||||
plugin = Echo
|
||||
@@ -1,66 +0,0 @@
|
||||
import logging
|
||||
|
||||
from ircbot.lib import Plugin, has_permission
|
||||
from ircbot.models import IrcChannel
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.ircplugins.ircmgmt')
|
||||
|
||||
|
||||
class ChannelManagement(Plugin):
|
||||
|
||||
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!join\s+([\S]+)',
|
||||
self.handle_join, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!part\s+([\S]+)',
|
||||
self.handle_part, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!quit\s*(.*)',
|
||||
self.handle_quit, -20)
|
||||
|
||||
super(ChannelManagement, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_join)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_part)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_quit)
|
||||
|
||||
super(ChannelManagement, self).stop()
|
||||
|
||||
def handle_join(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.manage_current_channels'):
|
||||
channel = match.group(1)
|
||||
# put it in the database if it isn't already
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
log.debug("joining channel %s", channel)
|
||||
self.connection.join(channel)
|
||||
|
||||
return self.bot.reply(event, "Joined channel {0:s}.".format(channel))
|
||||
|
||||
def handle_part(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.manage_current_channels'):
|
||||
channel = match.group(1)
|
||||
# put it in the database if it isn't already
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
log.debug("parting channel %s", channel)
|
||||
self.connection.part(channel)
|
||||
|
||||
return self.bot.reply(event, "Parted channel {0:s}.".format(channel))
|
||||
|
||||
def handle_quit(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.quit_bot'):
|
||||
self.bot.die(msg=match.group(1))
|
||||
|
||||
|
||||
plugin = ChannelManagement
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Watch channel topics for changes and note them."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from ircbot.lib import Plugin
|
||||
from ircbot.models import IrcChannel
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.ircplugins.topicmonitor')
|
||||
|
||||
|
||||
class TopicMonitor(Plugin):
|
||||
|
||||
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_handler('topic', handle_topic, -20)
|
||||
|
||||
super(TopicMonitor, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_handler('topic', handle_topic)
|
||||
|
||||
super(TopicMonitor, self).stop()
|
||||
|
||||
|
||||
def handle_topic(connection, event):
|
||||
"""Store topic changes in the channel model."""
|
||||
|
||||
channel = event.target
|
||||
topic = event.arguments[0]
|
||||
setter = event.source
|
||||
log.debug("topic change '%s' by %s in %s", topic, setter, channel)
|
||||
|
||||
channel, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
channel.topic_msg = topic
|
||||
channel.topic_time = timezone.now()
|
||||
channel.topic_by = setter
|
||||
channel.save()
|
||||
|
||||
|
||||
plugin = TopicMonitor
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Library and convenience methods for the IRC bot and plugins."""
|
||||
|
||||
import logging
|
||||
|
||||
import irc.client
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from ircbot.models import BotUser
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.lib')
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
|
||||
"""Plugin base class."""
|
||||
|
||||
def __init__(self, bot, connection, event):
|
||||
"""Initialization stuff here --- global handlers, configs from database, so on."""
|
||||
|
||||
self.bot = bot
|
||||
self.connection = connection
|
||||
self.event = event
|
||||
|
||||
log.info("initialized %s", self.__class__.__name__)
|
||||
|
||||
def start(self):
|
||||
"""Initialization stuff here --- global handlers, configs from database, so on."""
|
||||
|
||||
log.info("started %s", self.__class__.__name__)
|
||||
|
||||
def stop(self):
|
||||
"""Teardown stuff here --- unregister handlers, for example."""
|
||||
|
||||
log.info("stopped %s", self.__class__.__name__)
|
||||
|
||||
def _unencode_xml(self, text):
|
||||
"""Convert <, >, & to their real entities."""
|
||||
|
||||
text = text.replace('<', '<')
|
||||
text = text.replace('>', '>')
|
||||
text = text.replace('&', '&')
|
||||
return text
|
||||
|
||||
|
||||
def has_permission(source, permission):
|
||||
"""Check if the provided event source is a bot admin."""
|
||||
|
||||
try:
|
||||
bot_user = BotUser.objects.get(nickmask=source)
|
||||
log.debug("found bot user %s", bot_user)
|
||||
except BotUser.DoesNotExist:
|
||||
log.debug("could not find bot user for %s", source)
|
||||
return False
|
||||
|
||||
try:
|
||||
django_user = bot_user.user
|
||||
if django_user.has_perm(permission):
|
||||
log.debug("bot user %s has requested permission %s", bot_user, permission)
|
||||
return True
|
||||
except ObjectDoesNotExist:
|
||||
log.error("could not find django user for bot user %s", bot_user)
|
||||
return False
|
||||
|
||||
log.debug("bot user %s does not have requested permission %s", bot_user, permission)
|
||||
return False
|
||||
|
||||
|
||||
def reply_destination_for_event(event):
|
||||
"""Get the "natural" reply destination for an event.
|
||||
|
||||
If the event appears to be from a person within a channel, the channel
|
||||
is the reply destination. Otherwise, the source (assumed to be the speaker
|
||||
in a privmsg)'s nick is the reply destination.
|
||||
"""
|
||||
|
||||
if irc.client.is_channel(event.target):
|
||||
return event.target
|
||||
else:
|
||||
return irc.client.NickMask(event.source).nick
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Start the IRC bot via Django management command."""
|
||||
|
||||
import logging
|
||||
import signal
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from ircbot.bot import IRCBot
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Provide the command to start the IRC bot.
|
||||
|
||||
This will run until the bot disconnects and shuts down.
|
||||
"""
|
||||
|
||||
help = "Start the IRC bot"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Start the IRC bot and spin forever."""
|
||||
|
||||
irc = IRCBot()
|
||||
signal.signal(signal.SIGINT, irc.sigint_handler)
|
||||
irc.start()
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IrcChannel',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=200)),
|
||||
('autojoin', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BotAdmin',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('nickmask', models.CharField(unique=True, max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IrcPlugin',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('path', models.CharField(unique=True, max_length=200)),
|
||||
('autojoin', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0002_botadmin_ircplugin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='ircplugin',
|
||||
old_name='autojoin',
|
||||
new_name='autoload',
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0003_auto_20150512_1934'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Alias',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('pattern', models.CharField(unique=True, max_length=200)),
|
||||
('replacement', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0004_alias'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='alias',
|
||||
options={'verbose_name_plural': 'aliases'},
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0005_auto_20150514_2317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ircchannel',
|
||||
name='topic_by',
|
||||
field=models.CharField(default=b'', max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ircchannel',
|
||||
name='topic_msg',
|
||||
field=models.TextField(default=b''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ircchannel',
|
||||
name='topic_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0006_auto_20150515_2141'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ircplugin',
|
||||
options={'ordering': ['path']},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0007_auto_20150519_2156'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ircchannel',
|
||||
name='topic_by',
|
||||
field=models.CharField(default=b'', max_length=200, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ircchannel',
|
||||
name='topic_msg',
|
||||
field=models.TextField(default=b'', blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ircbot', '0008_auto_20150521_2113'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='botadmin',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0009_botadmin_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='BotAdmin',
|
||||
new_name='BotUser',
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0010_auto_20150620_0930'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='botuser',
|
||||
options={'permissions': (('quit_bot', 'Can tell the bot to quit via IRC'),)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ircchannel',
|
||||
options={'permissions': (('manage_current_channels', 'Can join/part channels via IRC'),)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ircplugin',
|
||||
options={'ordering': ['path'], 'permissions': (('manage_loaded_plugins', 'Can load/unload plugins via IRC'),)},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0011_auto_20150620_0951'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ircchannel',
|
||||
name='markov_learn_from_channel',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.db import models, migrations
|
||||
import ircbot.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0012_ircchannel_markov_learn_from_channel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ircchannel',
|
||||
name='name',
|
||||
field=ircbot.models.LowerCaseCharField(unique=True, max_length=200),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ircbot', '0013_auto_20150917_2255'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ircchannel',
|
||||
name='topic_by',
|
||||
field=models.CharField(default='', blank=True, max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ircchannel',
|
||||
name='topic_msg',
|
||||
field=models.TextField(default='', blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,103 +0,0 @@
|
||||
"""Track basic IRC settings and similar."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.models')
|
||||
|
||||
|
||||
class LowerCaseCharField(models.CharField):
|
||||
def get_prep_value(self, value):
|
||||
value = super(LowerCaseCharField, self).get_prep_value(value)
|
||||
if value is not None:
|
||||
value = value.lower()
|
||||
return value
|
||||
|
||||
|
||||
class Alias(models.Model):
|
||||
|
||||
"""Allow for aliasing of arbitrary regexes to normal supported commands."""
|
||||
|
||||
pattern = models.CharField(max_length=200, unique=True)
|
||||
replacement = models.CharField(max_length=200)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "aliases"
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} -> {1:s}".format(self.pattern, self.replacement)
|
||||
|
||||
def replace(self, what):
|
||||
command = None
|
||||
if re.search(self.pattern, what, flags=re.IGNORECASE):
|
||||
command = re.sub(self.pattern, self.replacement, what, flags=re.IGNORECASE)
|
||||
|
||||
return command
|
||||
|
||||
|
||||
class BotUser(models.Model):
|
||||
|
||||
"""Configure bot users, which can do things through the bot and standard Django auth."""
|
||||
|
||||
nickmask = models.CharField(max_length=200, unique=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('quit_bot', "Can tell the bot to quit via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s} (Django user {1:s})".format(self.nickmask, self.user.username)
|
||||
|
||||
|
||||
class IrcChannel(models.Model):
|
||||
|
||||
"""Track channel settings."""
|
||||
|
||||
name = LowerCaseCharField(max_length=200, unique=True)
|
||||
autojoin = models.BooleanField(default=False)
|
||||
|
||||
topic_msg = models.TextField(default='', blank=True)
|
||||
topic_time = models.DateTimeField(default=timezone.now)
|
||||
topic_by = models.CharField(max_length=200, default='', blank=True)
|
||||
|
||||
markov_learn_from_channel = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('manage_current_channels', "Can join/part channels via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s}".format(self.name)
|
||||
|
||||
|
||||
class IrcPlugin(models.Model):
|
||||
|
||||
"""Represent an IRC plugin and its loading settings."""
|
||||
|
||||
path = models.CharField(max_length=200, unique=True)
|
||||
autoload = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['path']
|
||||
permissions = (
|
||||
('manage_loaded_plugins', "Can load/unload plugins via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
return "{0:s}".format(self.path)
|
||||
@@ -1,15 +0,0 @@
|
||||
{% extends 'adminplus/index.html' %}
|
||||
|
||||
{% block title %}Ircbot - Privmsg{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
<form id="ircbot_privmsg_form" action="{% url 'admin:ircbot_privmsg' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<input class="submit-button" type="submit" value="Privmsg"/>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,9 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,161 +0,0 @@
|
||||
"""Karma hooks for the IRC bot."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import irc.client
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
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+|$)'
|
||||
|
||||
where = event.target
|
||||
if where in settings.KARMA_IGNORE_CHATTER_TARGETS:
|
||||
log.debug("ignoring chatter in {0:s}".format(where))
|
||||
return
|
||||
|
||||
# check the line for karma
|
||||
log.debug("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."""
|
||||
|
||||
where = event.target
|
||||
if where in settings.KARMA_IGNORE_COMMAND_TARGETS:
|
||||
log.debug("ignoring command in {0:s}".format(where))
|
||||
return
|
||||
|
||||
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."""
|
||||
|
||||
where = event.target
|
||||
if where in settings.KARMA_IGNORE_COMMAND_TARGETS:
|
||||
log.debug("ignoring command in {0:s}".format(where))
|
||||
return
|
||||
|
||||
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."""
|
||||
|
||||
where = event.target
|
||||
if where in settings.KARMA_IGNORE_COMMAND_TARGETS:
|
||||
log.debug("ignoring command in {0:s}".format(where))
|
||||
return
|
||||
|
||||
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
|
||||
@@ -1,29 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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'},
|
||||
),
|
||||
]
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Karma logging models."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
from irc.client import NickMask
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
log = logging.getLogger('karma.models')
|
||||
|
||||
|
||||
def perdelta(start, end, delta):
|
||||
curr = start
|
||||
while curr < end:
|
||||
yield curr
|
||||
curr += delta
|
||||
yield end
|
||||
|
||||
|
||||
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 __str__(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
|
||||
|
||||
def history(self, mode='delta'):
|
||||
"""Determine the score for this karma entry at every delta."""
|
||||
|
||||
history = []
|
||||
|
||||
if mode == 'delta':
|
||||
entries = KarmaLogEntry.objects.filter(key=self).order_by('created')
|
||||
for entry in entries:
|
||||
timestamp = entry.created
|
||||
delta = entry.delta
|
||||
score = KarmaLogEntry.objects.filter(key=self).filter(created__lte=entry.created).aggregate(
|
||||
models.Sum('delta'))['delta__sum']
|
||||
|
||||
history.append((timestamp, delta, score))
|
||||
elif mode == 'date':
|
||||
first_entry = KarmaLogEntry.objects.filter(key=self).order_by('created')[0]
|
||||
slice_begin = first_entry.created.date()
|
||||
slice_end = timezone.now().date()
|
||||
for timeslice in perdelta(slice_begin, slice_end, timedelta(days=1)):
|
||||
score = KarmaLogEntry.objects.filter(key=self).filter(
|
||||
created__lte=timeslice+timedelta(days=1)).aggregate(models.Sum('delta'))['delta__sum']
|
||||
if not score:
|
||||
score = 0
|
||||
try:
|
||||
prev_score = history[-1][2]
|
||||
except IndexError:
|
||||
prev_score = 0
|
||||
|
||||
delta = score - prev_score
|
||||
|
||||
history.append((timeslice, delta, score))
|
||||
|
||||
return history
|
||||
|
||||
|
||||
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 __str__(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)
|
||||
@@ -1,12 +0,0 @@
|
||||
"""Serializers for the karma objects."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from karma.models import KarmaKey
|
||||
|
||||
|
||||
class KarmaKeySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = KarmaKey
|
||||
fields = ('id', 'key', 'score', 'rank')
|
||||
@@ -1,44 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_media %}
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/t/bs/jqc-1.12.0,dt-1.10.11/datatables.min.css"/>
|
||||
<script type="text/javascript" src="https://cdn.datatables.net/t/bs/jqc-1.12.0,dt-1.10.11/datatables.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}karma{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="width: 75%; margin-left: auto; margin-right: auto;">
|
||||
<table id="karma" class="display table table-striped table-bordered" cellspacing="0" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td><a href="{% url 'karma_key_detail' entry.key %}">{{ entry.key }}</a></td>
|
||||
<td>{{ entry.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('#karma').DataTable( {
|
||||
"order": [[ 1, "desc" ]],
|
||||
"lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
|
||||
} );
|
||||
} );
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,67 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_media %}
|
||||
<script type="text/javascript" src="{% get_static_prefix %}js/Chart.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}karma: {{ entry.key }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{{ entry.key }}</h3>
|
||||
<canvas id="karma_history"></canvas>
|
||||
<script type="text/javascript">
|
||||
Chart.defaults.global.responsive = true;
|
||||
Chart.defaults.global.maintainAspectRatio = true;
|
||||
Chart.types.Line.extend({name : "AltLine",
|
||||
initialize : function(data) {
|
||||
Chart.types.Line.prototype.initialize.apply(this, arguments);
|
||||
this.scale.draw = function() {
|
||||
if (this.display && (this.xLabelRotation > 90)) {
|
||||
this.endPoint = this.height - 5;
|
||||
}
|
||||
Chart.Scale.prototype.draw.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
});
|
||||
var ctx = $("#karma_history").get(0).getContext("2d");
|
||||
var data = {
|
||||
labels: [{% for x in entry_history %}"{{ x.0 }}", {% endfor %}],
|
||||
datasets: [
|
||||
{
|
||||
label: "{{ entry.key }}",
|
||||
fillColor: "rgba(150,150,220,0.2)",
|
||||
strokeColor: "rgba(100,100,220,1)",
|
||||
pointColor: "rgba(50,50,220,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(220,220,220,1)",
|
||||
data: [{% for x in entry_history %}{{ x.2 }}, {% endfor %}]
|
||||
}
|
||||
]
|
||||
};
|
||||
var myLineChart = new Chart(ctx).AltLine(data, {
|
||||
pointDot: false,
|
||||
{% if entry_history|length > 100 %}
|
||||
showTooltips: false,
|
||||
scaleShowVerticalLines: false,
|
||||
{% endif %}
|
||||
tooltipEvents: ["click"]
|
||||
});
|
||||
</script>
|
||||
<h5>Log</h5>
|
||||
<table border="1px">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Delta</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
{% for delta in entry_history %}
|
||||
<tr>
|
||||
<td>{{ delta.0 }}</td>
|
||||
<td>{{ delta.1 }}</td>
|
||||
<td>{{ delta.2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -1,16 +0,0 @@
|
||||
"""URL patterns for the karma views."""
|
||||
|
||||
from django.conf.urls import patterns, url, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from karma.views import key_detail, index, KarmaKeyViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'keys', KarmaKeyViewSet)
|
||||
|
||||
urlpatterns = patterns('races.views',
|
||||
url(r'^$', index, name='karma_index'),
|
||||
url(r'^key/(?P<karma_key>.+)/', key_detail, name='karma_key_detail'),
|
||||
|
||||
url(r'^api/', include(router.urls)),
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
"""Present karma data."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from rest_framework import viewsets
|
||||
|
||||
from karma.models import KarmaKey
|
||||
from karma.serializers import KarmaKeySerializer
|
||||
|
||||
log = logging.getLogger('karma.views')
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Display all karma keys."""
|
||||
|
||||
entries = KarmaKey.objects.all().order_by('key')
|
||||
|
||||
return render(request, 'karma/index.html', {'entries': entries})
|
||||
|
||||
|
||||
def key_detail(request, karma_key):
|
||||
"""Display the requested karma key."""
|
||||
|
||||
entry = get_object_or_404(KarmaKey, key=karma_key.lower())
|
||||
|
||||
return render(request, 'karma/karma_key.html', {'entry': entry, 'entry_history': entry.history(mode='date')})
|
||||
|
||||
|
||||
class KarmaKeyViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
"""Provide list and detail actions for karma keys."""
|
||||
|
||||
queryset = KarmaKey.objects.all()
|
||||
serializer_class = KarmaKeySerializer
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dr_botzo.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
@@ -1,112 +0,0 @@
|
||||
"""Manage Markov models and administrative commands."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.shortcuts import render
|
||||
|
||||
from markov.forms import LogUploadForm, TeachLineForm
|
||||
import markov.lib as markovlib
|
||||
from markov.models import MarkovContext, MarkovTarget, MarkovState
|
||||
|
||||
|
||||
log = logging.getLogger('markov.admin')
|
||||
|
||||
|
||||
admin.site.register(MarkovContext)
|
||||
admin.site.register(MarkovTarget)
|
||||
admin.site.register(MarkovState)
|
||||
|
||||
|
||||
@permission_required('import_text_file', raise_exception=True)
|
||||
def import_file(request):
|
||||
"""Accept a file upload and turn it into markov stuff.
|
||||
|
||||
Current file formats supported:
|
||||
* weechat
|
||||
"""
|
||||
|
||||
if request.method == 'POST':
|
||||
form = LogUploadForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
if form.cleaned_data['text_file_format'] == LogUploadForm.FILE_FORMAT_WEECHAT:
|
||||
text_file = request.FILES['text_file']
|
||||
context = form.cleaned_data['context']
|
||||
ignores = form.cleaned_data['ignore_nicks'].split(',')
|
||||
strips = form.cleaned_data['strip_prefixes'].split(' ')
|
||||
|
||||
whos = []
|
||||
for line in text_file:
|
||||
log.debug(line)
|
||||
(timestamp, who, what) = line.decode('utf-8').split('\t', 2)
|
||||
|
||||
if who in ('-->', '<--', '--', ' *'):
|
||||
continue
|
||||
|
||||
if who in ignores:
|
||||
continue
|
||||
|
||||
whos.append(who)
|
||||
|
||||
# this is a line we probably care about now
|
||||
what = [x for x in what.rstrip().split(' ') if x not in strips]
|
||||
markovlib.learn_line(' '.join(what), context)
|
||||
log.debug("learned")
|
||||
|
||||
log.debug(set(whos))
|
||||
form = LogUploadForm()
|
||||
elif form.cleaned_data['text_file_format'] == LogUploadForm.FILE_FORMAT_RAW_TEXT:
|
||||
text_file = request.FILES['text_file']
|
||||
context = form.cleaned_data['context']
|
||||
|
||||
k1 = MarkovState._start1
|
||||
k2 = MarkovState._start2
|
||||
for line in text_file:
|
||||
for word in [x for x in line.decode('utf-8') .rstrip().split(' ')]:
|
||||
log.info(word)
|
||||
if word:
|
||||
state, created = MarkovState.objects.get_or_create(context=context, k1=k1,
|
||||
k2=k2, v=word)
|
||||
state.count += 1
|
||||
state.save()
|
||||
|
||||
if word[-1] in ['.', '?', '!']:
|
||||
state, created = MarkovState.objects.get_or_create(context=context, k1=k2,
|
||||
k2=word, v=MarkovState._stop)
|
||||
state.count += 1
|
||||
state.save()
|
||||
|
||||
k1 = MarkovState._start1
|
||||
k2 = MarkovState._start2
|
||||
else:
|
||||
k1 = k2
|
||||
k2 = word
|
||||
else:
|
||||
form = LogUploadForm()
|
||||
|
||||
return render(request, 'markov/import_file.html', {'form': form})
|
||||
|
||||
|
||||
@permission_required('teach_line', raise_exception=True)
|
||||
def teach_line(request):
|
||||
"""Teach one line directly."""
|
||||
|
||||
if request.method == 'POST':
|
||||
form = TeachLineForm(request.POST)
|
||||
if form.is_valid():
|
||||
line = form.cleaned_data['line']
|
||||
context = form.cleaned_data['context']
|
||||
strips = form.cleaned_data['strip_prefixes'].split(' ')
|
||||
what = [x for x in line.rstrip().split(' ') if x not in strips]
|
||||
markovlib.learn_line(' '.join(what), context)
|
||||
form = TeachLineForm()
|
||||
else:
|
||||
form = TeachLineForm()
|
||||
|
||||
return render(request, 'markov/teach_line.html', {'form': form})
|
||||
|
||||
|
||||
admin.site.register_view('markov/importfile/', "Markov - Import log file", view=import_file,
|
||||
urlname='markov_import_file')
|
||||
admin.site.register_view('markov/teach/', "Markov - Teach line", view=teach_line, urlname='markov_teach_line')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user