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.""" """Track dispatcher configurations."""
import logging import logging
from django.db import models from django.db import models
log = logging.getLogger('dispatch.models') log = logging.getLogger('dispatch.models')
@ -13,6 +11,9 @@ class Dispatcher(models.Model):
key = models.CharField(max_length=16, unique=True) 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: class Meta:
"""Meta options.""" """Meta options."""
@ -21,7 +22,7 @@ class Dispatcher(models.Model):
) )
def __str__(self): def __str__(self):
"""String representation.""" """Provide string representation."""
return "{0:s}".format(self.key) return "{0:s}".format(self.key)
@ -42,5 +43,5 @@ class DispatcherAction(models.Model):
include_key = models.BooleanField(default=False) include_key = models.BooleanField(default=False)
def __str__(self): def __str__(self):
"""String representation.""" """Provide string representation."""
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination) 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.""" """Handle dispatcher API requests."""
import copy import copy
import logging import logging
import os import os
import xmlrpc.client import xmlrpc.client
from django.conf import settings
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from dispatch.models import Dispatcher, DispatcherAction 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') log = logging.getLogger('dispatch.views')
@ -77,7 +74,7 @@ class DispatchMessage(generics.GenericAPIView):
if action.type == DispatcherAction.PRIVMSG_TYPE: if action.type == DispatcherAction.PRIVMSG_TYPE:
# connect over XML-RPC and send # connect over XML-RPC and send
try: 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) bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
log.debug("sending '%s' to channel %s", text, action.destination) log.debug("sending '%s' to channel %s", text, action.destination)
bot.reply(None, text, False, action.destination) bot.reply(None, text, False, action.destination)

View File

@ -154,32 +154,6 @@ ACCOUNT_ACTIVATION_DAYS = 7
REGISTRATION_AUTO_LOGIN = True 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 # IRC module stuff
# karma # karma

View File

@ -3,23 +3,15 @@
import logging import logging
import xmlrpc.client import xmlrpc.client
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.shortcuts import render from django.shortcuts import render
from ircbot.forms import PrivmsgForm 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') 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): def send_privmsg(request):
"""Send a privmsg over XML-RPC to the IRC bot.""" """Send a privmsg over XML-RPC to the IRC bot."""
if request.method == 'POST': if request.method == 'POST':
@ -28,7 +20,8 @@ def send_privmsg(request):
target = form.cleaned_data['target'] target = form.cleaned_data['target']
message = form.cleaned_data['message'] 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 = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
bot.reply(None, message, False, target) bot.reply(None, message, False, target)
form = PrivmsgForm() form = PrivmsgForm()
@ -37,4 +30,11 @@ def send_privmsg(request):
return render(request, 'privmsg.html', {'form': form}) 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') 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 irc.modes
import ircbot.lib as ircbotlib 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') log = logging.getLogger('ircbot.bot')
@ -54,6 +54,8 @@ class LenientServerConnection(irc.client.ServerConnection):
buffer_class = irc.buffer.LenientDecodingLineBuffer buffer_class = irc.buffer.LenientDecodingLineBuffer
server_config = None
def _prep_message(self, string): def _prep_message(self, string):
"""Override SimpleIRCClient._prep_message to add some logging.""" """Override SimpleIRCClient._prep_message to add some logging."""
log.debug("preparing message %s", string) log.debug("preparing message %s", string)
@ -163,7 +165,8 @@ class DrReactor(irc.client.Reactor):
event.original_msg = what event.original_msg = what
# check if we were addressed or not # 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) addressed_pattern = r'^(({nicks})[:,]|@({nicks}))\s+(?P<addressed_msg>.*)'.format(nicks=all_nicks)
match = re.match(addressed_pattern, what, re.IGNORECASE) match = re.match(addressed_pattern, what, re.IGNORECASE)
if match: if match:
@ -351,15 +354,16 @@ class IRCBot(irc.client.SimpleIRCClient):
reactor_class = DrReactor reactor_class = DrReactor
splitter = "..." splitter = "..."
def __init__(self, reconnection_interval=60): def __init__(self, server_name, reconnection_interval=60):
"""Initialize bot.""" """Initialize bot."""
super(IRCBot, self).__init__() super(IRCBot, self).__init__()
self.channels = IRCDict() self.channels = IRCDict()
self.plugins = [] self.plugins = []
# set up the server list self.server_config = IrcServer.objects.get(name=server_name)
self.server_list = settings.IRCBOT_SERVER_LIST # 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 # set reconnection interval
if not reconnection_interval or reconnection_interval < 0: if not reconnection_interval or reconnection_interval < 0:
@ -367,8 +371,8 @@ class IRCBot(irc.client.SimpleIRCClient):
self.reconnection_interval = reconnection_interval self.reconnection_interval = reconnection_interval
# set basic stuff # set basic stuff
self._nickname = settings.IRCBOT_NICKNAME self._nickname = self.server_config.nickname
self._realname = settings.IRCBOT_REALNAME self._realname = self.server_config.realname
# guess at nickmask. hopefully _on_welcome() will set this, but this should be # guess at nickmask. hopefully _on_welcome() will set this, but this should be
# a pretty good guess if not # a pretty good guess if not
@ -395,7 +399,7 @@ class IRCBot(irc.client.SimpleIRCClient):
getattr(self, 'handle_reload'), -20) getattr(self, 'handle_reload'), -20)
# load XML-RPC server # 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) requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions() self.xmlrpc.register_introspection_functions()
@ -414,16 +418,15 @@ class IRCBot(irc.client.SimpleIRCClient):
self.jump_server() self.jump_server()
def _connect(self): def _connect(self):
server = self.server_list[0]
try: try:
# build the connection factory as determined by IPV6/SSL settings # build the connection factory as determined by IPV6/SSL settings
if settings.IRCBOT_SSL: if self.server_config.use_ssl:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=settings.IRCBOT_IPV6) connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=self.server_config.use_ipv6)
else: 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, self.connect(self.server_config.hostname, self.server_config.port, self._nickname,
connect_factory=connect_factory) self.server_config.password, ircname=self._realname, connect_factory=connect_factory)
except irc.client.ServerConnectionError: except irc.client.ServerConnectionError:
pass pass
@ -528,13 +531,13 @@ class IRCBot(irc.client.SimpleIRCClient):
log.debug("welcome: %s", what) log.debug("welcome: %s", what)
# run automsg commands # 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 # TODO NOTE: if the bot is sending something that changes the vhost
# (like 'hostserv on') we don't pick it up # (like 'hostserv on') we don't pick it up
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:])) self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:]))
# sleep before doing autojoins # 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): for chan in IrcChannel.objects.filter(autojoin=True):
log.info("autojoining %s", chan.name) log.info("autojoining %s", chan.name)
@ -584,7 +587,6 @@ class IRCBot(irc.client.SimpleIRCClient):
if self.connection.is_connected(): if self.connection.is_connected():
self.connection.disconnect(msg) self.connection.disconnect(msg)
self.server_list.append(self.server_list.pop(0))
self._connect() self._connect()
def on_ctcp(self, c, e): def on_ctcp(self, c, e):

View File

@ -1,10 +1,9 @@
"""Forms for doing ircbot stuff.""" """Forms for doing ircbot stuff."""
import logging 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): class PrivmsgForm(Form):
@ -12,3 +11,5 @@ class PrivmsgForm(Form):
target = CharField() target = CharField()
message = CharField(widget=Textarea) message = CharField(widget=Textarea)
xmlrpc_host = CharField()
xmlrpc_port = IntegerField()

View File

@ -17,8 +17,13 @@ class Command(BaseCommand):
help = "Start the IRC bot" 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): def handle(self, *args, **options):
"""Start the IRC bot and spin forever.""" """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) signal.signal(signal.SIGINT, irc.sigint_handler)
irc.start() 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) 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): class IrcChannel(models.Model):
"""Track channel settings.""" """Track channel settings."""

View File

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