Compare commits
11 Commits
cbbf6eb311
...
d7b7bdf73d
Author | SHA1 | Date |
---|---|---|
Brian S. Stephan | d7b7bdf73d | |
Brian S. Stephan | a03c69258f | |
Brian S. Stephan | 43f2b09057 | |
Brian S. Stephan | 3aa3fb14e4 | |
Brian S. Stephan | 1fc8af09f8 | |
Brian S. Stephan | 53c874dc21 | |
Brian S. Stephan | 1036c08147 | |
Brian S. Stephan | 9c1109107b | |
Brian S. Stephan | 6136127c5f | |
Brian S. Stephan | 44d8b7db00 | |
Brian S. Stephan | d518cb2b77 |
|
@ -12,6 +12,7 @@ from django.utils import timezone
|
|||
|
||||
from countdown.models import CountdownItem
|
||||
from ircbot.lib import Plugin, most_specific_message
|
||||
from ircbot.models import IrcChannel
|
||||
|
||||
log = logging.getLogger('countdown.ircplugin')
|
||||
|
||||
|
@ -22,13 +23,14 @@ class Countdown(Plugin):
|
|||
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'(until\s+(?P<recurring_until>.*?)\s+)?'
|
||||
r'(to|that|about)\s+(?P<text>.*?)'
|
||||
r'(to|that|about|with)\s+(?P<text>.*?)'
|
||||
r'(?=\s+\("(?P<name>.*)"\)|$)')
|
||||
|
||||
def __init__(self, bot, connection, event):
|
||||
"""Initialize some stuff."""
|
||||
self.running_reminders = []
|
||||
self.send_reminders = True
|
||||
self.server_config = connection.server_config
|
||||
|
||||
t = threading.Thread(target=self.reminder_thread)
|
||||
t.daemon = True
|
||||
|
@ -66,13 +68,15 @@ class Countdown(Plugin):
|
|||
# TODO: figure out if we need this sleep, which exists so we don't send reminders while still connecting to IRC
|
||||
time.sleep(30)
|
||||
while self.send_reminders:
|
||||
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False, at_time__lte=timezone.now())
|
||||
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False,
|
||||
at_time__lte=timezone.now(),
|
||||
reminder_target_new__server=self.server_config)
|
||||
|
||||
for reminder in reminders:
|
||||
log.debug("%s @ %s", reminder.reminder_message, reminder.at_time)
|
||||
if reminder.at_time <= timezone.now():
|
||||
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target)
|
||||
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target)
|
||||
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target_new.name)
|
||||
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target_new.name)
|
||||
|
||||
# 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:
|
||||
|
@ -143,8 +147,12 @@ class Countdown(Plugin):
|
|||
|
||||
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,
|
||||
reminder_message=message, reminder_target=event.sent_location)
|
||||
reminder_message=message, reminder_target_new=reminder_target)
|
||||
if recurring_period:
|
||||
countdown_item.recurring_period = recurring_period
|
||||
if recurring_until:
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
|
@ -14,6 +14,8 @@ class CountdownItem(models.Model):
|
|||
|
||||
reminder_message = models.TextField(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_until = models.DateTimeField(null=True, blank=True, default=None)
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
"""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')
|
|
@ -1,9 +1,13 @@
|
|||
"""URL patterns for the facts web views."""
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from facts.views import index, factcategory_detail
|
||||
from facts.views import factcategory_detail, index, rpc_get_facts, rpc_get_random_fact
|
||||
|
||||
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'^(?P<factcategory_name>.+)/$', factcategory_detail, name='facts_factcategory_detail'),
|
||||
]
|
||||
|
|
|
@ -2,12 +2,49 @@
|
|||
import logging
|
||||
|
||||
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.serializers import FactSerializer
|
||||
|
||||
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):
|
||||
"""Display a simple list of the fact categories, for the moment."""
|
||||
factcategories = FactCategory.objects.all()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,13 +165,19 @@ 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()])
|
||||
if connection.server_config.additional_addressed_nicks:
|
||||
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)
|
||||
match = re.match(addressed_pattern, what, re.IGNORECASE)
|
||||
if match:
|
||||
event.addressed = True
|
||||
event.addressed_msg = match.group('addressed_msg')
|
||||
|
||||
log.debug("all_nicks: %s, addressed: %s", all_nicks, event.addressed)
|
||||
|
||||
# only do aliasing for pubmsg/privmsg
|
||||
log.debug("checking for alias for %s", what)
|
||||
|
||||
|
@ -351,15 +359,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 +376,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 +404,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 +423,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,15 +536,16 @@ class IRCBot(irc.client.SimpleIRCClient):
|
|||
log.debug("welcome: %s", what)
|
||||
|
||||
# run automsg commands
|
||||
for cmd in settings.IRCBOT_POST_CONNECT_COMMANDS:
|
||||
if self.server_config.post_connect:
|
||||
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):
|
||||
for chan in IrcChannel.objects.filter(autojoin=True, server=connection.server_config):
|
||||
log.info("autojoining %s", chan.name)
|
||||
self.connection.join(chan)
|
||||
|
||||
|
@ -584,7 +593,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):
|
||||
|
@ -889,6 +897,12 @@ class IRCBot(irc.client.SimpleIRCClient):
|
|||
log.warning("reply() called with no event and no explicit target, aborting")
|
||||
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)
|
||||
|
||||
if replystr is not None:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
"""Provide some commands for basic IRC functionality."""
|
||||
import logging
|
||||
|
||||
from ircbot.lib import Plugin, has_permission
|
||||
from ircbot.models import IrcChannel
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.ircplugins.ircmgmt')
|
||||
|
||||
|
||||
class ChannelManagement(Plugin):
|
||||
|
||||
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!join\s+([\S]+)',
|
||||
self.handle_join, -20)
|
||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!part\s+([\S]+)',
|
||||
|
@ -25,7 +23,6 @@ class ChannelManagement(Plugin):
|
|||
|
||||
def stop(self):
|
||||
"""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_part)
|
||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_quit)
|
||||
|
@ -34,11 +31,10 @@ class ChannelManagement(Plugin):
|
|||
|
||||
def handle_join(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.manage_current_channels'):
|
||||
channel = match.group(1)
|
||||
# put it in the database if it isn't already
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
|
||||
log.debug("joining channel %s", channel)
|
||||
self.connection.join(channel)
|
||||
|
||||
|
@ -46,11 +42,10 @@ class ChannelManagement(Plugin):
|
|||
|
||||
def handle_part(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.manage_current_channels'):
|
||||
channel = match.group(1)
|
||||
# put it in the database if it isn't already
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
|
||||
log.debug("parting channel %s", channel)
|
||||
self.connection.part(channel)
|
||||
|
||||
|
@ -58,7 +53,6 @@ class ChannelManagement(Plugin):
|
|||
|
||||
def handle_quit(self, connection, event, match):
|
||||
"""Handle the join command."""
|
||||
|
||||
if has_permission(event.source, 'ircbot.quit_bot'):
|
||||
self.bot.die(msg=match.group(1))
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""Watch channel topics for changes and note them."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
@ -7,24 +6,20 @@ from django.utils import timezone
|
|||
from ircbot.lib import Plugin
|
||||
from ircbot.models import IrcChannel
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.ircplugins.topicmonitor')
|
||||
|
||||
|
||||
class TopicMonitor(Plugin):
|
||||
|
||||
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
|
||||
|
||||
def start(self):
|
||||
"""Set up the handlers."""
|
||||
|
||||
self.connection.reactor.add_global_handler('topic', handle_topic, -20)
|
||||
|
||||
super(TopicMonitor, self).start()
|
||||
|
||||
def stop(self):
|
||||
"""Tear down handlers."""
|
||||
|
||||
self.connection.reactor.remove_global_handler('topic', handle_topic)
|
||||
|
||||
super(TopicMonitor, self).stop()
|
||||
|
@ -32,13 +27,12 @@ class TopicMonitor(Plugin):
|
|||
|
||||
def handle_topic(connection, event):
|
||||
"""Store topic changes in the channel model."""
|
||||
|
||||
channel = event.target
|
||||
topic = event.arguments[0]
|
||||
setter = event.source
|
||||
log.debug("topic change '%s' by %s in %s", topic, setter, channel)
|
||||
|
||||
channel, c = IrcChannel.objects.get_or_create(name=channel)
|
||||
channel, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
|
||||
channel.topic_msg = topic
|
||||
channel.topic_time = timezone.now()
|
||||
channel.topic_by = setter
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""Start the IRC bot via Django management command."""
|
||||
|
||||
import logging
|
||||
import signal
|
||||
|
||||
|
@ -7,7 +6,6 @@ from django.core.management import BaseCommand
|
|||
|
||||
from ircbot.bot import IRCBot
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot')
|
||||
|
||||
|
||||
|
@ -19,9 +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()
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,4 @@
|
|||
"""Track basic IRC settings and similar."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
@ -7,12 +6,14 @@ from django.conf import settings
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
log = logging.getLogger('ircbot.models')
|
||||
|
||||
|
||||
class LowerCaseCharField(models.CharField):
|
||||
"""Provide a case-insensitive, forced-lower CharField."""
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""Manipulate the field value to make it lowercase."""
|
||||
value = super(LowerCaseCharField, self).get_prep_value(value)
|
||||
if value is not None:
|
||||
value = value.lower()
|
||||
|
@ -20,21 +21,22 @@ class LowerCaseCharField(models.CharField):
|
|||
|
||||
|
||||
class Alias(models.Model):
|
||||
|
||||
"""Allow for aliasing of arbitrary regexes to normal supported commands."""
|
||||
|
||||
pattern = models.CharField(max_length=200, unique=True)
|
||||
replacement = models.CharField(max_length=200)
|
||||
|
||||
class Meta:
|
||||
"""Settings for the model."""
|
||||
|
||||
verbose_name_plural = "aliases"
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
"""Provide string representation."""
|
||||
return "{0:s} -> {1:s}".format(self.pattern, self.replacement)
|
||||
|
||||
def replace(self, what):
|
||||
"""Match the regex and replace with the command."""
|
||||
command = None
|
||||
if re.search(self.pattern, what, flags=re.IGNORECASE):
|
||||
command = re.sub(self.pattern, self.replacement, what, flags=re.IGNORECASE)
|
||||
|
@ -43,28 +45,57 @@ class Alias(models.Model):
|
|||
|
||||
|
||||
class BotUser(models.Model):
|
||||
|
||||
"""Configure bot users, which can do things through the bot and standard Django auth."""
|
||||
|
||||
nickmask = models.CharField(max_length=200, unique=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
"""Settings for the model."""
|
||||
|
||||
permissions = (
|
||||
('quit_bot', "Can tell the bot to quit via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
"""Provide string representation."""
|
||||
return "{0:s} (Django user {1:s})".format(self.nickmask, self.user.username)
|
||||
|
||||
|
||||
class IrcChannel(models.Model):
|
||||
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):
|
||||
"""Track channel settings."""
|
||||
|
||||
name = LowerCaseCharField(max_length=200, unique=True)
|
||||
name = LowerCaseCharField(max_length=200)
|
||||
server = models.ForeignKey('IrcServer', on_delete=models.CASCADE)
|
||||
autojoin = models.BooleanField(default=False)
|
||||
|
||||
topic_msg = models.TextField(default='', blank=True)
|
||||
|
@ -74,30 +105,34 @@ class IrcChannel(models.Model):
|
|||
markov_learn_from_channel = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
"""Settings for the model."""
|
||||
|
||||
constraints = (
|
||||
models.UniqueConstraint(fields=['name', 'server'], name='unique_server_channel'),
|
||||
)
|
||||
permissions = (
|
||||
('manage_current_channels', "Can join/part channels via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
"""Provide string representation."""
|
||||
return "{0:s}".format(self.name)
|
||||
|
||||
|
||||
class IrcPlugin(models.Model):
|
||||
|
||||
"""Represent an IRC plugin and its loading settings."""
|
||||
|
||||
path = models.CharField(max_length=200, unique=True)
|
||||
autoload = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
"""Settings for the model."""
|
||||
|
||||
ordering = ['path']
|
||||
permissions = (
|
||||
('manage_loaded_plugins', "Can load/unload plugins via IRC"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""String representation."""
|
||||
|
||||
"""Provide string representation."""
|
||||
return "{0:s}".format(self.path)
|
||||
|
|
|
@ -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,12 @@ 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()])
|
||||
if connection.server_config.additional_addressed_nicks:
|
||||
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)
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
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
|
||||
channel = None
|
||||
if irc.client.is_channel(target):
|
||||
channel, c = IrcChannel.objects.get_or_create(name=target)
|
||||
channel, c = IrcChannel.objects.get_or_create(name=target, server=connection.server_config)
|
||||
|
||||
if channel and not channel.markov_learn_from_channel:
|
||||
log.debug("not learning from %s as i've been told to ignore it", channel)
|
||||
|
|
|
@ -31,6 +31,8 @@ class Twitter(Plugin):
|
|||
|
||||
self.poll_mentions = False
|
||||
|
||||
self.server = connection.server_config
|
||||
|
||||
super(Twitter, self).__init__(bot, connection, event)
|
||||
|
||||
def start(self):
|
||||
|
@ -272,6 +274,10 @@ class Twitter(Plugin):
|
|||
out_chan = twittersettings.mentions_output_channel.name
|
||||
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.reverse()
|
||||
for mention in mentions:
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
# coding: utf-8
|
||||
"""Get results of weather queries."""
|
||||
import logging
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query_wttr_in(query):
|
||||
"""Hit the wttr.in JSON API with the provided query."""
|
||||
logger.info(f"about to query wttr.in with '{query}'")
|
||||
logger.info("about to query wttr.in with '%s'", query)
|
||||
response = requests.get(f'http://wttr.in/{quote(query)}?format=j1')
|
||||
response.raise_for_status()
|
||||
|
||||
weather_info = response.json()
|
||||
logger.debug(f"results: {weather_info}")
|
||||
logger.debug("results: %s", weather_info)
|
||||
return weather_info
|
||||
|
||||
|
||||
def weather_summary(query):
|
||||
"""Create a more consumable version of the weather report."""
|
||||
logger.info(f"assembling weather summary for '{query}'")
|
||||
logger.info("assembling weather summary for '%s'", query)
|
||||
weather_info = query_wttr_in(query)
|
||||
|
||||
# get some common/nested stuff once now
|
||||
|
@ -30,20 +31,46 @@ def weather_summary(query):
|
|||
tomorrow_forecast = weather_info['weather'][1]
|
||||
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']]
|
||||
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']]
|
||||
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']]
|
||||
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 = {
|
||||
'location': query,
|
||||
'location': location_str,
|
||||
'current': {
|
||||
'description': weather_desc.get('value'),
|
||||
'temp_C': f"{current.get('temp_C')}°C",
|
||||
|
@ -82,5 +109,5 @@ def weather_summary(query):
|
|||
},
|
||||
}
|
||||
|
||||
logger.debug(f"results: {summary}")
|
||||
logger.debug("results: %s", summary)
|
||||
return summary
|
||||
|
|
Loading…
Reference in New Issue