collapsing all of dr_botzo one directory

This commit is contained in:
2017-02-04 11:48:55 -06:00
parent 38d14bb0d2
commit cd23f062a9
194 changed files with 0 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +0,0 @@
"""Manage choices models."""
from django.contrib import admin
from choices.models import ChoiceSet
admin.site.register(ChoiceSet)

View File

@@ -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()),
],
),
]

View File

@@ -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(',')

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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'),
)

View File

@@ -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})

View File

@@ -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)

View File

@@ -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

View File

@@ -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)),
],
),
]

View File

@@ -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',
),
]

View File

@@ -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'))

View File

@@ -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

View File

@@ -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)

View File

@@ -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)),
],
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'),
)

View File

@@ -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

View File

@@ -1,7 +0,0 @@
from django.contrib import admin
from facts.models import Fact, FactCategory
admin.site.register(Fact)
admin.site.register(FactCategory)

View File

@@ -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

View File

@@ -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'),
),
]

View File

@@ -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'},
),
]

View File

@@ -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),
),
]

View File

@@ -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,
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)),
],
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 &lt;, &gt;, &amp; to their real entities."""
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&amp;', '&')
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

View File

@@ -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()

View File

@@ -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)),
],
),
]

View File

@@ -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)),
],
),
]

View File

@@ -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',
),
]

View File

@@ -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)),
],
),
]

View File

@@ -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'},
),
]

View File

@@ -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),
),
]

View File

@@ -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']},
),
]

View File

@@ -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),
),
]

View File

@@ -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,
),
]

View File

@@ -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',
),
]

View File

@@ -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'),)},
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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)

View File

@@ -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

View File

@@ -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')),
],
),
]

View File

@@ -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'},
),
]

View File

@@ -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)

View File

@@ -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')

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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)),
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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