sopel/trigger.py

195 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
The trigger abstraction layer.
"""
import re
import sys
import datetime
import tools
class PreTrigger(object):
"""A parsed message from the server, which has not been matched against
any rules."""
component_regex = re.compile(r'([^!]*)!?([^@]*)@?(.*)')
intent_regex = re.compile('\x01(\\S+) ?(.*)\x01')
def __init__(self, own_nick, line):
"""own_nick is the bot's nick, needed to correctly parse sender.
line is the full line from the server."""
line = line.strip('\r')
self.line = line
# Break off IRCv3 message tags, if present
self.tags = {}
if line.startswith('@'):
tagstring, line = line.split(' ', 1)
for tag in tagstring[1:].split(';'):
tag = tag.split('=', 1)
if len(tag) > 1:
self.tags[tag[0]] = tag[1]
else:
self.tags[tag[0]] = None
self.time = datetime.datetime.utcnow()
if 'time' in self.tags:
try:
self.time = datetime.datetime.strptime(self.tags['time'], '%Y-%m-%dT%H:%M:%S.%fZ')
except ValueError:
pass # Server isn't conforming to spec, ignore the server-time
# TODO note what this is doing and why
if line.startswith(':'):
self.hostmask, line = line[1:].split(' ', 1)
else:
self.hostmask = None
# TODO note what this is doing and why
if ' :' in line:
argstr, text = line.split(' :', 1)
self.args = argstr.split(' ')
self.args.append(text)
else:
self.args = line.split(' ')
self.text = self.args[-1]
self.event = self.args[0]
self.args = self.args[1:]
components = PreTrigger.component_regex.match(self.hostmask or '').groups()
self.nick, self.user, self.host = components
self.nick = tools.Identifier(self.nick)
# If we have arguments, the first one is the sender
# Unless it's a QUIT event
if self.args and self.event != 'QUIT':
target = tools.Identifier(self.args[0])
else:
target = None
# Unless we're messaging the bot directly, in which case that second
# arg will be our bot's name.
if target and target.lower() == own_nick.lower():
target = self.nick
self.sender = target
# Parse CTCP into a form consistent with IRCv3 intents
if self.event == 'PRIVMSG' or self.event == 'NOTICE':
intent_match = PreTrigger.intent_regex.match(self.args[-1])
if intent_match:
intent, message = intent_match.groups()
self.tags['intent'] = intent
self.args[-1] = message or ''
# Populate account from extended-join messages
if self.event == 'JOIN' and len(self.args) == 3:
# Account is the second arg `...JOIN #Sopel account :realname`
self.tags['account'] = self.args[1]
class Trigger(str):
"""A line from the server, which has matched a callable's rules.
Note that CTCP messages (`PRIVMSG`es and `NOTICE`es which start and end
with `'\\x01'`) will have the `'\\x01'` bytes stripped, and the command
(e.g. `ACTION`) placed mapped to the `'intent'` key in `Trigger.tags`.
"""
sender = property(lambda self: self._pretrigger.sender)
"""The channel from which the message was sent.
In a private message, this is the nick that sent the message."""
time = property(lambda self: self._pretrigger.time)
"""A datetime object at which the message was received by the IRC server.
If the server does not support server-time, then `time` will be the time
that the message was received by Sopel"""
raw = property(lambda self: self._pretrigger.line)
"""The entire message, as sent from the server. This includes the CTCP
\\x01 bytes and command, if they were included."""
is_privmsg = property(lambda self: self._is_privmsg)
"""True if the trigger is from a user, False if it's from a channel."""
hostmask = property(lambda self: self._pretrigger.hostmask)
"""Hostmask of the person who sent the message as <nick>!<user>@<host>"""
user = property(lambda self: self._pretrigger.user)
"""Local username of the person who sent the message"""
nick = property(lambda self: self._pretrigger.nick)
"""The :class:`sopel.tools.Identifier` of the person who sent the message.
"""
host = property(lambda self: self._pretrigger.host)
"""The hostname of the person who sent the message"""
event = property(lambda self: self._pretrigger.event)
"""The IRC event (e.g. ``PRIVMSG`` or ``MODE``) which triggered the
message."""
match = property(lambda self: self._match)
"""The regular expression :class:`re.MatchObject` for the triggering line.
"""
group = property(lambda self: self._match.group)
"""The ``group`` function of the ``match`` attribute.
See Python :mod:`re` documentation for details."""
groups = property(lambda self: self._match.groups)
"""The ``groups`` function of the ``match`` attribute.
See Python :mod:`re` documentation for details."""
groupdict = property(lambda self: self._match.groupdict)
"""The ``groupdict`` function of the ``match`` attribute.
See Python :mod:`re` documentation for details."""
args = property(lambda self: self._pretrigger.args)
"""
A tuple containing each of the arguments to an event. These are the
strings passed between the event name and the colon. For example,
setting ``mode -m`` on the channel ``#example``, args would be
``('#example', '-m')``
"""
tags = property(lambda self: self._pretrigger.tags)
"""A map of the IRCv3 message tags on the message."""
admin = property(lambda self: self._admin)
"""True if the nick which triggered the command is one of the bot's admins.
"""
owner = property(lambda self: self._owner)
"""True if the nick which triggered the command is the bot's owner."""
account = property(lambda self: self.tags.get('account') or self._account)
"""The account name of the user sending the message.
This is only available if either the account-tag or the account-notify and
extended-join capabilites are available. If this isn't the case, or the user
sending the message isn't logged in, this will be None.
"""
def __new__(cls, config, message, match, account=None):
self = str.__new__(cls, message.args[-1] if message.args else '')
self._account = account
self._pretrigger = message
self._match = match
self._is_privmsg = message.sender and message.sender.is_nick()
def match_host_or_nick(pattern):
pattern = tools.get_hostmask_regex(pattern)
return bool(
pattern.match(self.nick) or
pattern.match('@'.join((self.nick, self.host)))
)
if config.core.owner_account:
self._owner = config.core.owner_account == self.account
else:
self._owner = match_host_or_nick(config.core.owner)
self._admin = (
self._owner or
self.account in config.core.admin_accounts or
any(match_host_or_nick(item) for item in config.core.admins)
)
return self
def set_nick(self, new_nick):
"""Sets the trigger's nick to something new."""
self._pretrigger.nick = new_nick
self._nick = self._pretrigger.nick
def set_group(self, new_group):
"""Sets the trigger's group to something new."""
self._match = re.match("\..+", new_group)
self._group = self._match.group()