2021-04-25 08:59:01 -05:00
|
|
|
"""IRC support for Markov chain learning and text generation."""
|
2015-05-15 17:02:28 -05:00
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
|
|
|
|
import irc.client
|
|
|
|
|
2021-04-25 08:59:01 -05:00
|
|
|
import markov.lib as markovlib
|
2015-05-15 17:02:28 -05:00
|
|
|
from ircbot.lib import Plugin, reply_destination_for_event
|
2015-09-17 22:22:59 -05:00
|
|
|
from ircbot.models import IrcChannel
|
2023-02-19 17:38:25 -06:00
|
|
|
from markov.models import MarkovContext, MarkovTarget
|
2015-05-15 17:02:28 -05:00
|
|
|
|
|
|
|
log = logging.getLogger('markov.ircplugin')
|
|
|
|
|
|
|
|
|
|
|
|
class Markov(Plugin):
|
|
|
|
"""Build Markov chains and reply with them."""
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
"""Set up the handlers."""
|
2015-05-15 22:21:15 -05:00
|
|
|
self.connection.add_global_handler('pubmsg', self.handle_chatter, -20)
|
|
|
|
self.connection.add_global_handler('privmsg', self.handle_chatter, -20)
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2015-05-15 22:21:15 -05:00
|
|
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
|
|
|
|
r'^!markov\s+reply(\s+min=(\d+))?(\s+max=(\d+))?(\s+(.*)$|$)',
|
|
|
|
self.handle_reply, -20)
|
2015-05-15 18:37:48 -05:00
|
|
|
|
2015-05-15 17:02:28 -05:00
|
|
|
super(Markov, self).start()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
"""Tear down handlers."""
|
2015-05-15 22:21:15 -05:00
|
|
|
self.connection.remove_global_handler('pubmsg', self.handle_chatter)
|
|
|
|
self.connection.remove_global_handler('privmsg', self.handle_chatter)
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2015-05-15 22:21:15 -05:00
|
|
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_reply)
|
2015-05-15 18:37:48 -05:00
|
|
|
|
2015-05-15 17:02:28 -05:00
|
|
|
super(Markov, self).stop()
|
|
|
|
|
2015-05-15 18:37:48 -05:00
|
|
|
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
|
|
|
|
max_size = 30
|
2023-02-19 17:38:25 -06:00
|
|
|
context = self.get_or_create_target_context(target)
|
2015-05-15 18:37:48 -05:00
|
|
|
|
|
|
|
if match.group(2):
|
|
|
|
min_size = int(match.group(2))
|
|
|
|
if match.group(4):
|
|
|
|
max_size = int(match.group(4))
|
|
|
|
|
|
|
|
if match.group(5) != '':
|
|
|
|
line = match.group(6)
|
|
|
|
topics = [x for x in line.split(' ') if len(x) >= 3]
|
|
|
|
|
2016-01-16 17:58:11 -06:00
|
|
|
return self.bot.reply(event, " ".join(markovlib.generate_line(context, topics=topics,
|
2016-06-30 23:26:04 -05:00
|
|
|
min_words=min_size, max_words=max_size)))
|
2015-05-15 18:37:48 -05:00
|
|
|
else:
|
2016-01-16 17:58:11 -06:00
|
|
|
return self.bot.reply(event, " ".join(markovlib.generate_line(context, min_words=min_size,
|
2016-06-30 23:26:04 -05:00
|
|
|
max_words=max_size)))
|
2015-05-15 18:37:48 -05:00
|
|
|
|
2015-05-15 17:02:28 -05:00
|
|
|
def handle_chatter(self, connection, event):
|
|
|
|
"""Learn from IRC chatter."""
|
|
|
|
what = event.arguments[0]
|
2023-02-18 18:44:42 -06:00
|
|
|
who = irc.client.NickMask(event.source).nick
|
2015-05-15 17:02:28 -05:00
|
|
|
target = reply_destination_for_event(event)
|
|
|
|
|
2023-02-18 18:44:42 -06:00
|
|
|
log.debug("what: '%s', who: '%s', target: '%s'", what, who, target)
|
2015-09-17 22:22:59 -05:00
|
|
|
# check to see whether or not we should learn from this channel
|
|
|
|
channel = None
|
|
|
|
if irc.client.is_channel(target):
|
2021-04-25 11:01:05 -05:00
|
|
|
channel, c = IrcChannel.objects.get_or_create(name=target, server=connection.server_config)
|
2015-09-17 22:22:59 -05:00
|
|
|
|
|
|
|
if channel and not channel.markov_learn_from_channel:
|
|
|
|
log.debug("not learning from %s as i've been told to ignore it", channel)
|
|
|
|
else:
|
|
|
|
# learn the line
|
2023-02-18 18:54:22 -06:00
|
|
|
learning_what = what
|
2023-02-18 20:25:32 -06:00
|
|
|
|
|
|
|
# don't learn the speaker's nick if this came over a bridge
|
|
|
|
if channel and who == channel.discord_bridge:
|
|
|
|
learning_what = ' '.join(learning_what.split(' ')[1:])
|
|
|
|
|
2023-02-18 18:54:22 -06:00
|
|
|
# remove our own nick and aliases from what we learn
|
2023-02-18 18:37:34 -06:00
|
|
|
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()
|
2023-02-19 22:55:14 -06:00
|
|
|
learning_what = re.sub(r'^(({nicks})[:,]|@({nicks})[:,]?)\s+'.format(nicks=all_nicks), '', learning_what)
|
2023-02-18 18:54:22 -06:00
|
|
|
|
2017-03-10 18:29:31 -06:00
|
|
|
recursing = getattr(event, 'recursing', False)
|
2015-09-17 22:22:59 -05:00
|
|
|
if not recursing:
|
2023-02-18 18:54:22 -06:00
|
|
|
log.debug("learning %s", learning_what)
|
2023-02-19 17:38:25 -06:00
|
|
|
context = self.get_or_create_target_context(target)
|
2023-02-18 18:54:22 -06:00
|
|
|
markovlib.learn_line(learning_what, context)
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2019-01-10 08:48:15 -06:00
|
|
|
log.debug("searching '%s' for '%s'", what, all_nicks)
|
|
|
|
if re.search(all_nicks, what, re.IGNORECASE) is not None:
|
2023-02-19 17:38:25 -06:00
|
|
|
context = self.get_or_create_target_context(target)
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2023-02-19 22:55:14 -06:00
|
|
|
addressed_pattern = r'^(({nicks})[:,]|@({nicks})[:,]?)\s+(?P<addressed_msg>.*)'.format(nicks=all_nicks)
|
2019-01-10 08:48:15 -06:00
|
|
|
match = re.match(addressed_pattern, what, re.IGNORECASE)
|
|
|
|
if match:
|
2015-05-15 17:02:28 -05:00
|
|
|
# i was addressed directly, so respond, addressing
|
|
|
|
# the speaker
|
2019-01-10 08:48:15 -06:00
|
|
|
topics = [x for x in match.group('addressed_msg').split(' ') if len(x) >= 3]
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2016-01-16 17:58:11 -06:00
|
|
|
return self.bot.reply(event, "{0:s}: {1:s}"
|
2023-02-18 18:44:42 -06:00
|
|
|
"".format(who, " ".join(markovlib.generate_line(context, topics=topics))))
|
2015-05-15 17:02:28 -05:00
|
|
|
else:
|
|
|
|
# i wasn't addressed directly, so just respond
|
|
|
|
topics = [x for x in what.split(' ') if len(x) >= 3]
|
|
|
|
|
2016-01-16 17:58:11 -06:00
|
|
|
return self.bot.reply(event, "{0:s}"
|
2016-06-30 23:26:04 -05:00
|
|
|
"".format(" ".join(markovlib.generate_line(context, topics=topics))))
|
2015-05-15 17:02:28 -05:00
|
|
|
|
2023-02-19 17:38:25 -06:00
|
|
|
def get_or_create_target_context(self, target_name):
|
|
|
|
"""Return the context for a provided nick/channel, creating missing ones."""
|
|
|
|
target_name = target_name.lower()
|
|
|
|
|
|
|
|
# find the stuff, or create it
|
2023-05-04 17:21:27 -05:00
|
|
|
channel, c = IrcChannel.objects.get_or_create(name=target_name, server=self.connection.server_config)
|
2023-02-19 17:38:25 -06:00
|
|
|
try:
|
2023-05-04 17:21:27 -05:00
|
|
|
target = MarkovTarget.objects.get(channel=channel)
|
2023-02-19 17:38:25 -06:00
|
|
|
except MarkovTarget.DoesNotExist:
|
|
|
|
# we need to create a context and a target, and we have to make the context first
|
|
|
|
# make a context --- lacking a good idea, just create one with this target name until configured otherwise
|
|
|
|
context, c = MarkovContext.objects.get_or_create(name=target_name)
|
|
|
|
target, c = MarkovTarget.objects.get_or_create(name=target_name, context=context, channel=channel)
|
|
|
|
|
|
|
|
return target.context
|
|
|
|
|
|
|
|
try:
|
|
|
|
return target.context
|
|
|
|
except MarkovContext.DoesNotExist:
|
|
|
|
# make a context --- lacking a good idea, just create one with this target name until configured otherwise
|
|
|
|
context, c = MarkovContext.objects.get_or_create(name=target_name)
|
|
|
|
target.context = context
|
|
|
|
target.save()
|
|
|
|
|
|
|
|
return target.context
|
|
|
|
|
2015-05-15 17:02:28 -05:00
|
|
|
|
|
|
|
plugin = Markov
|