Compare commits

...

11 Commits

Author SHA1 Message Date
Brian S. Stephan d7b7bdf73d add another word match for countdown text triggers 2021-04-25 21:05:09 -05:00
Brian S. Stephan a03c69258f use per-server IrcChannel for reminders
this makes it so that if we have multiple bot instances running, they
will only pay attention to the countdown items for their current server
2021-04-25 21:04:11 -05:00
Brian S. Stephan 43f2b09057 don't add the empty string to additional nicks
thinko on my part, this was making the regex for matching all nicks to
'|nick' when the field is '', because of split producing ['']. in
particular this was making markov trigger on every line
2021-04-25 21:00:34 -05:00
Brian S. Stephan 3aa3fb14e4 add RPC API to retrieve fact(s) 2021-04-25 18:48:34 -05:00
Brian S. Stephan 1fc8af09f8 use nearest area to produce return location 2021-04-25 16:34:01 -05:00
Brian S. Stephan 53c874dc21 option to replace IRC control chars with markdown
^C^B isn't allowed through Discord's API, and I'm sure some other stuff
like colors that I don't use. this makes it a server option to replace
them with Markdown, though I think this would only ever be interesting
for BitlBee + Discord
2021-04-25 12:11:59 -05:00
Brian S. Stephan 1036c08147 only autojoin channels for this connection 2021-04-25 11:38:19 -05:00
Brian S. Stephan 9c1109107b relate channels to their server
this is necessary for supporting multiple irc servers in one bot config.
this also has the side effect of requiring some code in ircbot and
markov which autocreates channels to also include the server (retrieved
via the connection). this will again help keep channels coherent for
multi-server arrangements

the twitter bot change here is untested but seems like the right idea (I
haven't used the twitter package in forever)
2021-04-25 11:13:10 -05:00
Brian S. Stephan 6136127c5f move IRC server settings to database
this is the first step in trying to get the bot to support multiple
servers with different channels, countdown triggers, and so on

this also ends up affecting some configuration around:
* dispatch
* markov
* admin privmsg form
2021-04-25 10:17:41 -05:00
Brian S. Stephan 44d8b7db00 lint cleanups 2021-04-24 20:49:19 -05:00
Brian S. Stephan d518cb2b77 lint cleanups 2021-04-24 20:49:14 -05:00
24 changed files with 386 additions and 127 deletions

View File

@ -12,6 +12,7 @@ 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')
@ -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+' 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)\s+(?P<text>.*?)' r'(to|that|about|with)\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
@ -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 # 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, 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: 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) 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) 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 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:
@ -143,8 +147,12 @@ 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=event.sent_location) reminder_message=message, reminder_target_new=reminder_target)
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

@ -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'),
),
]

View File

@ -14,6 +14,8 @@ 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

@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2021-04-25 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dispatch', '0005_auto_20160116_1955'),
]
operations = [
migrations.AddField(
model_name='dispatcher',
name='bot_xmlrpc_host',
field=models.CharField(default='localhost', max_length=200),
),
migrations.AddField(
model_name='dispatcher',
name='bot_xmlrpc_port',
field=models.PositiveSmallIntegerField(default=13132),
),
]

View File

@ -1,10 +1,8 @@
"""Track dispatcher configurations.""" """Track dispatcher configurations."""
import logging import logging
from django.db import models from django.db import models
log = logging.getLogger('dispatch.models') log = logging.getLogger('dispatch.models')
@ -13,6 +11,9 @@ class Dispatcher(models.Model):
key = models.CharField(max_length=16, unique=True) key = models.CharField(max_length=16, unique=True)
bot_xmlrpc_host = models.CharField(max_length=200, default='localhost')
bot_xmlrpc_port = models.PositiveSmallIntegerField(default=13132)
class Meta: class Meta:
"""Meta options.""" """Meta options."""
@ -21,7 +22,7 @@ class Dispatcher(models.Model):
) )
def __str__(self): def __str__(self):
"""String representation.""" """Provide string representation."""
return "{0:s}".format(self.key) return "{0:s}".format(self.key)
@ -42,5 +43,5 @@ class DispatcherAction(models.Model):
include_key = models.BooleanField(default=False) include_key = models.BooleanField(default=False)
def __str__(self): def __str__(self):
"""String representation.""" """Provide string representation."""
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination) return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination)

View File

@ -1,18 +1,15 @@
"""Handle dispatcher API requests.""" """Handle dispatcher API requests."""
import copy import copy
import logging import logging
import os import os
import xmlrpc.client import xmlrpc.client
from django.conf import settings
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from dispatch.models import Dispatcher, DispatcherAction from dispatch.models import Dispatcher, DispatcherAction
from dispatch.serializers import DispatchMessageSerializer, DispatcherSerializer, DispatcherActionSerializer from dispatch.serializers import DispatcherActionSerializer, DispatcherSerializer, DispatchMessageSerializer
log = logging.getLogger('dispatch.views') log = logging.getLogger('dispatch.views')
@ -77,7 +74,7 @@ class DispatchMessage(generics.GenericAPIView):
if action.type == DispatcherAction.PRIVMSG_TYPE: if action.type == DispatcherAction.PRIVMSG_TYPE:
# connect over XML-RPC and send # connect over XML-RPC and send
try: try:
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT) bot_url = 'http://{0:s}:{1:d}/'.format(dispatcher.bot_xmlrpc_host, dispatcher.bot_xmlrpc_port)
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True) bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
log.debug("sending '%s' to channel %s", text, action.destination) log.debug("sending '%s' to channel %s", text, action.destination)
bot.reply(None, text, False, action.destination) bot.reply(None, text, False, action.destination)

View File

@ -154,32 +154,6 @@ ACCOUNT_ACTIVATION_DAYS = 7
REGISTRATION_AUTO_LOGIN = True REGISTRATION_AUTO_LOGIN = True
# IRC bot stuff
# tuple of hostname, port number, and password (or None)
IRCBOT_SERVER_LIST = [
('localhost', 6667, None),
]
IRCBOT_NICKNAME = 'dr_botzo'
IRCBOT_REALNAME = 'Dr. Botzo'
IRCBOT_SSL = False
IRCBOT_IPV6 = False
# post-connect, pre-autojoin stuff
IRCBOT_SLEEP_BEFORE_AUTOJOIN_SECONDS = 10
IRCBOT_POST_CONNECT_COMMANDS = [ ]
# XML-RPC settings
IRCBOT_XMLRPC_HOST = 'localhost'
IRCBOT_XMLRPC_PORT = 13132
# nick hack for discord through bitlbee
ADDITIONAL_NICK_MATCHES = []
# IRC module stuff # IRC module stuff
# karma # karma

14
facts/serializers.py Normal file
View File

@ -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')

View File

@ -1,9 +1,13 @@
"""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 index, factcategory_detail from facts.views import factcategory_detail, index, rpc_get_facts, rpc_get_random_fact
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,12 +2,49 @@
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,23 +3,15 @@
import logging import logging
import xmlrpc.client import xmlrpc.client
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.shortcuts import render from django.shortcuts import render
from ircbot.forms import PrivmsgForm from ircbot.forms import PrivmsgForm
from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.admin') log = logging.getLogger('ircbot.admin')
admin.site.register(Alias)
admin.site.register(BotUser)
admin.site.register(IrcChannel)
admin.site.register(IrcPlugin)
def send_privmsg(request): def send_privmsg(request):
"""Send a privmsg over XML-RPC to the IRC bot.""" """Send a privmsg over XML-RPC to the IRC bot."""
if request.method == 'POST': if request.method == 'POST':
@ -28,7 +20,8 @@ def send_privmsg(request):
target = form.cleaned_data['target'] target = form.cleaned_data['target']
message = form.cleaned_data['message'] message = form.cleaned_data['message']
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT) bot_url = 'http://{0:s}:{1:d}/'.format(form.cleaned_data['xmlrpc_host'],
form.cleaned_data['xmlrpc_port'])
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True) bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
bot.reply(None, message, False, target) bot.reply(None, message, False, target)
form = PrivmsgForm() form = PrivmsgForm()
@ -37,4 +30,11 @@ def send_privmsg(request):
return render(request, 'privmsg.html', {'form': form}) return render(request, 'privmsg.html', {'form': form})
admin.site.register(Alias)
admin.site.register(BotUser)
admin.site.register(IrcChannel)
admin.site.register(IrcPlugin)
admin.site.register(IrcServer)
admin.site.register_view('ircbot/privmsg/', "Ircbot - privmsg", view=send_privmsg, urlname='ircbot_privmsg') admin.site.register_view('ircbot/privmsg/', "Ircbot - privmsg", view=send_privmsg, urlname='ircbot_privmsg')

View File

@ -22,7 +22,7 @@ from irc.dict import IRCDict
import irc.modes import irc.modes
import ircbot.lib as ircbotlib import ircbot.lib as ircbotlib
from ircbot.models import Alias, IrcChannel, IrcPlugin from ircbot.models import Alias, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.bot') log = logging.getLogger('ircbot.bot')
@ -54,6 +54,8 @@ class LenientServerConnection(irc.client.ServerConnection):
buffer_class = irc.buffer.LenientDecodingLineBuffer buffer_class = irc.buffer.LenientDecodingLineBuffer
server_config = None
def _prep_message(self, string): def _prep_message(self, string):
"""Override SimpleIRCClient._prep_message to add some logging.""" """Override SimpleIRCClient._prep_message to add some logging."""
log.debug("preparing message %s", string) log.debug("preparing message %s", string)
@ -163,13 +165,19 @@ class DrReactor(irc.client.Reactor):
event.original_msg = what event.original_msg = what
# check if we were addressed or not # check if we were addressed or not
all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()]) 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) 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)
@ -351,15 +359,16 @@ class IRCBot(irc.client.SimpleIRCClient):
reactor_class = DrReactor reactor_class = DrReactor
splitter = "..." splitter = "..."
def __init__(self, reconnection_interval=60): def __init__(self, server_name, reconnection_interval=60):
"""Initialize bot.""" """Initialize bot."""
super(IRCBot, self).__init__() super(IRCBot, self).__init__()
self.channels = IRCDict() self.channels = IRCDict()
self.plugins = [] self.plugins = []
# set up the server list self.server_config = IrcServer.objects.get(name=server_name)
self.server_list = settings.IRCBOT_SERVER_LIST # the reactor made the connection, save the server reference in it since we pass that around
self.connection.server_config = self.server_config
# set reconnection interval # set reconnection interval
if not reconnection_interval or reconnection_interval < 0: if not reconnection_interval or reconnection_interval < 0:
@ -367,8 +376,8 @@ class IRCBot(irc.client.SimpleIRCClient):
self.reconnection_interval = reconnection_interval self.reconnection_interval = reconnection_interval
# set basic stuff # set basic stuff
self._nickname = settings.IRCBOT_NICKNAME self._nickname = self.server_config.nickname
self._realname = settings.IRCBOT_REALNAME self._realname = self.server_config.realname
# guess at nickmask. hopefully _on_welcome() will set this, but this should be # guess at nickmask. hopefully _on_welcome() will set this, but this should be
# a pretty good guess if not # a pretty good guess if not
@ -395,7 +404,7 @@ class IRCBot(irc.client.SimpleIRCClient):
getattr(self, 'handle_reload'), -20) getattr(self, 'handle_reload'), -20)
# load XML-RPC server # load XML-RPC server
self.xmlrpc = SimpleXMLRPCServer((settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT), self.xmlrpc = SimpleXMLRPCServer((self.server_config.xmlrpc_host, self.server_config.xmlrpc_port),
requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True) requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions() self.xmlrpc.register_introspection_functions()
@ -414,16 +423,15 @@ class IRCBot(irc.client.SimpleIRCClient):
self.jump_server() self.jump_server()
def _connect(self): def _connect(self):
server = self.server_list[0]
try: try:
# build the connection factory as determined by IPV6/SSL settings # build the connection factory as determined by IPV6/SSL settings
if settings.IRCBOT_SSL: if self.server_config.use_ssl:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=settings.IRCBOT_IPV6) connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=self.server_config.use_ipv6)
else: else:
connect_factory = Factory(ipv6=settings.IRCBOT_IPV6) connect_factory = Factory(ipv6=self.server_config.use_ipv6)
self.connect(server[0], server[1], self._nickname, server[2], ircname=self._realname, self.connect(self.server_config.hostname, self.server_config.port, self._nickname,
connect_factory=connect_factory) self.server_config.password, ircname=self._realname, connect_factory=connect_factory)
except irc.client.ServerConnectionError: except irc.client.ServerConnectionError:
pass pass
@ -528,15 +536,16 @@ class IRCBot(irc.client.SimpleIRCClient):
log.debug("welcome: %s", what) log.debug("welcome: %s", what)
# run automsg commands # run automsg commands
for cmd in settings.IRCBOT_POST_CONNECT_COMMANDS: if self.server_config.post_connect:
# TODO NOTE: if the bot is sending something that changes the vhost for cmd in self.server_config.post_connect.split('\n'):
# (like 'hostserv on') we don't pick it up # TODO NOTE: if the bot is sending something that changes the vhost
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:])) # (like 'hostserv on') we don't pick it up
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:]))
# sleep before doing autojoins # sleep before doing autojoins
time.sleep(settings.IRCBOT_SLEEP_BEFORE_AUTOJOIN_SECONDS) time.sleep(self.server_config.delay_before_joins)
for chan in IrcChannel.objects.filter(autojoin=True): for chan in IrcChannel.objects.filter(autojoin=True, server=connection.server_config):
log.info("autojoining %s", chan.name) log.info("autojoining %s", chan.name)
self.connection.join(chan) self.connection.join(chan)
@ -584,7 +593,6 @@ class IRCBot(irc.client.SimpleIRCClient):
if self.connection.is_connected(): if self.connection.is_connected():
self.connection.disconnect(msg) self.connection.disconnect(msg)
self.server_list.append(self.server_list.pop(0))
self._connect() self._connect()
def on_ctcp(self, c, e): def on_ctcp(self, c, e):
@ -889,6 +897,12 @@ 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,10 +1,9 @@
"""Forms for doing ircbot stuff.""" """Forms for doing ircbot stuff."""
import logging import logging
from django.forms import Form, CharField, Textarea from django.forms import CharField, Form, IntegerField, Textarea
log = logging.getLogger('markov.forms') log = logging.getLogger('ircbot.forms')
class PrivmsgForm(Form): class PrivmsgForm(Form):
@ -12,3 +11,5 @@ class PrivmsgForm(Form):
target = CharField() target = CharField()
message = CharField(widget=Textarea) message = CharField(widget=Textarea)
xmlrpc_host = CharField()
xmlrpc_port = IntegerField()

View File

@ -1,19 +1,17 @@
"""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]+)',
@ -25,7 +23,6 @@ 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)
@ -34,11 +31,10 @@ 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) chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
log.debug("joining channel %s", channel) log.debug("joining channel %s", channel)
self.connection.join(channel) self.connection.join(channel)
@ -46,11 +42,10 @@ 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) chan_mod, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
log.debug("parting channel %s", channel) log.debug("parting channel %s", channel)
self.connection.part(channel) self.connection.part(channel)
@ -58,7 +53,6 @@ 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,5 +1,4 @@
"""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
@ -7,24 +6,20 @@ 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()
@ -32,13 +27,12 @@ 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) channel, c = IrcChannel.objects.get_or_create(name=channel, server=connection.server_config)
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,5 +1,4 @@
"""Start the IRC bot via Django management command.""" """Start the IRC bot via Django management command."""
import logging import logging
import signal import signal
@ -7,7 +6,6 @@ from django.core.management import BaseCommand
from ircbot.bot import IRCBot from ircbot.bot import IRCBot
log = logging.getLogger('ircbot') log = logging.getLogger('ircbot')
@ -19,9 +17,13 @@ class Command(BaseCommand):
help = "Start the IRC bot" help = "Start the IRC bot"
def add_arguments(self, parser):
"""Add arguments to the bot startup."""
parser.add_argument('server_name')
def handle(self, *args, **options): def handle(self, *args, **options):
"""Start the IRC bot and spin forever.""" """Start the IRC bot and spin forever."""
self.stdout.write(self.style.NOTICE(f"Starting up {options['server_name']} bot"))
irc = IRCBot() irc = IRCBot(options['server_name'])
signal.signal(signal.SIGINT, irc.sigint_handler) signal.signal(signal.SIGINT, irc.sigint_handler)
irc.start() irc.start()

View File

@ -0,0 +1,32 @@
# Generated by Django 3.1.2 on 2021-04-25 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0014_auto_20160116_1955'),
]
operations = [
migrations.CreateModel(
name='IrcServer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, unique=True)),
('hostname', models.CharField(max_length=200)),
('port', models.PositiveSmallIntegerField(default=6667)),
('password', models.CharField(blank=True, default=None, max_length=200, null=True)),
('nickname', models.CharField(max_length=32)),
('realname', models.CharField(blank=True, default='', max_length=32)),
('additional_addressed_nicks', models.TextField(blank=True, default='', help_text='For e.g. BitlBee alternative nicks')),
('use_ssl', models.BooleanField(default=False)),
('use_ipv6', models.BooleanField(default=False)),
('post_connect', models.TextField(blank=True, default='')),
('delay_before_joins', models.PositiveSmallIntegerField(default=0)),
('xmlrpc_host', models.CharField(default='localhost', max_length=200)),
('xmlrpc_port', models.PositiveSmallIntegerField(default=13132)),
],
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.2 on 2021-04-25 04:11
from django.db import migrations
def create_placeholder_server(apps, schema_editor):
"""Create the first server entry, to be configured by the admin."""
IrcServer = apps.get_model('ircbot', 'IrcServer')
IrcServer.objects.create(name='default', hostname='irc.example.org', port=6667)
def delete_placeholder_server(apps, schema_editor):
"""Remove the default server."""
IrcServer = apps.get_model('ircbot', 'IrcServer')
IrcServer.objects.filter(name='default').delete()
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0015_ircserver'),
]
operations = [
migrations.RunPython(create_placeholder_server, delete_placeholder_server),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -1,5 +1,4 @@
"""Track basic IRC settings and similar.""" """Track basic IRC settings and similar."""
import logging import logging
import re import re
@ -7,12 +6,14 @@ 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()
@ -20,21 +21,22 @@ 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):
"""String representation.""" """Provide 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)
@ -43,28 +45,57 @@ 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):
"""String representation.""" """Provide 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 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.""" """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) autojoin = models.BooleanField(default=False)
topic_msg = models.TextField(default='', blank=True) topic_msg = models.TextField(default='', blank=True)
@ -74,30 +105,34 @@ 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):
"""String representation.""" """Provide 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):
"""String representation.""" """Provide string representation."""
return "{0:s}".format(self.path) return "{0:s}".format(self.path)

View File

@ -1,23 +1,22 @@
"""IRC support for Markov chain learning and text generation."""
import logging import logging
import re import re
import irc.client import irc.client
from django.conf import settings from django.conf import settings
import markov.lib as markovlib
from ircbot.lib import Plugin, reply_destination_for_event from ircbot.lib import Plugin, reply_destination_for_event
from ircbot.models import IrcChannel from ircbot.models import IrcChannel
import markov.lib as markovlib
log = logging.getLogger('markov.ircplugin') log = logging.getLogger('markov.ircplugin')
class Markov(Plugin): class Markov(Plugin):
"""Build Markov chains and reply with them.""" """Build Markov chains and reply with them."""
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.connection.add_global_handler('pubmsg', self.handle_chatter, -20) self.connection.add_global_handler('pubmsg', self.handle_chatter, -20)
self.connection.add_global_handler('privmsg', self.handle_chatter, -20) self.connection.add_global_handler('privmsg', self.handle_chatter, -20)
@ -29,7 +28,6 @@ class Markov(Plugin):
def stop(self): def stop(self):
"""Tear down handlers.""" """Tear down handlers."""
self.connection.remove_global_handler('pubmsg', self.handle_chatter) self.connection.remove_global_handler('pubmsg', self.handle_chatter)
self.connection.remove_global_handler('privmsg', self.handle_chatter) self.connection.remove_global_handler('privmsg', self.handle_chatter)
@ -39,7 +37,6 @@ class Markov(Plugin):
def handle_reply(self, connection, event, match): def handle_reply(self, connection, event, match):
"""Generate a reply to one line, without learning it.""" """Generate a reply to one line, without learning it."""
target = reply_destination_for_event(event) target = reply_destination_for_event(event)
min_size = 15 min_size = 15
@ -63,9 +60,12 @@ class Markov(Plugin):
def handle_chatter(self, connection, event): def handle_chatter(self, connection, event):
"""Learn from IRC chatter.""" """Learn from IRC chatter."""
what = event.arguments[0] what = event.arguments[0]
all_nicks = '|'.join(settings.ADDITIONAL_NICK_MATCHES + [connection.get_nickname()]) 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) 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) channel, c = IrcChannel.objects.get_or_create(name=target, server=connection.server_config)
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,6 +31,8 @@ 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):
@ -272,6 +274,10 @@ 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,26 +1,27 @@
# coding: utf-8 # coding: utf-8
"""Get results of weather queries.""" """Get results of weather queries."""
import logging import logging
import requests
from urllib.parse import quote from urllib.parse import quote
import requests
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(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 = 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(f"results: {weather_info}") logger.debug("results: %s", 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(f"assembling weather summary for '{query}'") logger.info("assembling weather summary for '%s'", 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
@ -30,20 +31,46 @@ 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': query, 'location': location_str,
'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",
@ -82,5 +109,5 @@ def weather_summary(query):
}, },
} }
logger.debug(f"results: {summary}") logger.debug("results: %s", summary)
return summary return summary