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)