move IRC server settings to database

this is the first step in trying to get the bot to support multiple
servers with different channels, countdown triggers, and so on

this also ends up affecting some configuration around:
* dispatch
* markov
* admin privmsg form
This commit is contained in:
Brian S. Stephan 2021-04-25 08:59:01 -05:00
parent 44d8b7db00
commit 6136127c5f
12 changed files with 158 additions and 73 deletions

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2021-04-25 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dispatch', '0005_auto_20160116_1955'),
]
operations = [
migrations.AddField(
model_name='dispatcher',
name='bot_xmlrpc_host',
field=models.CharField(default='localhost', max_length=200),
),
migrations.AddField(
model_name='dispatcher',
name='bot_xmlrpc_port',
field=models.PositiveSmallIntegerField(default=13132),
),
]

View File

@ -1,10 +1,8 @@
"""Track dispatcher configurations."""
import logging
from django.db import models
log = logging.getLogger('dispatch.models')
@ -13,6 +11,9 @@ class Dispatcher(models.Model):
key = models.CharField(max_length=16, unique=True)
bot_xmlrpc_host = models.CharField(max_length=200, default='localhost')
bot_xmlrpc_port = models.PositiveSmallIntegerField(default=13132)
class Meta:
"""Meta options."""
@ -21,7 +22,7 @@ class Dispatcher(models.Model):
)
def __str__(self):
"""String representation."""
"""Provide string representation."""
return "{0:s}".format(self.key)
@ -42,5 +43,5 @@ class DispatcherAction(models.Model):
include_key = models.BooleanField(default=False)
def __str__(self):
"""String representation."""
"""Provide string representation."""
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination)

View File

@ -1,18 +1,15 @@
"""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
from dispatch.serializers import DispatcherActionSerializer, DispatcherSerializer, DispatchMessageSerializer
log = logging.getLogger('dispatch.views')
@ -77,7 +74,7 @@ class DispatchMessage(generics.GenericAPIView):
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_url = 'http://{0:s}:{1:d}/'.format(dispatcher.bot_xmlrpc_host, dispatcher.bot_xmlrpc_port)
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
log.debug("sending '%s' to channel %s", text, action.destination)
bot.reply(None, text, False, action.destination)

View File

@ -154,32 +154,6 @@ ACCOUNT_ACTIVATION_DAYS = 7
REGISTRATION_AUTO_LOGIN = True
# IRC bot stuff
# tuple of hostname, port number, and password (or None)
IRCBOT_SERVER_LIST = [
('localhost', 6667, None),
]
IRCBOT_NICKNAME = 'dr_botzo'
IRCBOT_REALNAME = 'Dr. Botzo'
IRCBOT_SSL = False
IRCBOT_IPV6 = False
# post-connect, pre-autojoin stuff
IRCBOT_SLEEP_BEFORE_AUTOJOIN_SECONDS = 10
IRCBOT_POST_CONNECT_COMMANDS = [ ]
# XML-RPC settings
IRCBOT_XMLRPC_HOST = 'localhost'
IRCBOT_XMLRPC_PORT = 13132
# nick hack for discord through bitlbee
ADDITIONAL_NICK_MATCHES = []
# IRC module stuff
# karma

View File

@ -3,23 +3,15 @@
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
from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin, IrcServer
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':
@ -28,7 +20,8 @@ def send_privmsg(request):
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_url = 'http://{0:s}:{1:d}/'.format(form.cleaned_data['xmlrpc_host'],
form.cleaned_data['xmlrpc_port'])
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
bot.reply(None, message, False, target)
form = PrivmsgForm()
@ -37,4 +30,11 @@ def send_privmsg(request):
return render(request, 'privmsg.html', {'form': form})
admin.site.register(Alias)
admin.site.register(BotUser)
admin.site.register(IrcChannel)
admin.site.register(IrcPlugin)
admin.site.register(IrcServer)
admin.site.register_view('ircbot/privmsg/', "Ircbot - privmsg", view=send_privmsg, urlname='ircbot_privmsg')

View File

@ -22,7 +22,7 @@ from irc.dict import IRCDict
import irc.modes
import ircbot.lib as ircbotlib
from ircbot.models import Alias, IrcChannel, IrcPlugin
from ircbot.models import Alias, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.bot')
@ -54,6 +54,8 @@ class LenientServerConnection(irc.client.ServerConnection):
buffer_class = irc.buffer.LenientDecodingLineBuffer
server_config = None
def _prep_message(self, string):
"""Override SimpleIRCClient._prep_message to add some logging."""
log.debug("preparing message %s", string)
@ -163,7 +165,8 @@ class DrReactor(irc.client.Reactor):
event.original_msg = what
# check if we were addressed or not
all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()])
all_nicks = '|'.join(connection.server_config.additional_addressed_nicks.split('\n') +
[connection.get_nickname()])
addressed_pattern = r'^(({nicks})[:,]|@({nicks}))\s+(?P<addressed_msg>.*)'.format(nicks=all_nicks)
match = re.match(addressed_pattern, what, re.IGNORECASE)
if match:
@ -351,15 +354,16 @@ class IRCBot(irc.client.SimpleIRCClient):
reactor_class = DrReactor
splitter = "..."
def __init__(self, reconnection_interval=60):
def __init__(self, server_name, reconnection_interval=60):
"""Initialize bot."""
super(IRCBot, self).__init__()
self.channels = IRCDict()
self.plugins = []
# set up the server list
self.server_list = settings.IRCBOT_SERVER_LIST
self.server_config = IrcServer.objects.get(name=server_name)
# the reactor made the connection, save the server reference in it since we pass that around
self.connection.server_config = self.server_config
# set reconnection interval
if not reconnection_interval or reconnection_interval < 0:
@ -367,8 +371,8 @@ class IRCBot(irc.client.SimpleIRCClient):
self.reconnection_interval = reconnection_interval
# set basic stuff
self._nickname = settings.IRCBOT_NICKNAME
self._realname = settings.IRCBOT_REALNAME
self._nickname = self.server_config.nickname
self._realname = self.server_config.realname
# guess at nickmask. hopefully _on_welcome() will set this, but this should be
# a pretty good guess if not
@ -395,7 +399,7 @@ class IRCBot(irc.client.SimpleIRCClient):
getattr(self, 'handle_reload'), -20)
# load XML-RPC server
self.xmlrpc = SimpleXMLRPCServer((settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT),
self.xmlrpc = SimpleXMLRPCServer((self.server_config.xmlrpc_host, self.server_config.xmlrpc_port),
requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions()
@ -414,16 +418,15 @@ class IRCBot(irc.client.SimpleIRCClient):
self.jump_server()
def _connect(self):
server = self.server_list[0]
try:
# build the connection factory as determined by IPV6/SSL settings
if settings.IRCBOT_SSL:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=settings.IRCBOT_IPV6)
if self.server_config.use_ssl:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=self.server_config.use_ipv6)
else:
connect_factory = Factory(ipv6=settings.IRCBOT_IPV6)
connect_factory = Factory(ipv6=self.server_config.use_ipv6)
self.connect(server[0], server[1], self._nickname, server[2], ircname=self._realname,
connect_factory=connect_factory)
self.connect(self.server_config.hostname, self.server_config.port, self._nickname,
self.server_config.password, ircname=self._realname, connect_factory=connect_factory)
except irc.client.ServerConnectionError:
pass
@ -528,13 +531,13 @@ class IRCBot(irc.client.SimpleIRCClient):
log.debug("welcome: %s", what)
# run automsg commands
for cmd in settings.IRCBOT_POST_CONNECT_COMMANDS:
for cmd in self.server_config.post_connect.split('\n'):
# TODO NOTE: if the bot is sending something that changes the vhost
# (like 'hostserv on') we don't pick it up
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:]))
# sleep before doing autojoins
time.sleep(settings.IRCBOT_SLEEP_BEFORE_AUTOJOIN_SECONDS)
time.sleep(self.server_config.delay_before_joins)
for chan in IrcChannel.objects.filter(autojoin=True):
log.info("autojoining %s", chan.name)
@ -584,7 +587,6 @@ class IRCBot(irc.client.SimpleIRCClient):
if self.connection.is_connected():
self.connection.disconnect(msg)
self.server_list.append(self.server_list.pop(0))
self._connect()
def on_ctcp(self, c, e):

View File

@ -1,10 +1,9 @@
"""Forms for doing ircbot stuff."""
import logging
from django.forms import Form, CharField, Textarea
from django.forms import CharField, Form, IntegerField, Textarea
log = logging.getLogger('markov.forms')
log = logging.getLogger('ircbot.forms')
class PrivmsgForm(Form):
@ -12,3 +11,5 @@ class PrivmsgForm(Form):
target = CharField()
message = CharField(widget=Textarea)
xmlrpc_host = CharField()
xmlrpc_port = IntegerField()

View File

@ -17,8 +17,13 @@ class Command(BaseCommand):
help = "Start the IRC bot"
def add_arguments(self, parser):
"""Add arguments to the bot startup."""
parser.add_argument('server_name')
def handle(self, *args, **options):
"""Start the IRC bot and spin forever."""
irc = IRCBot()
self.stdout.write(self.style.NOTICE(f"Starting up {options['server_name']} bot"))
irc = IRCBot(options['server_name'])
signal.signal(signal.SIGINT, irc.sigint_handler)
irc.start()

View File

@ -0,0 +1,32 @@
# Generated by Django 3.1.2 on 2021-04-25 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0014_auto_20160116_1955'),
]
operations = [
migrations.CreateModel(
name='IrcServer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, unique=True)),
('hostname', models.CharField(max_length=200)),
('port', models.PositiveSmallIntegerField(default=6667)),
('password', models.CharField(blank=True, default=None, max_length=200, null=True)),
('nickname', models.CharField(max_length=32)),
('realname', models.CharField(blank=True, default='', max_length=32)),
('additional_addressed_nicks', models.TextField(blank=True, default='', help_text='For e.g. BitlBee alternative nicks')),
('use_ssl', models.BooleanField(default=False)),
('use_ipv6', models.BooleanField(default=False)),
('post_connect', models.TextField(blank=True, default='')),
('delay_before_joins', models.PositiveSmallIntegerField(default=0)),
('xmlrpc_host', models.CharField(default='localhost', max_length=200)),
('xmlrpc_port', models.PositiveSmallIntegerField(default=13132)),
],
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.2 on 2021-04-25 04:11
from django.db import migrations
def create_placeholder_server(apps, schema_editor):
"""Create the first server entry, to be configured by the admin."""
IrcServer = apps.get_model('ircbot', 'IrcServer')
IrcServer.objects.create(name='default', hostname='irc.example.org', port=6667)
def delete_placeholder_server(apps, schema_editor):
"""Remove the default server."""
IrcServer = apps.get_model('ircbot', 'IrcServer')
IrcServer.objects.filter(name='default').delete()
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0015_ircserver'),
]
operations = [
migrations.RunPython(create_placeholder_server, delete_placeholder_server),
]

View File

@ -62,6 +62,33 @@ class BotUser(models.Model):
return "{0:s} (Django user {1:s})".format(self.nickmask, self.user.username)
class IrcServer(models.Model):
"""Contain server information in an object, and help contextualize channels."""
name = models.CharField(max_length=200, unique=True)
hostname = models.CharField(max_length=200)
port = models.PositiveSmallIntegerField(default=6667)
password = models.CharField(max_length=200, default=None, null=True, blank=True)
nickname = models.CharField(max_length=32)
realname = models.CharField(max_length=32, default='', blank=True)
additional_addressed_nicks = models.TextField(default='', blank=True,
help_text="For e.g. BitlBee alternative nicks")
use_ssl = models.BooleanField(default=False)
use_ipv6 = models.BooleanField(default=False)
post_connect = models.TextField(default='', blank=True)
delay_before_joins = models.PositiveSmallIntegerField(default=0)
xmlrpc_host = models.CharField(max_length=200, default='localhost')
xmlrpc_port = models.PositiveSmallIntegerField(default=13132)
def __str__(self):
"""Provide string summary of the server."""
return f"{self.name} ({self.hostname}/{self.port})"
class IrcChannel(models.Model):
"""Track channel settings."""

View File

@ -1,23 +1,22 @@
"""IRC support for Markov chain learning and text generation."""
import logging
import re
import irc.client
from django.conf import settings
import markov.lib as markovlib
from ircbot.lib import Plugin, reply_destination_for_event
from ircbot.models import IrcChannel
import markov.lib as markovlib
log = logging.getLogger('markov.ircplugin')
class Markov(Plugin):
"""Build Markov chains and reply with them."""
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)
@ -29,7 +28,6 @@ class Markov(Plugin):
def stop(self):
"""Tear down handlers."""
self.connection.remove_global_handler('pubmsg', self.handle_chatter)
self.connection.remove_global_handler('privmsg', self.handle_chatter)
@ -39,7 +37,6 @@ class Markov(Plugin):
def handle_reply(self, connection, event, match):
"""Generate a reply to one line, without learning it."""
target = reply_destination_for_event(event)
min_size = 15
@ -63,9 +60,9 @@ class Markov(Plugin):
def handle_chatter(self, connection, event):
"""Learn from IRC chatter."""
what = event.arguments[0]
all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()])
all_nicks = '|'.join(connection.server_config.additional_addressed_nicks.split('\n') +
[connection.get_nickname()])
trimmed_what = re.sub(r'^(({nicks})[:,]|@({nicks}))\s+'.format(nicks=all_nicks), '', what)
nick = irc.client.NickMask(event.source).nick
target = reply_destination_for_event(event)