From 6136127c5fe222237e4e63675a0de8ac413283a6 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Apr 2021 08:59:01 -0500 Subject: [PATCH] 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 --- dispatch/migrations/0006_xmlrpc_settings.py | 23 ++++++++++++ dispatch/models.py | 9 ++--- dispatch/views.py | 7 ++-- dr_botzo/settings.py | 26 -------------- ircbot/admin.py | 20 +++++------ ircbot/bot.py | 36 ++++++++++--------- ircbot/forms.py | 7 ++-- ircbot/management/commands/runircbot.py | 7 +++- ircbot/migrations/0015_ircserver.py | 32 +++++++++++++++++ .../migrations/0016_placeholder_ircserver.py | 26 ++++++++++++++ ircbot/models.py | 27 ++++++++++++++ markov/ircplugin.py | 11 +++--- 12 files changed, 158 insertions(+), 73 deletions(-) create mode 100644 dispatch/migrations/0006_xmlrpc_settings.py create mode 100644 ircbot/migrations/0015_ircserver.py create mode 100644 ircbot/migrations/0016_placeholder_ircserver.py diff --git a/dispatch/migrations/0006_xmlrpc_settings.py b/dispatch/migrations/0006_xmlrpc_settings.py new file mode 100644 index 0000000..5e09c34 --- /dev/null +++ b/dispatch/migrations/0006_xmlrpc_settings.py @@ -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), + ), + ] diff --git a/dispatch/models.py b/dispatch/models.py index c879b91..a8f97b1 100644 --- a/dispatch/models.py +++ b/dispatch/models.py @@ -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) diff --git a/dispatch/views.py b/dispatch/views.py index 21f534f..4c07476 100644 --- a/dispatch/views.py +++ b/dispatch/views.py @@ -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) diff --git a/dr_botzo/settings.py b/dr_botzo/settings.py index b4e2fa4..63856df 100644 --- a/dr_botzo/settings.py +++ b/dr_botzo/settings.py @@ -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 diff --git a/ircbot/admin.py b/ircbot/admin.py index 88d305e..08dab20 100644 --- a/ircbot/admin.py +++ b/ircbot/admin.py @@ -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') diff --git a/ircbot/bot.py b/ircbot/bot.py index 23b6365..024abb1 100644 --- a/ircbot/bot.py +++ b/ircbot/bot.py @@ -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.*)'.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): diff --git a/ircbot/forms.py b/ircbot/forms.py index 80d3f37..9f287ab 100644 --- a/ircbot/forms.py +++ b/ircbot/forms.py @@ -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() diff --git a/ircbot/management/commands/runircbot.py b/ircbot/management/commands/runircbot.py index f528dc0..ddadcea 100644 --- a/ircbot/management/commands/runircbot.py +++ b/ircbot/management/commands/runircbot.py @@ -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() diff --git a/ircbot/migrations/0015_ircserver.py b/ircbot/migrations/0015_ircserver.py new file mode 100644 index 0000000..52dd67d --- /dev/null +++ b/ircbot/migrations/0015_ircserver.py @@ -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)), + ], + ), + ] diff --git a/ircbot/migrations/0016_placeholder_ircserver.py b/ircbot/migrations/0016_placeholder_ircserver.py new file mode 100644 index 0000000..0c87592 --- /dev/null +++ b/ircbot/migrations/0016_placeholder_ircserver.py @@ -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), + ] diff --git a/ircbot/models.py b/ircbot/models.py index 9b01e1b..3769702 100644 --- a/ircbot/models.py +++ b/ircbot/models.py @@ -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.""" diff --git a/markov/ircplugin.py b/markov/ircplugin.py index c4318e1..92387b0 100644 --- a/markov/ircplugin.py +++ b/markov/ircplugin.py @@ -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)