Compare commits

..

No commits in common. "d7b7bdf73d329d805db076cf379de61898811138" and "cbbf6eb311fd40559b02299093bd9da3371f0e03" have entirely different histories.

24 changed files with 127 additions and 386 deletions

View File

@ -12,7 +12,6 @@ from django.utils import timezone
from countdown.models import CountdownItem from countdown.models import CountdownItem
from ircbot.lib import Plugin, most_specific_message from ircbot.lib import Plugin, most_specific_message
from ircbot.models import IrcChannel
log = logging.getLogger('countdown.ircplugin') log = logging.getLogger('countdown.ircplugin')
@ -23,14 +22,13 @@ class Countdown(Plugin):
new_reminder_regex = (r'remind\s+(?P<who>[^\s]+)\s+(?P<when_type>at|in|on)\s+(?P<when>.*?)\s+' new_reminder_regex = (r'remind\s+(?P<who>[^\s]+)\s+(?P<when_type>at|in|on)\s+(?P<when>.*?)\s+'
r'(and\s+every\s+(?P<recurring_period>.*?)\s+)?' r'(and\s+every\s+(?P<recurring_period>.*?)\s+)?'
r'(until\s+(?P<recurring_until>.*?)\s+)?' r'(until\s+(?P<recurring_until>.*?)\s+)?'
r'(to|that|about|with)\s+(?P<text>.*?)' r'(to|that|about)\s+(?P<text>.*?)'
r'(?=\s+\("(?P<name>.*)"\)|$)') r'(?=\s+\("(?P<name>.*)"\)|$)')
def __init__(self, bot, connection, event): def __init__(self, bot, connection, event):
"""Initialize some stuff.""" """Initialize some stuff."""
self.running_reminders = [] self.running_reminders = []
self.send_reminders = True self.send_reminders = True
self.server_config = connection.server_config
t = threading.Thread(target=self.reminder_thread) t = threading.Thread(target=self.reminder_thread)
t.daemon = True t.daemon = True
@ -68,15 +66,13 @@ class Countdown(Plugin):
# TODO: figure out if we need this sleep, which exists so we don't send reminders while still connecting to IRC # TODO: figure out if we need this sleep, which exists so we don't send reminders while still connecting to IRC
time.sleep(30) time.sleep(30)
while self.send_reminders: while self.send_reminders:
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False, reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False, at_time__lte=timezone.now())
at_time__lte=timezone.now(),
reminder_target_new__server=self.server_config)
for reminder in reminders: for reminder in reminders:
log.debug("%s @ %s", reminder.reminder_message, reminder.at_time) log.debug("%s @ %s", reminder.reminder_message, reminder.at_time)
if reminder.at_time <= timezone.now(): if reminder.at_time <= timezone.now():
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target_new.name) log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target)
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target_new.name) self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target)
# if recurring and not hit until, set a new at time, otherwise stop reminding # if recurring and not hit until, set a new at time, otherwise stop reminding
if reminder.recurring_until is not None and timezone.now() >= reminder.recurring_until: if reminder.recurring_until is not None and timezone.now() >= reminder.recurring_until:
@ -147,12 +143,8 @@ class Countdown(Plugin):
log.debug("%s / %s / %s", item_name, when_t, message) log.debug("%s / %s / %s", item_name, when_t, message)
# get the IrcChannel to send to
reminder_target, _ = IrcChannel.objects.get_or_create(name=event.sent_location,
server=connection.server_config)
countdown_item = CountdownItem.objects.create(name=item_name, at_time=when_t, is_reminder=True, countdown_item = CountdownItem.objects.create(name=item_name, at_time=when_t, is_reminder=True,
reminder_message=message, reminder_target_new=reminder_target) reminder_message=message, reminder_target=event.sent_location)
if recurring_period: if recurring_period:
countdown_item.recurring_period = recurring_period countdown_item.recurring_period = recurring_period
if recurring_until: if recurring_until:

View File

@ -1,20 +0,0 @@
# Generated by Django 3.1.2 on 2021-04-26 00:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0018_ircserver_replace_irc_control_with_markdown'),
('countdown', '0006_auto_20201025_1716'),
]
operations = [
migrations.AddField(
model_name='countdownitem',
name='reminder_target_new',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='ircbot.ircchannel'),
),
]

View File

@ -14,8 +14,6 @@ class CountdownItem(models.Model):
reminder_message = models.TextField(default="") reminder_message = models.TextField(default="")
reminder_target = models.CharField(max_length=64, blank=True, default='') reminder_target = models.CharField(max_length=64, blank=True, default='')
reminder_target_new = models.ForeignKey('ircbot.IrcChannel', null=True, blank=True,
default=None, on_delete=models.CASCADE)
recurring_period = models.CharField(max_length=64, blank=True, default='') recurring_period = models.CharField(max_length=64, blank=True, default='')
recurring_until = models.DateTimeField(null=True, blank=True, default=None) recurring_until = models.DateTimeField(null=True, blank=True, default=None)

View File

@ -1,23 +0,0 @@
# 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,8 +1,10 @@
"""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')
@ -11,9 +13,6 @@ 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."""
@ -22,7 +21,7 @@ class Dispatcher(models.Model):
) )
def __str__(self): def __str__(self):
"""Provide string representation.""" """String representation."""
return "{0:s}".format(self.key) return "{0:s}".format(self.key)
@ -43,5 +42,5 @@ class DispatcherAction(models.Model):
include_key = models.BooleanField(default=False) include_key = models.BooleanField(default=False)
def __str__(self): def __str__(self):
"""Provide string representation.""" """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,15 +1,18 @@
"""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 DispatcherActionSerializer, DispatcherSerializer, DispatchMessageSerializer from dispatch.serializers import DispatchMessageSerializer, DispatcherSerializer, DispatcherActionSerializer
log = logging.getLogger('dispatch.views') log = logging.getLogger('dispatch.views')
@ -74,7 +77,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(dispatcher.bot_xmlrpc_host, dispatcher.bot_xmlrpc_port) bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_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,6 +154,32 @@ 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

@ -1,14 +0,0 @@
"""Serializers for the fact objects."""
from rest_framework import serializers
from facts.models import Fact
class FactSerializer(serializers.ModelSerializer):
"""Serializer for the REST API."""
class Meta:
"""Meta options."""
model = Fact
fields = ('id', 'fact', 'category')

View File

@ -1,13 +1,9 @@
"""URL patterns for the facts web views.""" """URL patterns for the facts web views."""
from django.conf.urls import url from django.conf.urls import url
from django.urls import path
from facts.views import factcategory_detail, index, rpc_get_facts, rpc_get_random_fact from facts.views import index, factcategory_detail
urlpatterns = [ urlpatterns = [
path('rpc/<category>/', rpc_get_facts, name='weather_rpc_get_facts'),
path('rpc/<category>/random/', rpc_get_random_fact, name='weather_rpc_get_random_fact'),
url(r'^$', index, name='facts_index'), url(r'^$', index, name='facts_index'),
url(r'^(?P<factcategory_name>.+)/$', factcategory_detail, name='facts_factcategory_detail'), url(r'^(?P<factcategory_name>.+)/$', factcategory_detail, name='facts_factcategory_detail'),
] ]

View File

@ -2,49 +2,12 @@
import logging import logging
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from rest_framework.authentication import BasicAuthentication
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from facts.models import FactCategory from facts.models import FactCategory
from facts.serializers import FactSerializer
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@api_view(['GET'])
@authentication_classes((BasicAuthentication, ))
@permission_classes((IsAuthenticated, ))
def rpc_get_facts(request, category):
"""Get all the facts in a category."""
if request.method != 'GET':
return Response({'detail': "Supported method: GET."}, status=405)
try:
fact_category = FactCategory.objects.get(name=category)
except FactCategory.DoesNotExist:
return Response({'detail': f"Item set category '{category}' not found."}, status=404)
return Response(FactSerializer(fact_category.fact_set.all(), many=True).data)
@api_view(['GET'])
@authentication_classes((BasicAuthentication, ))
@permission_classes((IsAuthenticated, ))
def rpc_get_random_fact(request, category):
"""Get all the facts in a category."""
if request.method != 'GET':
return Response({'detail': "Supported method: GET."}, status=405)
try:
fact_category = FactCategory.objects.get(name=category)
except FactCategory.DoesNotExist:
return Response({'detail': f"Item set category '{category}' not found."}, status=404)
return Response(FactSerializer(fact_category.random_fact()).data)
def index(request): def index(request):
"""Display a simple list of the fact categories, for the moment.""" """Display a simple list of the fact categories, for the moment."""
factcategories = FactCategory.objects.all() factcategories = FactCategory.objects.all()

View File

@ -3,15 +3,23 @@
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, IrcServer from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin
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':
@ -20,8 +28,7 @@ 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(form.cleaned_data['xmlrpc_host'], bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT)
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()
@ -30,11 +37,4 @@ 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, IrcServer from ircbot.models import Alias, IrcChannel, IrcPlugin
log = logging.getLogger('ircbot.bot') log = logging.getLogger('ircbot.bot')
@ -54,8 +54,6 @@ 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)
@ -165,19 +163,13 @@ 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
if connection.server_config.additional_addressed_nicks: all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()])
all_nicks = '|'.join(connection.server_config.additional_addressed_nicks.split('\n') +
[connection.get_nickname()])
else:
all_nicks = 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:
event.addressed = True event.addressed = True
event.addressed_msg = match.group('addressed_msg') event.addressed_msg = match.group('addressed_msg')
log.debug("all_nicks: %s, addressed: %s", all_nicks, event.addressed)
# only do aliasing for pubmsg/privmsg # only do aliasing for pubmsg/privmsg
log.debug("checking for alias for %s", what) log.debug("checking for alias for %s", what)
@ -359,16 +351,15 @@ class IRCBot(irc.client.SimpleIRCClient):
reactor_class = DrReactor reactor_class = DrReactor
splitter = "..." splitter = "..."
def __init__(self, server_name, reconnection_interval=60): def __init__(self, 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 = []
self.server_config = IrcServer.objects.get(name=server_name) # set up the server list
# the reactor made the connection, save the server reference in it since we pass that around self.server_list = settings.IRCBOT_SERVER_LIST
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:
@ -376,8 +367,8 @@ class IRCBot(irc.client.SimpleIRCClient):
self.reconnection_interval = reconnection_interval self.reconnection_interval = reconnection_interval
# set basic stuff # set basic stuff
self._nickname = self.server_config.nickname self._nickname = settings.IRCBOT_NICKNAME
self._realname = self.server_config.realname self._realname = settings.IRCBOT_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
@ -404,7 +395,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((self.server_config.xmlrpc_host, self.server_config.xmlrpc_port), self.xmlrpc = SimpleXMLRPCServer((settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT),
requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True) requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions() self.xmlrpc.register_introspection_functions()
@ -423,15 +414,16 @@ 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 self.server_config.use_ssl: if settings.IRCBOT_SSL:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=self.server_config.use_ipv6) connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=settings.IRCBOT_IPV6)
else: else:
connect_factory = Factory(ipv6=self.server_config.use_ipv6) connect_factory = Factory(ipv6=settings.IRCBOT_IPV6)
self.connect(self.server_config.hostname, self.server_config.port, self._nickname, self.connect(server[0], server[1], self._nickname, server[2], ircname=self._realname,
self.server_config.password, ircname=self._realname, connect_factory=connect_factory) connect_factory=connect_factory)
except irc.client.ServerConnectionError: except irc.client.ServerConnectionError:
pass pass
@ -536,16 +528,15 @@ class IRCBot(irc.client.SimpleIRCClient):
log.debug("welcome: %s", what) log.debug("welcome: %s", what)
# run automsg commands # run automsg commands
if self.server_config.post_connect: 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(self.server_config.delay_before_joins) time.sleep(settings.IRCBOT_SLEEP_BEFORE_AUTOJOIN_SECONDS)
for chan in IrcChannel.objects.filter(autojoin=True, server=connection.server_config): for chan in IrcChannel.objects.filter(autojoin=True):
log.info("autojoining %s", chan.name) log.info("autojoining %s", chan.name)
self.connection.join(chan) self.connection.join(chan)
@ -593,6 +584,7 @@ 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):
@ -897,12 +889,6 @@ class IRCBot(irc.client.SimpleIRCClient):
log.warning("reply() called with no event and no explicit target, aborting") log.warning("reply() called with no event and no explicit target, aborting")
return return
# convert characters that don't make sense for Discord (like ^C^B)
if self.connection.server_config.replace_irc_control_with_markdown:
log.debug("old replystr: %s", replystr)
replystr = replystr.replace('\x02', '**')
log.debug("new replystr: %s", replystr)
log.debug("replypath: %s", replypath) log.debug("replypath: %s", replypath)
if replystr is not None: if replystr is not None:

View File

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

View File

@ -1,17 +1,19 @@
"""Provide some commands for basic IRC functionality."""
import logging import logging
from ircbot.lib import Plugin, has_permission from ircbot.lib import Plugin, has_permission
from ircbot.models import IrcChannel from ircbot.models import IrcChannel
log = logging.getLogger('ircbot.ircplugins.ircmgmt') log = logging.getLogger('ircbot.ircplugins.ircmgmt')
class ChannelManagement(Plugin): class ChannelManagement(Plugin):
"""Have IRC commands to do IRC things (join channels, quit, etc.).""" """Have IRC commands to do IRC things (join channels, quit, etc.)."""
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!join\s+([\S]+)', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!join\s+([\S]+)',
self.handle_join, -20) self.handle_join, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!part\s+([\S]+)', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!part\s+([\S]+)',
@ -23,6 +25,7 @@ class ChannelManagement(Plugin):
def stop(self): def stop(self):
"""Tear down handlers.""" """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_join)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_part) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_part)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_quit) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_quit)
@ -31,10 +34,11 @@ class ChannelManagement(Plugin):
def handle_join(self, connection, event, match): def handle_join(self, connection, event, match):
"""Handle the join command.""" """Handle the join command."""
if has_permission(event.source, 'ircbot.manage_current_channels'): if has_permission(event.source, 'ircbot.manage_current_channels'):
channel = match.group(1) channel = match.group(1)
# put it in the database if it isn't already # put it in the database if it isn't already
chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config) chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
log.debug("joining channel %s", channel) log.debug("joining channel %s", channel)
self.connection.join(channel) self.connection.join(channel)
@ -42,10 +46,11 @@ class ChannelManagement(Plugin):
def handle_part(self, connection, event, match): def handle_part(self, connection, event, match):
"""Handle the join command.""" """Handle the join command."""
if has_permission(event.source, 'ircbot.manage_current_channels'): if has_permission(event.source, 'ircbot.manage_current_channels'):
channel = match.group(1) channel = match.group(1)
# put it in the database if it isn't already # put it in the database if it isn't already
chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config) chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
log.debug("parting channel %s", channel) log.debug("parting channel %s", channel)
self.connection.part(channel) self.connection.part(channel)
@ -53,6 +58,7 @@ class ChannelManagement(Plugin):
def handle_quit(self, connection, event, match): def handle_quit(self, connection, event, match):
"""Handle the join command.""" """Handle the join command."""
if has_permission(event.source, 'ircbot.quit_bot'): if has_permission(event.source, 'ircbot.quit_bot'):
self.bot.die(msg=match.group(1)) self.bot.die(msg=match.group(1))

View File

@ -1,4 +1,5 @@
"""Watch channel topics for changes and note them.""" """Watch channel topics for changes and note them."""
import logging import logging
from django.utils import timezone from django.utils import timezone
@ -6,20 +7,24 @@ from django.utils import timezone
from ircbot.lib import Plugin from ircbot.lib import Plugin
from ircbot.models import IrcChannel from ircbot.models import IrcChannel
log = logging.getLogger('ircbot.ircplugins.topicmonitor') log = logging.getLogger('ircbot.ircplugins.topicmonitor')
class TopicMonitor(Plugin): class TopicMonitor(Plugin):
"""Have IRC commands to do IRC things (join channels, quit, etc.).""" """Have IRC commands to do IRC things (join channels, quit, etc.)."""
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.connection.reactor.add_global_handler('topic', handle_topic, -20) self.connection.reactor.add_global_handler('topic', handle_topic, -20)
super(TopicMonitor, self).start() super(TopicMonitor, self).start()
def stop(self): def stop(self):
"""Tear down handlers.""" """Tear down handlers."""
self.connection.reactor.remove_global_handler('topic', handle_topic) self.connection.reactor.remove_global_handler('topic', handle_topic)
super(TopicMonitor, self).stop() super(TopicMonitor, self).stop()
@ -27,12 +32,13 @@ class TopicMonitor(Plugin):
def handle_topic(connection, event): def handle_topic(connection, event):
"""Store topic changes in the channel model.""" """Store topic changes in the channel model."""
channel = event.target channel = event.target
topic = event.arguments[0] topic = event.arguments[0]
setter = event.source setter = event.source
log.debug("topic change '%s' by %s in %s", topic, setter, channel) log.debug("topic change '%s' by %s in %s", topic, setter, channel)
channel, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config) channel, c = IrcChannel.objects.get_or_create(name=channel)
channel.topic_msg = topic channel.topic_msg = topic
channel.topic_time = timezone.now() channel.topic_time = timezone.now()
channel.topic_by = setter channel.topic_by = setter

View File

@ -1,4 +1,5 @@
"""Start the IRC bot via Django management command.""" """Start the IRC bot via Django management command."""
import logging import logging
import signal import signal
@ -6,6 +7,7 @@ from django.core.management import BaseCommand
from ircbot.bot import IRCBot from ircbot.bot import IRCBot
log = logging.getLogger('ircbot') log = logging.getLogger('ircbot')
@ -17,13 +19,9 @@ 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."""
self.stdout.write(self.style.NOTICE(f"Starting up {options['server_name']} bot"))
irc = IRCBot(options['server_name']) irc = IRCBot()
signal.signal(signal.SIGINT, irc.sigint_handler) signal.signal(signal.SIGINT, irc.sigint_handler)
irc.start() irc.start()

View File

@ -1,32 +0,0 @@
# 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

@ -1,26 +0,0 @@
# 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

@ -1,30 +0,0 @@
# Generated by Django 3.1.2 on 2021-04-25 16:11
from django.db import migrations, models
import django.db.models.deletion
import ircbot.models
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0016_placeholder_ircserver'),
]
operations = [
migrations.AddField(
model_name='ircchannel',
name='server',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='ircbot.ircserver'),
preserve_default=False,
),
migrations.AlterField(
model_name='ircchannel',
name='name',
field=ircbot.models.LowerCaseCharField(max_length=200),
),
migrations.AddConstraint(
model_name='ircchannel',
constraint=models.UniqueConstraint(fields=('name', 'server'), name='unique_server_channel'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.2 on 2021-04-25 17:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0017_ircchannel_server'),
]
operations = [
migrations.AddField(
model_name='ircserver',
name='replace_irc_control_with_markdown',
field=models.BooleanField(default=False),
),
]

View File

@ -1,4 +1,5 @@
"""Track basic IRC settings and similar.""" """Track basic IRC settings and similar."""
import logging import logging
import re import re
@ -6,14 +7,12 @@ from django.conf import settings
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
log = logging.getLogger('ircbot.models') log = logging.getLogger('ircbot.models')
class LowerCaseCharField(models.CharField): class LowerCaseCharField(models.CharField):
"""Provide a case-insensitive, forced-lower CharField."""
def get_prep_value(self, value): def get_prep_value(self, value):
"""Manipulate the field value to make it lowercase."""
value = super(LowerCaseCharField, self).get_prep_value(value) value = super(LowerCaseCharField, self).get_prep_value(value)
if value is not None: if value is not None:
value = value.lower() value = value.lower()
@ -21,22 +20,21 @@ class LowerCaseCharField(models.CharField):
class Alias(models.Model): class Alias(models.Model):
"""Allow for aliasing of arbitrary regexes to normal supported commands.""" """Allow for aliasing of arbitrary regexes to normal supported commands."""
pattern = models.CharField(max_length=200, unique=True) pattern = models.CharField(max_length=200, unique=True)
replacement = models.CharField(max_length=200) replacement = models.CharField(max_length=200)
class Meta: class Meta:
"""Settings for the model."""
verbose_name_plural = "aliases" verbose_name_plural = "aliases"
def __str__(self): def __str__(self):
"""Provide string representation.""" """String representation."""
return "{0:s} -> {1:s}".format(self.pattern, self.replacement) return "{0:s} -> {1:s}".format(self.pattern, self.replacement)
def replace(self, what): def replace(self, what):
"""Match the regex and replace with the command."""
command = None command = None
if re.search(self.pattern, what, flags=re.IGNORECASE): if re.search(self.pattern, what, flags=re.IGNORECASE):
command = re.sub(self.pattern, self.replacement, what, flags=re.IGNORECASE) command = re.sub(self.pattern, self.replacement, what, flags=re.IGNORECASE)
@ -45,57 +43,28 @@ class Alias(models.Model):
class BotUser(models.Model): class BotUser(models.Model):
"""Configure bot users, which can do things through the bot and standard Django auth.""" """Configure bot users, which can do things through the bot and standard Django auth."""
nickmask = models.CharField(max_length=200, unique=True) nickmask = models.CharField(max_length=200, unique=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta: class Meta:
"""Settings for the model."""
permissions = ( permissions = (
('quit_bot', "Can tell the bot to quit via IRC"), ('quit_bot', "Can tell the bot to quit via IRC"),
) )
def __str__(self): def __str__(self):
"""Provide string representation.""" """String representation."""
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)
replace_irc_control_with_markdown = models.BooleanField(default=False)
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."""
name = LowerCaseCharField(max_length=200) name = LowerCaseCharField(max_length=200, unique=True)
server = models.ForeignKey('IrcServer', on_delete=models.CASCADE)
autojoin = models.BooleanField(default=False) autojoin = models.BooleanField(default=False)
topic_msg = models.TextField(default='', blank=True) topic_msg = models.TextField(default='', blank=True)
@ -105,34 +74,30 @@ class IrcChannel(models.Model):
markov_learn_from_channel = models.BooleanField(default=True) markov_learn_from_channel = models.BooleanField(default=True)
class Meta: class Meta:
"""Settings for the model."""
constraints = (
models.UniqueConstraint(fields=['name', 'server'], name='unique_server_channel'),
)
permissions = ( permissions = (
('manage_current_channels', "Can join/part channels via IRC"), ('manage_current_channels', "Can join/part channels via IRC"),
) )
def __str__(self): def __str__(self):
"""Provide string representation.""" """String representation."""
return "{0:s}".format(self.name) return "{0:s}".format(self.name)
class IrcPlugin(models.Model): class IrcPlugin(models.Model):
"""Represent an IRC plugin and its loading settings.""" """Represent an IRC plugin and its loading settings."""
path = models.CharField(max_length=200, unique=True) path = models.CharField(max_length=200, unique=True)
autoload = models.BooleanField(default=False) autoload = models.BooleanField(default=False)
class Meta: class Meta:
"""Settings for the model."""
ordering = ['path'] ordering = ['path']
permissions = ( permissions = (
('manage_loaded_plugins', "Can load/unload plugins via IRC"), ('manage_loaded_plugins', "Can load/unload plugins via IRC"),
) )
def __str__(self): def __str__(self):
"""Provide string representation.""" """String representation."""
return "{0:s}".format(self.path) return "{0:s}".format(self.path)

View File

@ -1,22 +1,23 @@
"""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)
@ -28,6 +29,7 @@ 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)
@ -37,6 +39,7 @@ 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
@ -60,12 +63,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]
if connection.server_config.additional_addressed_nicks: all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()])
all_nicks = '|'.join(connection.server_config.additional_addressed_nicks.split('\n') +
[connection.get_nickname()])
else:
all_nicks = 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)
@ -73,7 +73,7 @@ class Markov(Plugin):
# check to see whether or not we should learn from this channel # check to see whether or not we should learn from this channel
channel = None channel = None
if irc.client.is_channel(target): if irc.client.is_channel(target):
channel, c = IrcChannel.objects.get_or_create(name=target, server=connection.server_config) channel, c = IrcChannel.objects.get_or_create(name=target)
if channel and not channel.markov_learn_from_channel: if channel and not channel.markov_learn_from_channel:
log.debug("not learning from %s as i've been told to ignore it", channel) log.debug("not learning from %s as i've been told to ignore it", channel)

View File

@ -31,8 +31,6 @@ class Twitter(Plugin):
self.poll_mentions = False self.poll_mentions = False
self.server = connection.server_config
super(Twitter, self).__init__(bot, connection, event) super(Twitter, self).__init__(bot, connection, event)
def start(self): def start(self):
@ -274,10 +272,6 @@ class Twitter(Plugin):
out_chan = twittersettings.mentions_output_channel.name out_chan = twittersettings.mentions_output_channel.name
since_id = twittersettings.mentions_since_id since_id = twittersettings.mentions_since_id
if out_chan.server != self.server:
self.poll_mentions = False
return
mentions = self.twit.get_mentions_timeline(since_id=since_id) mentions = self.twit.get_mentions_timeline(since_id=since_id)
mentions.reverse() mentions.reverse()
for mention in mentions: for mention in mentions:

View File

@ -1,27 +1,26 @@
# coding: utf-8 # coding: utf-8
"""Get results of weather queries.""" """Get results of weather queries."""
import logging import logging
from urllib.parse import quote
import requests import requests
from urllib.parse import quote
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def query_wttr_in(query): def query_wttr_in(query):
"""Hit the wttr.in JSON API with the provided query.""" """Hit the wttr.in JSON API with the provided query."""
logger.info("about to query wttr.in with '%s'", query) logger.info(f"about to query wttr.in with '{query}'")
response = requests.get(f'http://wttr.in/{quote(query)}?format=j1') response = requests.get(f'http://wttr.in/{quote(query)}?format=j1')
response.raise_for_status() response.raise_for_status()
weather_info = response.json() weather_info = response.json()
logger.debug("results: %s", weather_info) logger.debug(f"results: {weather_info}")
return weather_info return weather_info
def weather_summary(query): def weather_summary(query):
"""Create a more consumable version of the weather report.""" """Create a more consumable version of the weather report."""
logger.info("assembling weather summary for '%s'", query) logger.info(f"assembling weather summary for '{query}'")
weather_info = query_wttr_in(query) weather_info = query_wttr_in(query)
# get some common/nested stuff once now # get some common/nested stuff once now
@ -31,46 +30,20 @@ def weather_summary(query):
tomorrow_forecast = weather_info['weather'][1] tomorrow_forecast = weather_info['weather'][1]
day_after_tomorrow_forecast = weather_info['weather'][2] day_after_tomorrow_forecast = weather_info['weather'][2]
today_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value']} today_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value'] }
for item in today_forecast['hourly']] for item in today_forecast['hourly']]
today_noteworthy = sorted(today_notes, key=lambda i: i['code'], reverse=True)[0]['desc'] today_noteworthy = sorted(today_notes, key=lambda i: i['code'], reverse=True)[0]['desc']
tomorrow_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value']} tomorrow_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value'] }
for item in tomorrow_forecast['hourly']] for item in tomorrow_forecast['hourly']]
tomorrow_noteworthy = sorted(tomorrow_notes, key=lambda i: i['code'], reverse=True)[0]['desc'] tomorrow_noteworthy = sorted(tomorrow_notes, key=lambda i: i['code'], reverse=True)[0]['desc']
day_after_tomorrow_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value']} day_after_tomorrow_notes = [{'code': int(item['weatherCode']), 'desc': item['weatherDesc'][0]['value'] }
for item in day_after_tomorrow_forecast['hourly']] for item in day_after_tomorrow_forecast['hourly']]
day_after_tomorrow_noteworthy = sorted(day_after_tomorrow_notes, key=lambda i: i['code'], reverse=True)[0]['desc'] day_after_tomorrow_noteworthy = sorted(day_after_tomorrow_notes, key=lambda i: i['code'], reverse=True)[0]['desc']
location_str = None
locations = []
# try to get a smarter location
nearest_areas = weather_info.get('nearest_area', None)
if nearest_areas:
nearest_area = nearest_areas[0]
area_name = nearest_area.get('areaName')
if area_name:
locations.append(area_name[0]['value'])
region = nearest_area.get('region')
if region:
locations.append(region[0]['value'])
country = nearest_area.get('country')
if country:
locations.append(country[0]['value'])
if locations:
location_str = ', '.join(locations)
else:
latitude = nearest_area.get('latitude')
longitude = nearest_area.get('longitude')
if latitude and longitude:
location_str = f'{latitude},{longitude}'
if not location_str:
location_str = query
summary = { summary = {
'location': location_str, 'location': query,
'current': { 'current': {
'description': weather_desc.get('value'), 'description': weather_desc.get('value'),
'temp_C': f"{current.get('temp_C')}°C", 'temp_C': f"{current.get('temp_C')}°C",
@ -109,5 +82,5 @@ def weather_summary(query):
}, },
} }
logger.debug("results: %s", summary) logger.debug(f"results: {summary}")
return summary return summary