2013-02-01 20:38:53 +01:00
|
|
|
# -*- coding: utf8 -*-
|
2005-01-19 14:14:38 +01:00
|
|
|
###
|
2005-01-19 14:33:05 +01:00
|
|
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
2014-06-10 05:44:25 +02:00
|
|
|
# Copyright (c) 2014, James McCoy
|
2005-01-19 14:14:38 +01:00
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions are met:
|
|
|
|
#
|
|
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
|
|
# this list of conditions, and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
# this list of conditions, and the following disclaimer in the
|
|
|
|
# documentation and/or other materials provided with the distribution.
|
|
|
|
# * Neither the name of the author of this software nor the name of
|
|
|
|
# contributors to this software may be used to endorse or promote products
|
|
|
|
# derived from this software without specific prior written consent.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
###
|
|
|
|
|
|
|
|
"""
|
2005-02-07 07:10:41 +01:00
|
|
|
This module contains the basic callbacks for handling PRIVMSGs.
|
2005-01-19 14:14:38 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
import re
|
|
|
|
import copy
|
|
|
|
import time
|
2015-08-11 16:50:23 +02:00
|
|
|
from . import shlex
|
2013-01-22 20:35:11 +01:00
|
|
|
import codecs
|
2005-01-19 14:14:38 +01:00
|
|
|
import getopt
|
|
|
|
import inspect
|
2020-04-11 15:00:46 +02:00
|
|
|
import warnings
|
2013-01-06 11:34:32 +01:00
|
|
|
|
2015-08-11 16:50:23 +02:00
|
|
|
from . import (conf, ircdb, irclib, ircmsgs, ircutils, log, registry,
|
2015-08-09 00:23:03 +02:00
|
|
|
utils, world)
|
2015-08-11 16:50:23 +02:00
|
|
|
from .utils import minisix
|
2015-08-09 00:23:03 +02:00
|
|
|
from .utils.iter import any, all
|
2015-08-31 15:38:35 +02:00
|
|
|
from .i18n import PluginInternationalization
|
2015-08-09 00:23:03 +02:00
|
|
|
_ = PluginInternationalization()
|
|
|
|
|
2020-04-11 15:00:46 +02:00
|
|
|
def _addressed(irc, msg, prefixChars=None, nicks=None,
|
2005-01-19 14:14:38 +01:00
|
|
|
prefixStrings=None, whenAddressedByNick=None,
|
|
|
|
whenAddressedByNickAtEnd=None):
|
2020-04-11 15:00:46 +02:00
|
|
|
if isinstance(irc, str):
|
|
|
|
warnings.warn(
|
|
|
|
"callbacks.addressed's first argument should now be be the Irc "
|
|
|
|
"object instead of the bot's nick.",
|
|
|
|
DeprecationWarning)
|
|
|
|
network = None
|
|
|
|
nick = irc
|
|
|
|
else:
|
|
|
|
network = irc.network
|
|
|
|
nick = irc.nick
|
2005-01-19 14:14:38 +01:00
|
|
|
def get(group):
|
2020-04-11 15:00:46 +02:00
|
|
|
v = group.getSpecific(network=network, channel=msg.channel)
|
|
|
|
return v()
|
2005-01-19 14:14:38 +01:00
|
|
|
def stripPrefixStrings(payload):
|
|
|
|
for prefixString in prefixStrings:
|
|
|
|
if payload.startswith(prefixString):
|
|
|
|
payload = payload[len(prefixString):].lstrip()
|
|
|
|
return payload
|
|
|
|
|
|
|
|
assert msg.command == 'PRIVMSG'
|
2020-04-11 15:00:46 +02:00
|
|
|
target = msg.channel or msg.args[0]
|
|
|
|
payload = msg.args[1]
|
2005-01-19 14:14:38 +01:00
|
|
|
if not payload:
|
|
|
|
return ''
|
|
|
|
if prefixChars is None:
|
|
|
|
prefixChars = get(conf.supybot.reply.whenAddressedBy.chars)
|
|
|
|
if whenAddressedByNick is None:
|
|
|
|
whenAddressedByNick = get(conf.supybot.reply.whenAddressedBy.nick)
|
|
|
|
if whenAddressedByNickAtEnd is None:
|
|
|
|
r = conf.supybot.reply.whenAddressedBy.nick.atEnd
|
|
|
|
whenAddressedByNickAtEnd = get(r)
|
|
|
|
if prefixStrings is None:
|
|
|
|
prefixStrings = get(conf.supybot.reply.whenAddressedBy.strings)
|
|
|
|
# We have to check this before nicks -- try "@google supybot" with supybot
|
|
|
|
# and whenAddressedBy.nick.atEnd on to see why.
|
|
|
|
if any(payload.startswith, prefixStrings):
|
|
|
|
return stripPrefixStrings(payload)
|
|
|
|
elif payload[0] in prefixChars:
|
|
|
|
return payload[1:].strip()
|
|
|
|
if nicks is None:
|
|
|
|
nicks = get(conf.supybot.reply.whenAddressedBy.nicks)
|
2014-01-21 10:57:38 +01:00
|
|
|
nicks = list(map(ircutils.toLower, nicks))
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
nicks = list(nicks) # Just in case.
|
|
|
|
nicks.insert(0, ircutils.toLower(nick))
|
|
|
|
# Ok, let's see if it's a private message.
|
|
|
|
if ircutils.nickEqual(target, nick):
|
|
|
|
payload = stripPrefixStrings(payload)
|
|
|
|
while payload and payload[0] in prefixChars:
|
|
|
|
payload = payload[1:].lstrip()
|
|
|
|
return payload
|
|
|
|
# Ok, not private. Does it start with our nick?
|
|
|
|
elif whenAddressedByNick:
|
|
|
|
for nick in nicks:
|
|
|
|
lowered = ircutils.toLower(payload)
|
|
|
|
if lowered.startswith(nick):
|
|
|
|
try:
|
|
|
|
(maybeNick, rest) = payload.split(None, 1)
|
|
|
|
toContinue = False
|
|
|
|
while not ircutils.isNick(maybeNick, strictRfc=True):
|
|
|
|
if maybeNick[-1].isalnum():
|
|
|
|
toContinue = True
|
|
|
|
break
|
|
|
|
maybeNick = maybeNick[:-1]
|
|
|
|
if toContinue:
|
|
|
|
continue
|
|
|
|
if ircutils.nickEqual(maybeNick, nick):
|
|
|
|
return rest
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
except ValueError: # split didn't work.
|
|
|
|
continue
|
|
|
|
elif whenAddressedByNickAtEnd and lowered.endswith(nick):
|
|
|
|
rest = payload[:-len(nick)]
|
|
|
|
possiblePayload = rest.rstrip(' \t,;')
|
|
|
|
if possiblePayload != rest:
|
|
|
|
# There should be some separator between the nick and the
|
|
|
|
# previous alphanumeric character.
|
|
|
|
return possiblePayload
|
2010-07-27 01:48:37 +02:00
|
|
|
if get(conf.supybot.reply.whenNotAddressed):
|
2005-01-19 14:14:38 +01:00
|
|
|
return payload
|
|
|
|
else:
|
|
|
|
return ''
|
|
|
|
|
2020-04-11 15:00:46 +02:00
|
|
|
def addressed(irc, msg, **kwargs):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""If msg is addressed to 'name', returns the portion after the address.
|
|
|
|
Otherwise returns the empty string.
|
|
|
|
"""
|
|
|
|
payload = msg.addressed
|
|
|
|
if payload is not None:
|
|
|
|
return payload
|
|
|
|
else:
|
2020-04-11 15:00:46 +02:00
|
|
|
payload = _addressed(irc, msg, **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
msg.tag('addressed', payload)
|
|
|
|
return payload
|
|
|
|
|
2013-12-11 17:01:01 +01:00
|
|
|
def canonicalName(command, preserve_spaces=False):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""Turn a command into its canonical form.
|
|
|
|
|
|
|
|
Currently, this makes everything lowercase and removes all dashes and
|
|
|
|
underscores.
|
|
|
|
"""
|
2015-08-09 00:23:03 +02:00
|
|
|
if minisix.PY2 and isinstance(command, unicode):
|
2005-01-19 14:14:38 +01:00
|
|
|
command = command.encode('utf-8')
|
2015-08-09 00:23:03 +02:00
|
|
|
elif minisix.PY3 and isinstance(command, bytes):
|
2012-08-04 11:38:12 +02:00
|
|
|
command = command.decode()
|
2013-12-11 17:01:01 +01:00
|
|
|
special = '\t-_'
|
|
|
|
if not preserve_spaces:
|
|
|
|
special += ' '
|
2005-01-19 14:14:38 +01:00
|
|
|
reAppend = ''
|
|
|
|
while command and command[-1] in special:
|
|
|
|
reAppend = command[-1] + reAppend
|
|
|
|
command = command[:-1]
|
2012-08-04 11:38:12 +02:00
|
|
|
return ''.join([x for x in command if x not in special]).lower() + reAppend
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2019-08-24 14:14:33 +02:00
|
|
|
def reply(*args, **kwargs):
|
|
|
|
warnings.warn('callbacks.reply is deprecated. Use irc.reply instead.',
|
|
|
|
DeprecationWarning)
|
|
|
|
return _makeReply(dynamic.irc, *args, **kwargs)
|
|
|
|
|
|
|
|
def _makeReply(irc, msg, s,
|
|
|
|
prefixNick=None, private=None,
|
|
|
|
notice=None, to=None, action=None, error=False,
|
|
|
|
stripCtcp=True):
|
2005-01-19 14:14:38 +01:00
|
|
|
msg.tag('repliedTo')
|
|
|
|
# Ok, let's make the target:
|
|
|
|
# XXX This isn't entirely right. Consider to=#foo, private=True.
|
|
|
|
target = ircutils.replyTo(msg)
|
2019-08-24 14:14:33 +02:00
|
|
|
def isPublic(s):
|
2019-08-24 17:50:05 +02:00
|
|
|
return irc.isChannel(irc.stripChannelPrefix(s))
|
2019-08-24 14:14:33 +02:00
|
|
|
if to is not None and isPublic(to):
|
2009-02-03 00:42:04 +01:00
|
|
|
target = to
|
2019-08-24 14:14:33 +02:00
|
|
|
if isPublic(target):
|
2019-08-24 17:50:05 +02:00
|
|
|
channel = irc.stripChannelPrefix(target)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
channel = None
|
|
|
|
if notice is None:
|
2019-12-05 21:11:01 +01:00
|
|
|
notice = conf.get(conf.supybot.reply.withNotice,
|
|
|
|
channel=channel, network=irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
if private is None:
|
2019-12-05 21:11:01 +01:00
|
|
|
private = conf.get(conf.supybot.reply.inPrivate,
|
|
|
|
channel=channel, network=irc.network)
|
2005-06-01 23:08:30 +02:00
|
|
|
if prefixNick is None:
|
2019-12-05 21:11:01 +01:00
|
|
|
prefixNick = conf.get(conf.supybot.reply.withNickPrefix,
|
|
|
|
channel=channel, network=irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
if error:
|
2019-12-05 21:11:01 +01:00
|
|
|
notice =conf.get(conf.supybot.reply.error.withNotice,
|
|
|
|
channel=channel, network=irc.network) or notice
|
|
|
|
private=conf.get(conf.supybot.reply.error.inPrivate,
|
|
|
|
channel=channel, network=irc.network) or private
|
2010-10-20 18:27:58 +02:00
|
|
|
s = _('Error: ') + s
|
2005-01-19 14:14:38 +01:00
|
|
|
if private:
|
2005-06-01 23:08:30 +02:00
|
|
|
prefixNick = False
|
2005-01-19 14:14:38 +01:00
|
|
|
if to is None:
|
|
|
|
target = msg.nick
|
|
|
|
else:
|
|
|
|
target = to
|
2008-11-09 18:44:56 +01:00
|
|
|
if action:
|
|
|
|
prefixNick = False
|
2005-01-19 14:14:38 +01:00
|
|
|
if to is None:
|
|
|
|
to = msg.nick
|
2016-04-24 21:11:34 +02:00
|
|
|
if stripCtcp:
|
|
|
|
s = s.strip('\x01')
|
2005-01-19 14:14:38 +01:00
|
|
|
# Ok, now let's make the payload:
|
|
|
|
s = ircutils.safeArgument(s)
|
|
|
|
if not s and not action:
|
2010-10-20 18:27:58 +02:00
|
|
|
s = _('Error: I tried to send you an empty message.')
|
2019-08-24 14:14:33 +02:00
|
|
|
if prefixNick and isPublic(target):
|
2005-01-19 14:14:38 +01:00
|
|
|
# Let's may sure we don't do, "#channel: foo.".
|
2019-08-24 14:14:33 +02:00
|
|
|
if not isPublic(to):
|
2005-01-19 14:14:38 +01:00
|
|
|
s = '%s: %s' % (to, s)
|
2019-08-24 14:14:33 +02:00
|
|
|
if not isPublic(target):
|
2005-01-19 14:14:38 +01:00
|
|
|
if conf.supybot.reply.withNoticeWhenPrivate():
|
|
|
|
notice = True
|
|
|
|
# And now, let's decide whether it's a PRIVMSG or a NOTICE.
|
|
|
|
msgmaker = ircmsgs.privmsg
|
|
|
|
if notice:
|
|
|
|
msgmaker = ircmsgs.notice
|
|
|
|
# We don't use elif here because actions can't be sent as NOTICEs.
|
|
|
|
if action:
|
|
|
|
msgmaker = ircmsgs.action
|
|
|
|
# Finally, we'll return the actual message.
|
|
|
|
ret = msgmaker(target, s)
|
|
|
|
ret.tag('inReplyTo', msg)
|
2020-05-07 21:17:55 +02:00
|
|
|
if 'msgid' in msg.server_tags \
|
|
|
|
and conf.supybot.protocols.irc.experimentalExtensions() \
|
|
|
|
and 'message-tags' in irc.state.capabilities_ack:
|
|
|
|
# In theory, msgid being in server_tags implies message-tags was
|
|
|
|
# negotiated, but the +reply spec requires it explicitly. Plus, there's
|
|
|
|
# no harm in doing this extra check, in case a plugin is replying
|
|
|
|
# across network (as it may happen with '@network command').
|
|
|
|
ret.server_tags['+draft/reply'] = msg.server_tags['msgid']
|
2005-01-19 14:14:38 +01:00
|
|
|
return ret
|
|
|
|
|
2019-08-24 14:14:33 +02:00
|
|
|
def error(*args, **kwargs):
|
|
|
|
warnings.warn('callbacks.error is deprecated. Use irc.error instead.',
|
|
|
|
DeprecationWarning)
|
|
|
|
return _makeErrorReply(dynamic.irc, *args, **kwargs)
|
|
|
|
|
|
|
|
def _makeErrorReply(irc, msg, s, **kwargs):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""Makes an error reply to msg with the appropriate error payload."""
|
|
|
|
kwargs['error'] = True
|
|
|
|
msg.tag('isError')
|
2019-08-24 14:14:33 +02:00
|
|
|
return _makeReply(irc, msg, s, **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2006-08-28 15:13:43 +02:00
|
|
|
def getHelp(method, name=None, doc=None):
|
2005-01-19 14:14:38 +01:00
|
|
|
if name is None:
|
|
|
|
name = method.__name__
|
2006-08-28 15:13:43 +02:00
|
|
|
if doc is None:
|
2013-08-24 04:57:13 +02:00
|
|
|
if method.__doc__ is None:
|
|
|
|
doclines = ['This command has no help. Complain to the author.']
|
|
|
|
else:
|
|
|
|
doclines = method.__doc__.splitlines()
|
2006-08-28 15:13:43 +02:00
|
|
|
else:
|
|
|
|
doclines = doc.splitlines()
|
2005-01-19 14:14:38 +01:00
|
|
|
s = '%s %s' % (name, doclines.pop(0))
|
|
|
|
if doclines:
|
|
|
|
help = ' '.join(doclines)
|
|
|
|
s = '(%s) -- %s' % (ircutils.bold(s), help)
|
2005-01-27 07:59:08 +01:00
|
|
|
return utils.str.normalizeWhitespace(s)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2008-12-22 03:18:56 +01:00
|
|
|
def getSyntax(method, name=None, doc=None):
|
2005-01-19 14:14:38 +01:00
|
|
|
if name is None:
|
|
|
|
name = method.__name__
|
2008-12-22 03:18:56 +01:00
|
|
|
if doc is None:
|
|
|
|
doclines = method.__doc__.splitlines()
|
|
|
|
else:
|
|
|
|
doclines = doc.splitlines()
|
2005-01-19 14:14:38 +01:00
|
|
|
return '%s %s' % (name, doclines[0])
|
|
|
|
|
|
|
|
class Error(Exception):
|
|
|
|
"""Generic class for errors in Privmsg callbacks."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ArgumentError(Error):
|
|
|
|
"""The bot replies with a help message when this is raised."""
|
|
|
|
pass
|
|
|
|
|
2012-06-09 18:54:10 +02:00
|
|
|
class SilentError(Error):
|
|
|
|
"""An error that we should not notify the user."""
|
|
|
|
pass
|
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
class Tokenizer(object):
|
|
|
|
# This will be used as a global environment to evaluate strings in.
|
2009-08-16 23:17:05 +02:00
|
|
|
# Evaluation is, of course, necessary in order to allow escaped
|
2005-01-19 14:14:38 +01:00
|
|
|
# characters to be properly handled.
|
|
|
|
#
|
|
|
|
# These are the characters valid in a token. Everything printable except
|
|
|
|
# double-quote, left-bracket, and right-bracket.
|
2012-08-04 11:38:12 +02:00
|
|
|
separators = '\x00\r\n \t'
|
2005-01-19 14:14:38 +01:00
|
|
|
def __init__(self, brackets='', pipe=False, quotes='"'):
|
|
|
|
if brackets:
|
2012-08-04 11:38:12 +02:00
|
|
|
self.separators += brackets
|
2005-01-19 14:14:38 +01:00
|
|
|
self.left = brackets[0]
|
|
|
|
self.right = brackets[1]
|
|
|
|
else:
|
|
|
|
self.left = ''
|
|
|
|
self.right = ''
|
|
|
|
self.pipe = pipe
|
|
|
|
if self.pipe:
|
2012-08-04 11:38:12 +02:00
|
|
|
self.separators += '|'
|
2005-01-19 14:14:38 +01:00
|
|
|
self.quotes = quotes
|
2012-08-04 11:38:12 +02:00
|
|
|
self.separators += quotes
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _handleToken(self, token):
|
|
|
|
if token[0] == token[-1] and token[0] in self.quotes:
|
|
|
|
token = token[1:-1]
|
2013-01-31 19:24:05 +01:00
|
|
|
# FIXME: No need to tell you this is a hack.
|
2013-01-31 20:47:57 +01:00
|
|
|
# It has to handle both IRC commands and serialized configuration.
|
2013-01-22 21:02:04 +01:00
|
|
|
#
|
|
|
|
# Whoever you are, if you make a single modification to this
|
|
|
|
# code, TEST the code with Python 2 & 3, both with the unit
|
|
|
|
# tests and on IRC with this: @echo "好"
|
2015-08-09 00:23:03 +02:00
|
|
|
if minisix.PY2:
|
2012-11-07 19:20:26 +01:00
|
|
|
try:
|
2013-01-22 20:35:11 +01:00
|
|
|
token = token.encode('utf8').decode('string_escape')
|
2013-01-22 21:02:04 +01:00
|
|
|
token = token.decode('utf8')
|
2012-11-07 19:20:26 +01:00
|
|
|
except:
|
2013-01-22 20:35:11 +01:00
|
|
|
token = token.decode('string_escape')
|
|
|
|
else:
|
|
|
|
token = codecs.getencoder('utf8')(token)[0]
|
|
|
|
token = codecs.getdecoder('unicode_escape')(token)[0]
|
2013-01-23 15:48:24 +01:00
|
|
|
try:
|
|
|
|
token = token.encode('iso-8859-1').decode()
|
|
|
|
except: # Prevent issue with tokens like '"\\x80"'.
|
|
|
|
pass
|
2005-01-19 14:14:38 +01:00
|
|
|
return token
|
|
|
|
|
|
|
|
def _insideBrackets(self, lexer):
|
|
|
|
ret = []
|
|
|
|
while True:
|
|
|
|
token = lexer.get_token()
|
|
|
|
if not token:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise SyntaxError(_('Missing "%s". You may want to '
|
2010-10-20 18:27:58 +02:00
|
|
|
'quote your arguments with double '
|
|
|
|
'quotes in order to prevent extra '
|
|
|
|
'brackets from being evaluated '
|
2014-01-20 15:43:55 +01:00
|
|
|
'as nested commands.') % self.right)
|
2005-01-19 14:14:38 +01:00
|
|
|
elif token == self.right:
|
|
|
|
return ret
|
|
|
|
elif token == self.left:
|
|
|
|
ret.append(self._insideBrackets(lexer))
|
|
|
|
else:
|
|
|
|
ret.append(self._handleToken(token))
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def tokenize(self, s):
|
2015-08-10 17:55:25 +02:00
|
|
|
lexer = shlex.shlex(minisix.io.StringIO(s))
|
2005-01-19 14:14:38 +01:00
|
|
|
lexer.commenters = ''
|
|
|
|
lexer.quotes = self.quotes
|
2012-08-04 11:38:12 +02:00
|
|
|
lexer.separators = self.separators
|
2005-01-19 14:14:38 +01:00
|
|
|
args = []
|
|
|
|
ends = []
|
|
|
|
while True:
|
|
|
|
token = lexer.get_token()
|
|
|
|
if not token:
|
|
|
|
break
|
|
|
|
elif token == '|' and self.pipe:
|
|
|
|
# The "and self.pipe" might seem redundant here, but it's there
|
|
|
|
# for strings like 'foo | bar', where a pipe stands alone as a
|
|
|
|
# token, but shouldn't be treated specially.
|
|
|
|
if not args:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise SyntaxError(_('"|" with nothing preceding. I '
|
2010-10-20 18:27:58 +02:00
|
|
|
'obviously can\'t do a pipe with '
|
2014-01-20 15:43:55 +01:00
|
|
|
'nothing before the |.'))
|
2005-01-19 14:14:38 +01:00
|
|
|
ends.append(args)
|
|
|
|
args = []
|
|
|
|
elif token == self.left:
|
|
|
|
args.append(self._insideBrackets(lexer))
|
|
|
|
elif token == self.right:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise SyntaxError(_('Spurious "%s". You may want to '
|
2010-10-20 18:27:58 +02:00
|
|
|
'quote your arguments with double '
|
|
|
|
'quotes in order to prevent extra '
|
|
|
|
'brackets from being evaluated '
|
2014-01-20 15:43:55 +01:00
|
|
|
'as nested commands.') % self.right)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
args.append(self._handleToken(token))
|
|
|
|
if ends:
|
|
|
|
if not args:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise SyntaxError(_('"|" with nothing following. I '
|
2010-10-20 18:27:58 +02:00
|
|
|
'obviously can\'t do a pipe with '
|
2014-01-20 15:43:55 +01:00
|
|
|
'nothing after the |.'))
|
2005-01-19 14:14:38 +01:00
|
|
|
args.append(ends.pop())
|
|
|
|
while ends:
|
|
|
|
args[-1].append(ends.pop())
|
|
|
|
return args
|
|
|
|
|
2019-08-24 17:50:05 +02:00
|
|
|
def tokenize(s, channel=None, network=None):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""A utility function to create a Tokenizer and tokenize a string."""
|
|
|
|
pipe = False
|
|
|
|
brackets = ''
|
|
|
|
nested = conf.supybot.commands.nested
|
|
|
|
if nested():
|
2019-08-24 17:50:05 +02:00
|
|
|
brackets = nested.brackets.getSpecific(network, channel)()
|
2019-12-05 21:11:01 +01:00
|
|
|
if conf.get(nested.pipeSyntax,
|
|
|
|
channel=channel, network=network): # No nesting, no pipe.
|
2005-01-19 14:14:38 +01:00
|
|
|
pipe = True
|
2019-08-24 17:50:05 +02:00
|
|
|
quotes = conf.supybot.commands.quotes.getSpecific(network, channel)()
|
2005-01-19 14:14:38 +01:00
|
|
|
try:
|
|
|
|
ret = Tokenizer(brackets=brackets,pipe=pipe,quotes=quotes).tokenize(s)
|
|
|
|
return ret
|
2014-01-20 15:49:15 +01:00
|
|
|
except ValueError as e:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise SyntaxError(str(e))
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2005-02-18 06:17:23 +01:00
|
|
|
def formatCommand(command):
|
|
|
|
return ' '.join(command)
|
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
def checkCommandCapability(msg, cb, commandName):
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
plugin = cb.name().lower()
|
2015-09-23 12:00:18 +02:00
|
|
|
if not isinstance(commandName, minisix.string_types):
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
assert commandName[0] == plugin, ('checkCommandCapability no longer '
|
|
|
|
'accepts command names that do not start with the callback\'s '
|
|
|
|
'name (%s): %s') % (plugin, commandName)
|
2015-09-23 12:00:18 +02:00
|
|
|
commandName = '.'.join(commandName)
|
2005-01-19 14:14:38 +01:00
|
|
|
def checkCapability(capability):
|
|
|
|
assert ircdb.isAntiCapability(capability)
|
|
|
|
if ircdb.checkCapability(msg.prefix, capability):
|
|
|
|
log.info('Preventing %s from calling %s because of %s.',
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
msg.prefix, commandName, capability)
|
2014-01-20 15:43:55 +01:00
|
|
|
raise RuntimeError(capability)
|
2005-01-19 14:14:38 +01:00
|
|
|
try:
|
|
|
|
antiCommand = ircdb.makeAntiCapability(commandName)
|
|
|
|
checkCapability(antiCommand)
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
checkAtEnd = [commandName]
|
2005-01-19 14:14:38 +01:00
|
|
|
default = conf.supybot.capabilities.default()
|
2019-08-24 14:14:33 +02:00
|
|
|
if msg.channel:
|
|
|
|
channel = msg.channel
|
2005-01-19 14:14:38 +01:00
|
|
|
checkCapability(ircdb.makeChannelCapability(channel, antiCommand))
|
|
|
|
chanCommand = ircdb.makeChannelCapability(channel, commandName)
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
checkAtEnd += [chanCommand]
|
2005-01-19 14:14:38 +01:00
|
|
|
default &= ircdb.channels.getChannel(channel).defaultAllow
|
|
|
|
return not (default or \
|
|
|
|
any(lambda x: ircdb.checkCapability(msg.prefix, x),
|
|
|
|
checkAtEnd))
|
2014-01-20 15:49:15 +01:00
|
|
|
except RuntimeError as e:
|
2005-01-19 14:14:38 +01:00
|
|
|
s = ircdb.unAntiCapability(str(e))
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
class RichReplyMethods(object):
|
|
|
|
"""This is a mixin so these replies need only be defined once. It operates
|
|
|
|
under several assumptions, including the fact that 'self' is an Irc object
|
|
|
|
of some sort and there is a self.msg that is an IrcMsg."""
|
|
|
|
def __makeReply(self, prefix, s):
|
|
|
|
if s:
|
|
|
|
s = '%s %s' % (prefix, s)
|
|
|
|
else:
|
|
|
|
s = prefix
|
|
|
|
return ircutils.standardSubstitute(self, self.msg, s)
|
|
|
|
|
|
|
|
def _getConfig(self, wrapper):
|
2019-12-05 21:11:01 +01:00
|
|
|
return conf.get(wrapper,
|
|
|
|
channel=self.msg.channel, network=self.irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def replySuccess(self, s='', **kwargs):
|
|
|
|
v = self._getConfig(conf.supybot.replies.success)
|
|
|
|
if v:
|
|
|
|
s = self.__makeReply(v, s)
|
|
|
|
return self.reply(s, **kwargs)
|
|
|
|
else:
|
|
|
|
self.noReply()
|
|
|
|
|
|
|
|
def replyError(self, s='', **kwargs):
|
|
|
|
v = self._getConfig(conf.supybot.replies.error)
|
2011-06-22 21:37:34 +02:00
|
|
|
if 'msg' in kwargs:
|
|
|
|
msg = kwargs['msg']
|
|
|
|
if ircdb.checkCapability(msg.prefix, 'owner'):
|
|
|
|
v = self._getConfig(conf.supybot.replies.errorOwner)
|
2005-01-19 14:14:38 +01:00
|
|
|
s = self.__makeReply(v, s)
|
|
|
|
return self.reply(s, **kwargs)
|
|
|
|
|
2016-10-01 13:37:03 +02:00
|
|
|
def _getTarget(self, to=None):
|
|
|
|
"""Compute the target according to self.to, the provided to,
|
|
|
|
and self.private, and return it. Mainly used by reply methods."""
|
|
|
|
# FIXME: Don't set self.to.
|
|
|
|
# I still set it to be sure I don't introduce a regression,
|
|
|
|
# but it does not make sense for .reply() and .replies() to
|
|
|
|
# change the state of this Irc object.
|
|
|
|
if to is not None:
|
|
|
|
self.to = self.to or to
|
|
|
|
target = self.private and self.to or self.msg.args[0]
|
|
|
|
return target
|
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
def replies(self, L, prefixer=None, joiner=None,
|
2016-10-01 13:37:03 +02:00
|
|
|
onlyPrefixFirst=False,
|
2013-04-10 17:26:55 +02:00
|
|
|
oneToOne=None, **kwargs):
|
2005-01-19 14:14:38 +01:00
|
|
|
if prefixer is None:
|
|
|
|
prefixer = ''
|
|
|
|
if joiner is None:
|
2005-01-27 07:59:08 +01:00
|
|
|
joiner = utils.str.commaAndify
|
2015-08-10 20:24:11 +02:00
|
|
|
if isinstance(prefixer, minisix.string_types):
|
2005-01-19 14:14:38 +01:00
|
|
|
prefixer = prefixer.__add__
|
2015-08-10 20:24:11 +02:00
|
|
|
if isinstance(joiner, minisix.string_types):
|
2005-01-19 14:14:38 +01:00
|
|
|
joiner = joiner.join
|
2016-10-01 13:37:03 +02:00
|
|
|
to = self._getTarget(kwargs.get('to'))
|
2013-04-10 17:26:55 +02:00
|
|
|
if oneToOne is None: # Can be True, False, or None
|
2019-08-04 18:11:28 +02:00
|
|
|
if self.irc.isChannel(to):
|
2019-12-05 21:11:01 +01:00
|
|
|
oneToOne = conf.get(conf.supybot.reply.oneToOne,
|
|
|
|
channel=to, network=self.irc.network)
|
2013-04-10 17:26:55 +02:00
|
|
|
else:
|
|
|
|
oneToOne = conf.supybot.reply.oneToOne()
|
2012-08-10 14:27:25 +02:00
|
|
|
if oneToOne:
|
2016-10-01 13:37:03 +02:00
|
|
|
return self.reply(prefixer(joiner(L)), **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
msg = None
|
|
|
|
first = True
|
|
|
|
for s in L:
|
|
|
|
if onlyPrefixFirst:
|
|
|
|
if first:
|
|
|
|
first = False
|
2016-10-01 13:37:03 +02:00
|
|
|
msg = self.reply(prefixer(s), **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
2016-10-01 13:37:03 +02:00
|
|
|
msg = self.reply(s, **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
2016-10-01 13:37:03 +02:00
|
|
|
msg = self.reply(prefixer(s), **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
return msg
|
|
|
|
|
2017-10-21 15:37:43 +02:00
|
|
|
def noReply(self, msg=None):
|
2005-06-08 19:45:58 +02:00
|
|
|
self.repliedTo = True
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def _error(self, s, Raise=False, **kwargs):
|
|
|
|
if Raise:
|
2014-01-20 15:43:55 +01:00
|
|
|
raise Error(s)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
return self.error(s, **kwargs)
|
|
|
|
|
|
|
|
def errorNoCapability(self, capability, s='', **kwargs):
|
|
|
|
if 'Raise' not in kwargs:
|
|
|
|
kwargs['Raise'] = True
|
2015-09-23 12:00:18 +02:00
|
|
|
log.warning('Denying %s for lacking %q capability.',
|
|
|
|
self.msg.prefix, capability)
|
|
|
|
# noCapability means "don't send a specific capability error
|
|
|
|
# message" not "don't send a capability error message at all", like
|
|
|
|
# one would think
|
|
|
|
if self._getConfig(conf.supybot.reply.error.noCapability) or \
|
|
|
|
capability in conf.supybot.capabilities.private():
|
2005-01-19 14:14:38 +01:00
|
|
|
v = self._getConfig(conf.supybot.replies.genericNoCapability)
|
2015-09-23 12:00:18 +02:00
|
|
|
else:
|
|
|
|
v = self._getConfig(conf.supybot.replies.noCapability)
|
|
|
|
try:
|
|
|
|
v %= capability
|
|
|
|
except TypeError: # No %s in string
|
|
|
|
pass
|
|
|
|
s = self.__makeReply(v, s)
|
|
|
|
if s:
|
|
|
|
return self._error(s, **kwargs)
|
2018-10-06 08:13:06 +02:00
|
|
|
elif kwargs['Raise']:
|
|
|
|
raise Error()
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def errorPossibleBug(self, s='', **kwargs):
|
|
|
|
v = self._getConfig(conf.supybot.replies.possibleBug)
|
|
|
|
if s:
|
|
|
|
s += ' (%s)' % v
|
|
|
|
else:
|
|
|
|
s = v
|
|
|
|
return self._error(s, **kwargs)
|
|
|
|
|
|
|
|
def errorNotRegistered(self, s='', **kwargs):
|
|
|
|
v = self._getConfig(conf.supybot.replies.notRegistered)
|
|
|
|
return self._error(self.__makeReply(v, s), **kwargs)
|
|
|
|
|
|
|
|
def errorNoUser(self, s='', name='that user', **kwargs):
|
|
|
|
if 'Raise' not in kwargs:
|
|
|
|
kwargs['Raise'] = True
|
|
|
|
v = self._getConfig(conf.supybot.replies.noUser)
|
|
|
|
try:
|
|
|
|
v = v % name
|
|
|
|
except TypeError:
|
|
|
|
log.warning('supybot.replies.noUser should have one "%s" in it.')
|
|
|
|
return self._error(self.__makeReply(v, s), **kwargs)
|
|
|
|
|
|
|
|
def errorRequiresPrivacy(self, s='', **kwargs):
|
|
|
|
v = self._getConfig(conf.supybot.replies.requiresPrivacy)
|
|
|
|
return self._error(self.__makeReply(v, s), **kwargs)
|
|
|
|
|
|
|
|
def errorInvalid(self, what, given=None, s='', repr=True, **kwargs):
|
|
|
|
if given is not None:
|
|
|
|
if repr:
|
|
|
|
given = _repr(given)
|
|
|
|
else:
|
|
|
|
given = '"%s"' % given
|
2010-10-20 18:27:58 +02:00
|
|
|
v = _('%s is not a valid %s.') % (given, what)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
2010-10-20 18:27:58 +02:00
|
|
|
v = _('That\'s not a valid %s.') % what
|
2005-01-19 14:14:38 +01:00
|
|
|
if 'Raise' not in kwargs:
|
|
|
|
kwargs['Raise'] = True
|
2015-12-28 20:01:17 +01:00
|
|
|
if s:
|
|
|
|
v += ' ' + s
|
|
|
|
return self._error(v, **kwargs)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
_repr = repr
|
|
|
|
|
2005-04-13 05:18:17 +02:00
|
|
|
class ReplyIrcProxy(RichReplyMethods):
|
2005-03-28 15:00:37 +02:00
|
|
|
"""This class is a thin wrapper around an irclib.Irc object that gives it
|
|
|
|
the reply() and error() methods (as well as everything in RichReplyMethods,
|
|
|
|
based on those two)."""
|
|
|
|
def __init__(self, irc, msg):
|
|
|
|
self.irc = irc
|
|
|
|
self.msg = msg
|
2019-09-08 21:35:35 +02:00
|
|
|
self.getRealIrc()._setMsgChannel(self.msg)
|
2005-03-28 15:00:37 +02:00
|
|
|
|
|
|
|
def getRealIrc(self):
|
2009-08-14 17:22:23 +02:00
|
|
|
"""Returns the real irclib.Irc object underlying this proxy chain."""
|
2005-03-28 15:00:37 +02:00
|
|
|
if isinstance(self.irc, irclib.Irc):
|
|
|
|
return self.irc
|
|
|
|
else:
|
|
|
|
return self.irc.getRealIrc()
|
|
|
|
|
|
|
|
# This should make us be considered equal to our irclib.Irc object for
|
|
|
|
# hashing; an important thing (no more "too many open files" exceptions :))
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.getRealIrc())
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.getRealIrc() == other
|
|
|
|
__req__ = __eq__
|
|
|
|
def __ne__(self, other):
|
|
|
|
return not (self == other)
|
|
|
|
__rne__ = __ne__
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-03-28 15:00:37 +02:00
|
|
|
def error(self, s, msg=None, **kwargs):
|
|
|
|
if 'Raise' in kwargs and kwargs['Raise']:
|
Make irc.error do nothing is no string is given as argument.
Fixes c1d3bad64feca2529e29473f0ed8c622ad1937b1, which crashed with:
```
ERROR 2020-01-18T01:34:07 Uncaught exception in NickAuth._callCommand:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1337, in _callCommand
irc.errorNoCapability(cap)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 544, in errorNoCapability
raise Error()
supybot.callbacks.Error
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/log.py", line 368, in m
return f(self, *args, **kwargs)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1359, in _callCommand
irc.error(str(e))
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1065, in error
raise ArgumentError
supybot.callbacks.ArgumentError
```
2020-01-18 11:04:50 +01:00
|
|
|
raise Error()
|
2005-03-28 15:00:37 +02:00
|
|
|
if msg is None:
|
|
|
|
msg = self.msg
|
Make irc.error do nothing is no string is given as argument.
Fixes c1d3bad64feca2529e29473f0ed8c622ad1937b1, which crashed with:
```
ERROR 2020-01-18T01:34:07 Uncaught exception in NickAuth._callCommand:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1337, in _callCommand
irc.errorNoCapability(cap)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 544, in errorNoCapability
raise Error()
supybot.callbacks.Error
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/log.py", line 368, in m
return f(self, *args, **kwargs)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1359, in _callCommand
irc.error(str(e))
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1065, in error
raise ArgumentError
supybot.callbacks.ArgumentError
```
2020-01-18 11:04:50 +01:00
|
|
|
if s:
|
|
|
|
m = _makeErrorReply(self, msg, s, **kwargs)
|
|
|
|
self.irc.queueMsg(m)
|
|
|
|
return m
|
2005-03-28 15:00:37 +02:00
|
|
|
|
|
|
|
def reply(self, s, msg=None, **kwargs):
|
|
|
|
if msg is None:
|
|
|
|
msg = self.msg
|
|
|
|
assert not isinstance(s, ircmsgs.IrcMsg), \
|
|
|
|
'Old code alert: there is no longer a "msg" argument to reply.'
|
|
|
|
kwargs.pop('noLengthCheck', None)
|
2019-08-24 14:14:33 +02:00
|
|
|
m = _makeReply(self, msg, s, **kwargs)
|
2005-03-28 15:00:37 +02:00
|
|
|
self.irc.queueMsg(m)
|
|
|
|
return m
|
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
return getattr(self.irc, attr)
|
|
|
|
|
2005-04-13 05:18:17 +02:00
|
|
|
SimpleProxy = ReplyIrcProxy # Backwards-compatibility
|
2005-03-28 15:00:37 +02:00
|
|
|
|
2005-04-13 05:18:17 +02:00
|
|
|
class NestedCommandsIrcProxy(ReplyIrcProxy):
|
2009-08-14 17:22:23 +02:00
|
|
|
"A proxy object to allow proper nesting of commands (even threaded ones)."
|
2005-02-07 07:28:52 +01:00
|
|
|
_mores = ircutils.IrcDict()
|
2005-01-19 14:14:38 +01:00
|
|
|
def __init__(self, irc, msg, args, nested=0):
|
|
|
|
assert isinstance(args, list), 'Args should be a list, not a string.'
|
2019-09-08 21:35:35 +02:00
|
|
|
super(NestedCommandsIrcProxy, self).__init__(irc, msg)
|
2005-01-19 14:14:38 +01:00
|
|
|
self.nested = nested
|
2005-06-08 19:45:58 +02:00
|
|
|
self.repliedTo = False
|
2005-01-19 14:14:38 +01:00
|
|
|
if not self.nested and isinstance(irc, self.__class__):
|
2005-04-13 05:18:17 +02:00
|
|
|
# This means we were given an NestedCommandsIrcProxy instead of an
|
2005-02-18 00:26:11 +01:00
|
|
|
# irclib.Irc, and so we're obviously nested. But nested wasn't
|
|
|
|
# set! So we take our given Irc's nested value.
|
2005-01-19 14:14:38 +01:00
|
|
|
self.nested += irc.nested
|
|
|
|
maxNesting = conf.supybot.commands.nested.maximum()
|
|
|
|
if maxNesting and self.nested > maxNesting:
|
|
|
|
log.warning('%s attempted more than %s levels of nesting.',
|
|
|
|
self.msg.prefix, maxNesting)
|
2013-08-24 11:28:29 +02:00
|
|
|
self.error(_('You\'ve attempted more nesting than is '
|
2010-10-20 18:27:58 +02:00
|
|
|
'currently allowed on this bot.'))
|
2014-06-30 01:12:22 +02:00
|
|
|
return
|
2005-01-19 14:14:38 +01:00
|
|
|
# The deepcopy here is necessary for Scheduler; it re-runs already
|
2005-02-18 00:26:11 +01:00
|
|
|
# tokenized commands. There's a possibility a simple copy[:] would
|
|
|
|
# work, but we're being careful.
|
2005-01-19 14:14:38 +01:00
|
|
|
self.args = copy.deepcopy(args)
|
|
|
|
self.counter = 0
|
|
|
|
self._resetReplyAttributes()
|
|
|
|
if not args:
|
|
|
|
self.finalEvaled = True
|
|
|
|
self._callInvalidCommands()
|
|
|
|
else:
|
|
|
|
self.finalEvaled = False
|
|
|
|
world.commandsProcessed += 1
|
|
|
|
self.evalArgs()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return other == self.getRealIrc()
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.getRealIrc())
|
|
|
|
|
|
|
|
def _resetReplyAttributes(self):
|
|
|
|
self.to = None
|
|
|
|
self.action = None
|
|
|
|
self.notice = None
|
|
|
|
self.private = None
|
|
|
|
self.noLengthCheck = None
|
2019-08-24 14:14:33 +02:00
|
|
|
if self.msg.channel:
|
2005-06-01 23:08:30 +02:00
|
|
|
self.prefixNick = conf.get(conf.supybot.reply.withNickPrefix,
|
2019-12-05 21:11:01 +01:00
|
|
|
channel=self.msg.channel, network=self.irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
2005-06-01 23:08:30 +02:00
|
|
|
self.prefixNick = conf.supybot.reply.withNickPrefix()
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2015-11-14 19:20:18 +01:00
|
|
|
def evalArgs(self, withClass=None):
|
2005-01-19 14:14:38 +01:00
|
|
|
while self.counter < len(self.args):
|
2005-06-08 19:45:58 +02:00
|
|
|
self.repliedTo = False
|
2015-08-10 20:24:11 +02:00
|
|
|
if isinstance(self.args[self.counter], minisix.string_types):
|
2005-02-18 00:26:11 +01:00
|
|
|
# If it's a string, just go to the next arg. There is no
|
|
|
|
# evaluation to be done for strings. If, at some point,
|
|
|
|
# we decided to, say, convert every string using
|
|
|
|
# ircutils.standardSubstitute, this would be where we would
|
|
|
|
# probably put it.
|
2005-01-19 14:14:38 +01:00
|
|
|
self.counter += 1
|
|
|
|
else:
|
2005-02-18 00:26:11 +01:00
|
|
|
assert isinstance(self.args[self.counter], list)
|
2005-04-13 05:18:17 +02:00
|
|
|
# It's a list. So we spawn another NestedCommandsIrcProxy
|
2005-02-18 00:26:11 +01:00
|
|
|
# to evaluate its args. When that class has finished
|
|
|
|
# evaluating its args, it will call our reply method, which
|
|
|
|
# will subsequently call this function again, and we'll
|
|
|
|
# pick up where we left off via self.counter.
|
2015-11-14 19:20:18 +01:00
|
|
|
cls = withClass or self.__class__
|
|
|
|
cls(self, self.msg, self.args[self.counter],
|
|
|
|
nested=self.nested+1)
|
2005-04-13 05:18:17 +02:00
|
|
|
# We have to return here because the new NestedCommandsIrcProxy
|
2005-02-18 00:26:11 +01:00
|
|
|
# might not have called our reply method instantly, since
|
|
|
|
# its command might be threaded. So (obviously) we can't
|
|
|
|
# just fall through to self.finalEval.
|
2005-01-19 14:14:38 +01:00
|
|
|
return
|
2005-02-18 00:26:11 +01:00
|
|
|
# Once all the list args are evaluated, we then evaluate our own
|
|
|
|
# list of args, since we're assured that they're all strings now.
|
2015-08-10 20:24:11 +02:00
|
|
|
assert all(lambda x: isinstance(x, minisix.string_types), self.args)
|
2005-01-19 14:14:38 +01:00
|
|
|
self.finalEval()
|
|
|
|
|
|
|
|
def _callInvalidCommands(self):
|
|
|
|
log.debug('Calling invalidCommands.')
|
2005-04-13 05:20:58 +02:00
|
|
|
threaded = False
|
|
|
|
cbs = []
|
2005-01-19 14:14:38 +01:00
|
|
|
for cb in self.irc.callbacks:
|
|
|
|
if hasattr(cb, 'invalidCommand'):
|
2005-04-13 05:20:58 +02:00
|
|
|
cbs.append(cb)
|
|
|
|
threaded = threaded or cb.threaded
|
|
|
|
def callInvalidCommands():
|
2005-06-08 19:49:54 +02:00
|
|
|
self.repliedTo = False
|
2005-04-13 05:20:58 +02:00
|
|
|
for cb in cbs:
|
2005-06-08 19:45:58 +02:00
|
|
|
log.debug('Calling %s.invalidCommand.', cb.name())
|
|
|
|
try:
|
|
|
|
cb.invalidCommand(self, self.msg, self.args)
|
2014-01-20 15:49:15 +01:00
|
|
|
except Error as e:
|
2005-06-08 19:45:58 +02:00
|
|
|
self.error(str(e))
|
2014-01-20 15:49:15 +01:00
|
|
|
except Exception as e:
|
2005-06-08 19:45:58 +02:00
|
|
|
log.exception('Uncaught exception in %s.invalidCommand.',
|
|
|
|
cb.name())
|
|
|
|
log.debug('Finished calling %s.invalidCommand.', cb.name())
|
|
|
|
if self.repliedTo:
|
2005-01-19 14:14:38 +01:00
|
|
|
log.debug('Done calling invalidCommands: %s.',cb.name())
|
|
|
|
return
|
2005-04-13 05:20:58 +02:00
|
|
|
if threaded:
|
|
|
|
name = 'Thread #%s (for invalidCommands)' % world.threadsSpawned
|
2005-04-21 01:06:50 +02:00
|
|
|
t = world.SupyThread(target=callInvalidCommands, name=name)
|
2005-04-13 05:20:58 +02:00
|
|
|
t.setDaemon(True)
|
|
|
|
t.start()
|
|
|
|
else:
|
|
|
|
callInvalidCommands()
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2005-02-18 06:17:23 +01:00
|
|
|
def findCallbacksForArgs(self, args):
|
|
|
|
"""Returns a two-tuple of (command, plugins) that has the command
|
|
|
|
(a list of strings) and the plugins for which it was a command."""
|
2005-02-18 09:26:02 +01:00
|
|
|
assert isinstance(args, list)
|
2014-01-21 10:57:38 +01:00
|
|
|
args = list(map(canonicalName, args))
|
2005-02-18 06:17:23 +01:00
|
|
|
cbs = []
|
|
|
|
maxL = []
|
|
|
|
for cb in self.irc.callbacks:
|
2005-02-19 00:01:41 +01:00
|
|
|
if not hasattr(cb, 'getCommand'):
|
|
|
|
continue
|
2005-02-18 06:17:23 +01:00
|
|
|
L = cb.getCommand(args)
|
2005-07-01 00:04:59 +02:00
|
|
|
#log.debug('%s.getCommand(%r) returned %r', cb.name(), args, L)
|
2005-02-18 06:17:23 +01:00
|
|
|
if L and L >= maxL:
|
|
|
|
maxL = L
|
|
|
|
cbs.append((cb, L))
|
|
|
|
assert isinstance(L, list), \
|
2005-02-24 13:49:08 +01:00
|
|
|
'getCommand now returns a list, not a method.'
|
2005-02-18 06:17:23 +01:00
|
|
|
assert utils.iter.startswith(L, args), \
|
2005-02-24 13:49:08 +01:00
|
|
|
'getCommand must return a prefix of the args given. ' \
|
|
|
|
'(args given: %r, returned: %r)' % (args, L)
|
2006-05-01 19:08:44 +02:00
|
|
|
log.debug('findCallbacksForArgs: %r', cbs)
|
2005-02-18 06:17:23 +01:00
|
|
|
cbs = [cb for (cb, L) in cbs if L == maxL]
|
|
|
|
if len(maxL) == 1:
|
|
|
|
# Special case: one arg determines the callback. In this case, we
|
2005-05-23 13:23:53 +02:00
|
|
|
# have to check, in order:
|
|
|
|
# 1. Whether the arg is the same as the name of a callback. This
|
|
|
|
# callback would then win.
|
|
|
|
for cb in cbs:
|
|
|
|
if cb.canonicalName() == maxL[0]:
|
|
|
|
return (maxL, [cb])
|
|
|
|
|
|
|
|
# 2. Whether a defaultplugin is defined.
|
2005-02-18 06:17:23 +01:00
|
|
|
defaultPlugins = conf.supybot.commands.defaultPlugins
|
2005-01-19 14:14:38 +01:00
|
|
|
try:
|
2005-02-18 06:17:23 +01:00
|
|
|
defaultPlugin = defaultPlugins.get(maxL[0])()
|
|
|
|
log.debug('defaultPlugin: %r', defaultPlugin)
|
|
|
|
if defaultPlugin:
|
|
|
|
cb = self.irc.getCallback(defaultPlugin)
|
|
|
|
if cb in cbs:
|
|
|
|
# This is just a sanity check, but there's a small
|
|
|
|
# possibility that a default plugin for a command
|
|
|
|
# is configured to point to a plugin that doesn't
|
|
|
|
# actually have that command.
|
|
|
|
return (maxL, [cb])
|
|
|
|
except registry.NonExistentRegistryEntry:
|
2005-05-23 13:23:53 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
# 3. Whether an importantPlugin is one of the responses.
|
2005-02-18 07:30:22 +01:00
|
|
|
important = defaultPlugins.importantPlugins()
|
2014-01-21 10:57:38 +01:00
|
|
|
important = list(map(canonicalName, important))
|
2005-02-18 07:30:22 +01:00
|
|
|
importants = []
|
|
|
|
for cb in cbs:
|
|
|
|
if cb.canonicalName() in important:
|
|
|
|
importants.append(cb)
|
|
|
|
if len(importants) == 1:
|
|
|
|
return (maxL, importants)
|
2005-02-18 06:17:23 +01:00
|
|
|
return (maxL, cbs)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def finalEval(self):
|
2005-02-18 00:39:21 +01:00
|
|
|
# Now that we've already iterated through our args and made sure
|
|
|
|
# that any list of args was evaluated (by spawning another
|
2005-04-13 05:18:17 +02:00
|
|
|
# NestedCommandsIrcProxy to evaluated it into a string), we can finally
|
2005-02-18 00:39:21 +01:00
|
|
|
# evaluated our own list of arguments.
|
2005-01-19 14:14:38 +01:00
|
|
|
assert not self.finalEvaled, 'finalEval called twice.'
|
|
|
|
self.finalEvaled = True
|
2005-02-18 06:17:23 +01:00
|
|
|
# Now, the way we call a command is we iterate over the loaded pluings,
|
|
|
|
# asking each one if the list of args we have interests it. The
|
|
|
|
# way we do that is by calling getCommand on the plugin.
|
|
|
|
# The plugin will return a list of args which it considers to be
|
|
|
|
# "interesting." We will then give our args to the plugin which
|
|
|
|
# has the *longest* list. The reason we pick the longest list is
|
|
|
|
# that it seems reasonable that the longest the list, the more
|
|
|
|
# specific the command is. That is, given a list of length X, a list
|
|
|
|
# of length X+1 would be even more specific (assuming that both lists
|
|
|
|
# used the same prefix. Of course, if two plugins return a list of the
|
|
|
|
# same length, we'll just error out with a message about ambiguity.
|
|
|
|
(command, cbs) = self.findCallbacksForArgs(self.args)
|
2005-01-19 14:14:38 +01:00
|
|
|
if not cbs:
|
2005-02-18 06:17:23 +01:00
|
|
|
# We used to handle addressedRegexps here, but I think we'll let
|
|
|
|
# them handle themselves in getCommand. They can always just
|
|
|
|
# return the full list of args as their "command".
|
2005-01-19 14:14:38 +01:00
|
|
|
self._callInvalidCommands()
|
|
|
|
elif len(cbs) > 1:
|
|
|
|
names = sorted([cb.name() for cb in cbs])
|
2005-02-18 06:17:23 +01:00
|
|
|
command = formatCommand(command)
|
2010-10-20 18:27:58 +02:00
|
|
|
self.error(format(_('The command %q is available in the %L '
|
2005-02-18 06:17:23 +01:00
|
|
|
'plugins. Please specify the plugin '
|
|
|
|
'whose command you wish to call by using '
|
2010-10-20 18:27:58 +02:00
|
|
|
'its name as a command before %q.'),
|
2005-02-18 06:17:23 +01:00
|
|
|
command, names, command))
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
|
|
|
cb = cbs[0]
|
2005-02-18 06:17:23 +01:00
|
|
|
args = self.args[len(command):]
|
2005-01-19 14:14:38 +01:00
|
|
|
if world.isMainThread() and \
|
|
|
|
(cb.threaded or conf.supybot.debug.threadAllCommands()):
|
2005-02-16 03:17:05 +01:00
|
|
|
t = CommandThread(target=cb._callCommand,
|
2005-02-18 06:17:23 +01:00
|
|
|
args=(command, self, self.msg, args))
|
2005-01-19 14:14:38 +01:00
|
|
|
t.start()
|
|
|
|
else:
|
2005-02-18 06:17:23 +01:00
|
|
|
cb._callCommand(command, self, self.msg, args)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2015-01-17 07:13:30 +01:00
|
|
|
def reply(self, s, noLengthCheck=False, prefixNick=None, action=None,
|
2016-04-24 21:11:34 +02:00
|
|
|
private=None, notice=None, to=None, msg=None,
|
|
|
|
sendImmediately=False, stripCtcp=True):
|
2014-03-21 16:31:48 +01:00
|
|
|
"""
|
2005-01-19 14:14:38 +01:00
|
|
|
Keyword arguments:
|
2014-03-21 16:34:14 +01:00
|
|
|
|
2015-01-17 09:49:32 +01:00
|
|
|
* `noLengthCheck=False`: True if the length shouldn't be checked
|
|
|
|
(used for 'more' handling)
|
|
|
|
* `prefixNick=True`: False if the nick shouldn't be prefixed to the
|
|
|
|
reply.
|
|
|
|
* `action=False`: True if the reply should be an action.
|
|
|
|
* `private=False`: True if the reply should be in private.
|
|
|
|
* `notice=False`: True if the reply should be noticed when the
|
|
|
|
bot is configured to do so.
|
|
|
|
* `to=<nick|channel>`: The nick or channel the reply should go to.
|
|
|
|
Defaults to msg.args[0] (or msg.nick if private)
|
|
|
|
* `sendImmediately=False`: True if the reply should use sendMsg() which
|
|
|
|
bypasses conf.supybot.protocols.irc.throttleTime
|
|
|
|
and gets sent before any queued messages
|
2005-01-19 14:14:38 +01:00
|
|
|
"""
|
|
|
|
# These use and or or based on whether or not they default to True or
|
|
|
|
# False. Those that default to True use and; those that default to
|
|
|
|
# False use or.
|
|
|
|
assert not isinstance(s, ircmsgs.IrcMsg), \
|
|
|
|
'Old code alert: there is no longer a "msg" argument to reply.'
|
2005-06-08 19:45:58 +02:00
|
|
|
self.repliedTo = True
|
2015-01-17 09:49:32 +01:00
|
|
|
if sendImmediately:
|
2015-01-17 07:13:30 +01:00
|
|
|
sendMsg = self.irc.sendMsg
|
|
|
|
else:
|
|
|
|
sendMsg = self.irc.queueMsg
|
2005-01-19 14:14:38 +01:00
|
|
|
if msg is None:
|
|
|
|
msg = self.msg
|
2005-06-01 23:08:30 +02:00
|
|
|
if prefixNick is not None:
|
|
|
|
self.prefixNick = prefixNick
|
2005-01-19 14:14:38 +01:00
|
|
|
if action is not None:
|
|
|
|
self.action = self.action or action
|
2008-11-23 01:35:47 +01:00
|
|
|
if action:
|
|
|
|
self.prefixNick = False
|
2005-01-19 14:14:38 +01:00
|
|
|
if notice is not None:
|
|
|
|
self.notice = self.notice or notice
|
|
|
|
if private is not None:
|
|
|
|
self.private = self.private or private
|
2016-10-01 13:37:03 +02:00
|
|
|
target = self._getTarget(to)
|
2005-06-01 23:08:30 +02:00
|
|
|
# action=True implies noLengthCheck=True and prefixNick=False
|
2005-01-19 14:14:38 +01:00
|
|
|
self.noLengthCheck=noLengthCheck or self.noLengthCheck or self.action
|
2015-08-10 20:24:11 +02:00
|
|
|
if not isinstance(s, minisix.string_types): # avoid trying to str() unicode
|
2011-12-14 00:21:38 +01:00
|
|
|
s = str(s) # Allow non-string esses.
|
2020-04-11 16:18:50 +02:00
|
|
|
|
|
|
|
replyArgs = dict(
|
|
|
|
to=self.to,
|
|
|
|
notice=self.notice,
|
|
|
|
action=self.action,
|
|
|
|
private=self.private,
|
|
|
|
prefixNick=self.prefixNick,
|
|
|
|
stripCtcp=stripCtcp
|
|
|
|
)
|
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
if self.finalEvaled:
|
|
|
|
try:
|
2005-03-28 15:29:55 +02:00
|
|
|
if isinstance(self.irc, self.__class__):
|
2005-01-19 14:14:38 +01:00
|
|
|
s = s[:conf.supybot.reply.maximumLength()]
|
2020-04-11 16:18:50 +02:00
|
|
|
return self.irc.reply(s,
|
2016-04-24 21:11:34 +02:00
|
|
|
noLengthCheck=self.noLengthCheck,
|
2020-04-11 16:18:50 +02:00
|
|
|
**replyArgs)
|
2005-01-19 14:14:38 +01:00
|
|
|
elif self.noLengthCheck:
|
2005-04-13 05:18:17 +02:00
|
|
|
# noLengthCheck only matters to NestedCommandsIrcProxy, so
|
|
|
|
# it's not used here. Just in case you were wondering.
|
2020-04-11 16:18:50 +02:00
|
|
|
m = _makeReply(self, msg, s, **replyArgs)
|
2015-01-17 07:13:30 +01:00
|
|
|
sendMsg(m)
|
2005-01-19 14:14:38 +01:00
|
|
|
return m
|
|
|
|
else:
|
|
|
|
s = ircutils.safeArgument(s)
|
|
|
|
allowedLength = conf.get(conf.supybot.reply.mores.length,
|
2019-12-05 21:11:01 +01:00
|
|
|
channel=target, network=self.irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
if not allowedLength: # 0 indicates this.
|
2018-09-10 22:39:45 +02:00
|
|
|
allowedLength = (512
|
|
|
|
- len(':') - len(self.irc.prefix)
|
|
|
|
- len(' PRIVMSG ')
|
|
|
|
- len(target)
|
|
|
|
- len(' :')
|
|
|
|
- len('\r\n')
|
|
|
|
)
|
|
|
|
if self.prefixNick:
|
|
|
|
allowedLength -= len(msg.nick) + len(': ')
|
2005-01-19 14:14:38 +01:00
|
|
|
maximumMores = conf.get(conf.supybot.reply.mores.maximum,
|
2019-12-05 21:11:01 +01:00
|
|
|
channel=target, network=self.irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
maximumLength = allowedLength * maximumMores
|
|
|
|
if len(s) > maximumLength:
|
|
|
|
log.warning('Truncating to %s bytes from %s bytes.',
|
|
|
|
maximumLength, len(s))
|
|
|
|
s = s[:maximumLength]
|
2018-02-01 21:32:14 +01:00
|
|
|
s_size = len(s.encode()) if minisix.PY3 else len(s)
|
2018-02-01 22:37:24 +01:00
|
|
|
if s_size <= allowedLength or \
|
2019-12-05 21:11:01 +01:00
|
|
|
not conf.get(conf.supybot.reply.mores,
|
|
|
|
channel=target, network=self.irc.network):
|
2005-01-19 14:14:38 +01:00
|
|
|
# There's no need for action=self.action here because
|
|
|
|
# action implies noLengthCheck, which has already been
|
|
|
|
# handled. Let's stick an assert in here just in case.
|
|
|
|
assert not self.action
|
2020-04-11 16:18:50 +02:00
|
|
|
m = _makeReply(self, msg, s, **replyArgs)
|
2015-01-17 07:13:30 +01:00
|
|
|
sendMsg(m)
|
2005-01-19 14:14:38 +01:00
|
|
|
return m
|
2018-09-10 22:39:45 +02:00
|
|
|
# The '(XX more messages)' may have not the same
|
|
|
|
# length in the current locale
|
2019-01-06 16:48:04 +01:00
|
|
|
allowedLength -= len(_('(XX more messages)')) + 1 # bold
|
2020-04-11 16:40:03 +02:00
|
|
|
chunks = ircutils.wrap(s, allowedLength)
|
|
|
|
|
|
|
|
# Last messages to display at the beginning of the list
|
|
|
|
# (which is used like a stack)
|
|
|
|
chunks.reverse()
|
|
|
|
|
|
|
|
msgs = []
|
|
|
|
for (i, chunk) in enumerate(chunks):
|
|
|
|
if i == 0:
|
|
|
|
pass # last message, no suffix to add
|
|
|
|
else:
|
|
|
|
if i == 1:
|
|
|
|
more = _('more message')
|
|
|
|
else:
|
|
|
|
more = _('more messages')
|
|
|
|
n = ircutils.bold('(%i %s)' % (len(msgs), more))
|
|
|
|
chunk = '%s %s' % (chunk, n)
|
|
|
|
msgs.append(_makeReply(self, msg, chunk, **replyArgs))
|
|
|
|
|
2019-12-05 21:11:01 +01:00
|
|
|
instant = conf.get(conf.supybot.reply.mores.instant,
|
|
|
|
channel=target, network=self.irc.network)
|
2005-01-19 14:14:38 +01:00
|
|
|
while instant > 1 and msgs:
|
|
|
|
instant -= 1
|
|
|
|
response = msgs.pop()
|
2020-04-11 16:40:03 +02:00
|
|
|
sendMsg(response)
|
2005-01-19 14:14:38 +01:00
|
|
|
# XXX We should somehow allow these to be returned, but
|
|
|
|
# until someone complains, we'll be fine :) We
|
|
|
|
# can't return from here, though, for obvious
|
|
|
|
# reasons.
|
|
|
|
# return m
|
|
|
|
if not msgs:
|
|
|
|
return
|
|
|
|
response = msgs.pop()
|
|
|
|
prefix = msg.prefix
|
|
|
|
if self.to and ircutils.isNick(self.to):
|
|
|
|
try:
|
|
|
|
state = self.getRealIrc().state
|
|
|
|
prefix = state.nickToHostmask(self.to)
|
|
|
|
except KeyError:
|
|
|
|
pass # We'll leave it as it is.
|
|
|
|
mask = prefix.split('!', 1)[1]
|
2005-02-07 07:28:52 +01:00
|
|
|
self._mores[mask] = msgs
|
2019-08-24 14:14:33 +02:00
|
|
|
public = bool(self.msg.channel)
|
2005-01-19 14:14:38 +01:00
|
|
|
private = self.private or not public
|
2005-02-07 07:28:52 +01:00
|
|
|
self._mores[msg.nick] = (private, msgs)
|
2020-04-11 16:40:03 +02:00
|
|
|
sendMsg(response)
|
|
|
|
return response
|
2005-01-19 14:14:38 +01:00
|
|
|
finally:
|
|
|
|
self._resetReplyAttributes()
|
|
|
|
else:
|
2010-06-24 06:25:44 +02:00
|
|
|
if msg.ignored:
|
|
|
|
# Since the final reply string is constructed via
|
|
|
|
# ' '.join(self.args), the args index for ignored commands
|
|
|
|
# needs to be popped to avoid extra spaces in the final reply.
|
|
|
|
self.args.pop(self.counter)
|
|
|
|
msg.tag('ignored', False)
|
|
|
|
else:
|
|
|
|
self.args[self.counter] = s
|
2005-01-19 14:14:38 +01:00
|
|
|
self.evalArgs()
|
|
|
|
|
2017-10-21 15:37:43 +02:00
|
|
|
def noReply(self, msg=None):
|
|
|
|
if msg is None:
|
|
|
|
msg = self.msg
|
2017-10-21 15:53:38 +02:00
|
|
|
super(NestedCommandsIrcProxy, self).noReply(msg=msg)
|
2017-10-21 15:37:43 +02:00
|
|
|
if self.finalEvaled:
|
2017-10-26 09:21:21 +02:00
|
|
|
if isinstance(self.irc, NestedCommandsIrcProxy):
|
|
|
|
self.irc.noReply(msg=msg)
|
|
|
|
else:
|
|
|
|
msg.tag('ignored', True)
|
2017-10-21 15:37:43 +02:00
|
|
|
else:
|
|
|
|
self.args.pop(self.counter)
|
|
|
|
msg.tag('ignored', False)
|
|
|
|
self.evalArgs()
|
|
|
|
|
2016-02-20 12:57:04 +01:00
|
|
|
def replies(self, L, prefixer=None, joiner=None,
|
|
|
|
onlyPrefixFirst=False, to=None,
|
|
|
|
oneToOne=None, **kwargs):
|
|
|
|
if not self.finalEvaled and oneToOne is None:
|
|
|
|
oneToOne = True
|
|
|
|
return super(NestedCommandsIrcProxy, self).replies(L,
|
2016-10-01 13:37:03 +02:00
|
|
|
prefixer=prefixer, joiner=joiner,
|
|
|
|
onlyPrefixFirst=onlyPrefixFirst, to=to,
|
|
|
|
oneToOne=oneToOne, **kwargs)
|
2016-02-20 12:57:04 +01:00
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
def error(self, s='', Raise=False, **kwargs):
|
2005-07-01 00:10:04 +02:00
|
|
|
self.repliedTo = True
|
2005-01-19 14:14:38 +01:00
|
|
|
if Raise:
|
Make irc.error do nothing is no string is given as argument.
Fixes c1d3bad64feca2529e29473f0ed8c622ad1937b1, which crashed with:
```
ERROR 2020-01-18T01:34:07 Uncaught exception in NickAuth._callCommand:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1337, in _callCommand
irc.errorNoCapability(cap)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 544, in errorNoCapability
raise Error()
supybot.callbacks.Error
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/dist-packages/supybot/log.py", line 368, in m
return f(self, *args, **kwargs)
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1359, in _callCommand
irc.error(str(e))
File "/usr/local/lib/python3.7/dist-packages/supybot/callbacks.py", line 1065, in error
raise ArgumentError
supybot.callbacks.ArgumentError
```
2020-01-18 11:04:50 +01:00
|
|
|
raise Error(s)
|
|
|
|
if not isinstance(self.irc, irclib.Irc):
|
|
|
|
return self.irc.error(s, **kwargs)
|
|
|
|
elif s:
|
|
|
|
m = _makeErrorReply(self, self.msg, s, **kwargs)
|
|
|
|
self.irc.queueMsg(m)
|
|
|
|
return m
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
return getattr(self.irc, attr)
|
|
|
|
|
2005-04-13 05:18:17 +02:00
|
|
|
IrcObjectProxy = NestedCommandsIrcProxy
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
class CommandThread(world.SupyThread):
|
|
|
|
"""Just does some extra logging and error-recovery for commands that need
|
|
|
|
to run in threads.
|
|
|
|
"""
|
|
|
|
def __init__(self, target=None, args=(), kwargs={}):
|
2005-02-16 03:17:05 +01:00
|
|
|
self.command = args[0]
|
2015-08-10 18:09:52 +02:00
|
|
|
self.cb = target.__self__
|
2005-01-19 14:14:38 +01:00
|
|
|
threadName = 'Thread #%s (for %s.%s)' % (world.threadsSpawned,
|
2005-02-17 23:39:44 +01:00
|
|
|
self.cb.name(),
|
2005-02-16 03:17:05 +01:00
|
|
|
self.command)
|
2005-02-17 23:39:44 +01:00
|
|
|
log.debug('Spawning thread %s (args: %r)', threadName, args)
|
|
|
|
self.__parent = super(CommandThread, self)
|
2005-01-19 14:14:38 +01:00
|
|
|
self.__parent.__init__(target=target, name=threadName,
|
|
|
|
args=args, kwargs=kwargs)
|
|
|
|
self.setDaemon(True)
|
|
|
|
self.originalThreaded = self.cb.threaded
|
|
|
|
self.cb.threaded = True
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
try:
|
|
|
|
self.__parent.run()
|
|
|
|
finally:
|
|
|
|
self.cb.threaded = self.originalThreaded
|
|
|
|
|
2010-08-05 07:20:46 +02:00
|
|
|
class CommandProcess(world.SupyProcess):
|
|
|
|
"""Just does some extra logging and error-recovery for commands that need
|
|
|
|
to run in processes.
|
|
|
|
"""
|
|
|
|
def __init__(self, target=None, args=(), kwargs={}):
|
2010-08-05 19:45:02 +02:00
|
|
|
pn = kwargs.pop('pn', 'Unknown')
|
|
|
|
cn = kwargs.pop('cn', 'unknown')
|
2010-08-05 07:20:46 +02:00
|
|
|
procName = 'Process #%s (for %s.%s)' % (world.processesSpawned,
|
2010-08-05 19:45:02 +02:00
|
|
|
pn,
|
|
|
|
cn)
|
2010-08-05 07:20:46 +02:00
|
|
|
log.debug('Spawning process %s (args: %r)', procName, args)
|
|
|
|
self.__parent = super(CommandProcess, self)
|
|
|
|
self.__parent.__init__(target=target, name=procName,
|
|
|
|
args=args, kwargs=kwargs)
|
2010-08-05 07:20:46 +02:00
|
|
|
|
2010-08-05 07:20:46 +02:00
|
|
|
def run(self):
|
|
|
|
self.__parent.run()
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
class CanonicalString(registry.NormalizedString):
|
|
|
|
def normalize(self, s):
|
|
|
|
return canonicalName(s)
|
|
|
|
|
|
|
|
class CanonicalNameSet(utils.NormalizingSet):
|
|
|
|
def normalize(self, s):
|
|
|
|
return canonicalName(s)
|
|
|
|
|
|
|
|
class CanonicalNameDict(utils.InsensitivePreservingDict):
|
|
|
|
def key(self, s):
|
|
|
|
return canonicalName(s)
|
|
|
|
|
|
|
|
class Disabled(registry.SpaceSeparatedListOf):
|
|
|
|
sorted = True
|
|
|
|
Value = CanonicalString
|
|
|
|
List = CanonicalNameSet
|
|
|
|
|
|
|
|
conf.registerGlobalValue(conf.supybot.commands, 'disabled',
|
2010-10-20 18:27:58 +02:00
|
|
|
Disabled([], _("""Determines what commands are currently disabled. Such
|
2005-01-19 14:14:38 +01:00
|
|
|
commands will not appear in command lists, etc. They will appear not even
|
2010-10-20 18:27:58 +02:00
|
|
|
to exist.""")))
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
class DisabledCommands(object):
|
|
|
|
def __init__(self):
|
|
|
|
self.d = CanonicalNameDict()
|
|
|
|
for name in conf.supybot.commands.disabled():
|
|
|
|
if '.' in name:
|
|
|
|
(plugin, command) = name.split('.', 1)
|
|
|
|
if command in self.d:
|
|
|
|
if self.d[command] is not None:
|
|
|
|
self.d[command].add(plugin)
|
|
|
|
else:
|
|
|
|
self.d[command] = CanonicalNameSet([plugin])
|
|
|
|
else:
|
|
|
|
self.d[name] = None
|
|
|
|
|
|
|
|
def disabled(self, command, plugin=None):
|
|
|
|
if command in self.d:
|
|
|
|
if self.d[command] is None:
|
|
|
|
return True
|
|
|
|
elif plugin in self.d[command]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def add(self, command, plugin=None):
|
|
|
|
if plugin is None:
|
|
|
|
self.d[command] = None
|
|
|
|
else:
|
|
|
|
if command in self.d:
|
|
|
|
if self.d[command] is not None:
|
|
|
|
self.d[command].add(plugin)
|
|
|
|
else:
|
|
|
|
self.d[command] = CanonicalNameSet([plugin])
|
|
|
|
|
|
|
|
def remove(self, command, plugin=None):
|
|
|
|
if plugin is None:
|
|
|
|
del self.d[command]
|
|
|
|
else:
|
|
|
|
if self.d[command] is not None:
|
|
|
|
self.d[command].remove(plugin)
|
|
|
|
|
2005-02-24 13:49:08 +01:00
|
|
|
class BasePlugin(object):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.cbs = []
|
|
|
|
for attr in dir(self):
|
|
|
|
if attr != canonicalName(attr):
|
|
|
|
continue
|
|
|
|
obj = getattr(self, attr)
|
|
|
|
if isinstance(obj, type) and issubclass(obj, BasePlugin):
|
|
|
|
cb = obj(*args, **kwargs)
|
|
|
|
setattr(self, attr, cb)
|
|
|
|
self.cbs.append(cb)
|
|
|
|
cb.log = log.getPluginLogger('%s.%s' % (self.name(),cb.name()))
|
2008-12-08 21:38:11 +01:00
|
|
|
super(BasePlugin, self).__init__()
|
2005-02-24 13:49:08 +01:00
|
|
|
|
2015-08-11 16:50:23 +02:00
|
|
|
class MetaSynchronizedAndFirewalled(log.MetaFirewall, utils.python.MetaSynchronized):
|
|
|
|
pass
|
|
|
|
SynchronizedAndFirewalled = MetaSynchronizedAndFirewalled(
|
|
|
|
'SynchronizedAndFirewalled', (), {})
|
2005-05-15 20:27:12 +02:00
|
|
|
|
2015-08-11 16:50:23 +02:00
|
|
|
class Commands(BasePlugin, SynchronizedAndFirewalled):
|
2005-05-15 20:27:12 +02:00
|
|
|
__synchronized__ = (
|
|
|
|
'__call__',
|
|
|
|
'callCommand',
|
|
|
|
'invalidCommand',
|
|
|
|
)
|
2009-08-14 17:22:23 +02:00
|
|
|
# For a while, a comment stood here to say, "Eventually callCommand." But
|
2005-06-08 18:24:54 +02:00
|
|
|
# that's wrong, because we can't do generic error handling in this
|
|
|
|
# callCommand -- plugins need to be able to override callCommand and do
|
|
|
|
# error handling there (see the Web plugin for an example).
|
2005-02-24 13:49:08 +01:00
|
|
|
__firewalled__ = {'isCommand': None,
|
|
|
|
'_callCommand': None}
|
2005-01-19 14:14:38 +01:00
|
|
|
commandArgs = ['self', 'irc', 'msg', 'args']
|
|
|
|
# These must be class-scope, so all plugins use the same one.
|
|
|
|
_disabled = DisabledCommands()
|
2013-08-12 21:48:56 +02:00
|
|
|
pre_command_callbacks = []
|
2005-02-18 01:16:06 +01:00
|
|
|
def name(self):
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
|
|
|
def canonicalName(self):
|
|
|
|
return canonicalName(self.name())
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
def isDisabled(self, command):
|
|
|
|
return self._disabled.disabled(command, self.name())
|
|
|
|
|
2005-02-18 06:17:23 +01:00
|
|
|
def isCommandMethod(self, name):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""Returns whether a given method name is a command in this plugin."""
|
|
|
|
# This function is ugly, but I don't want users to call methods like
|
|
|
|
# doPrivmsg or __init__ or whatever, and this is good to stop them.
|
|
|
|
|
2009-08-27 22:41:34 +02:00
|
|
|
# Don't normalize this name: consider outFilter(self, irc, msg).
|
2005-01-19 14:14:38 +01:00
|
|
|
# name = canonicalName(name)
|
|
|
|
if self.isDisabled(name):
|
|
|
|
return False
|
2006-08-28 15:15:21 +02:00
|
|
|
if name != canonicalName(name):
|
|
|
|
return False
|
2005-01-19 14:14:38 +01:00
|
|
|
if hasattr(self, name):
|
|
|
|
method = getattr(self, name)
|
|
|
|
if inspect.ismethod(method):
|
2015-08-10 18:09:52 +02:00
|
|
|
code = method.__func__.__code__
|
2005-01-19 14:14:38 +01:00
|
|
|
return inspect.getargs(code)[0] == self.commandArgs
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2005-02-18 06:17:23 +01:00
|
|
|
def isCommand(self, command):
|
2005-02-24 13:49:08 +01:00
|
|
|
"""Convenience, backwards-compatibility, semi-deprecated."""
|
2015-08-10 20:24:11 +02:00
|
|
|
if isinstance(command, minisix.string_types):
|
2005-02-18 06:17:23 +01:00
|
|
|
return self.isCommandMethod(command)
|
|
|
|
else:
|
|
|
|
# Since we're doing a little type dispatching here, let's not be
|
|
|
|
# too liberal.
|
|
|
|
assert isinstance(command, list)
|
|
|
|
return self.getCommand(command) == command
|
|
|
|
|
2013-08-10 15:22:47 +02:00
|
|
|
def getCommand(self, args, stripOwnName=True):
|
2014-01-21 10:57:38 +01:00
|
|
|
assert args == list(map(canonicalName, args))
|
2005-02-24 13:49:08 +01:00
|
|
|
first = args[0]
|
|
|
|
for cb in self.cbs:
|
|
|
|
if first == cb.canonicalName():
|
2005-03-09 09:05:24 +01:00
|
|
|
return cb.getCommand(args)
|
2013-08-10 15:22:47 +02:00
|
|
|
if first == self.canonicalName() and len(args) > 1 and \
|
|
|
|
stripOwnName:
|
|
|
|
ret = self.getCommand(args[1:], stripOwnName=False)
|
2005-03-09 09:05:24 +01:00
|
|
|
if ret:
|
|
|
|
return [first] + ret
|
2005-03-09 11:42:16 +01:00
|
|
|
if self.isCommandMethod(first):
|
|
|
|
return [first]
|
2005-02-18 06:17:23 +01:00
|
|
|
return []
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-02-18 06:17:23 +01:00
|
|
|
def getCommandMethod(self, command):
|
2005-01-19 14:14:38 +01:00
|
|
|
"""Gets the given command from this plugin."""
|
2005-03-09 09:05:24 +01:00
|
|
|
#print '*** %s.getCommandMethod(%r)' % (self.name(), command)
|
2015-08-10 20:24:11 +02:00
|
|
|
assert not isinstance(command, minisix.string_types)
|
2014-01-21 10:57:38 +01:00
|
|
|
assert command == list(map(canonicalName, command))
|
2005-02-18 07:13:47 +01:00
|
|
|
assert self.getCommand(command) == command
|
2005-03-09 09:05:24 +01:00
|
|
|
for cb in self.cbs:
|
|
|
|
if command[0] == cb.canonicalName():
|
|
|
|
return cb.getCommandMethod(command)
|
2005-02-24 13:49:08 +01:00
|
|
|
if len(command) > 1:
|
2005-03-09 09:05:24 +01:00
|
|
|
assert command[0] == self.canonicalName()
|
|
|
|
return self.getCommandMethod(command[1:])
|
2005-02-18 07:13:47 +01:00
|
|
|
else:
|
2009-01-06 00:11:09 +01:00
|
|
|
method = getattr(self, command[0])
|
|
|
|
if inspect.ismethod(method):
|
2015-08-10 18:09:52 +02:00
|
|
|
code = method.__func__.__code__
|
2009-01-06 00:11:09 +01:00
|
|
|
if inspect.getargs(code)[0] == self.commandArgs:
|
|
|
|
return method
|
|
|
|
else:
|
|
|
|
raise AttributeError
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2009-05-23 00:52:15 +02:00
|
|
|
def listCommands(self, pluginCommands=[]):
|
|
|
|
commands = set(pluginCommands)
|
2005-02-04 21:08:38 +01:00
|
|
|
for s in dir(self):
|
2009-01-06 00:11:17 +01:00
|
|
|
if self.isCommandMethod(s):
|
2009-05-23 00:52:15 +02:00
|
|
|
commands.add(s)
|
2005-02-24 13:49:08 +01:00
|
|
|
for cb in self.cbs:
|
|
|
|
name = cb.canonicalName()
|
|
|
|
for command in cb.listCommands():
|
2005-03-09 09:05:24 +01:00
|
|
|
if command == name:
|
2009-05-23 00:52:15 +02:00
|
|
|
commands.add(command)
|
2005-03-09 09:05:24 +01:00
|
|
|
else:
|
2009-05-23 00:52:15 +02:00
|
|
|
commands.add(' '.join([name, command]))
|
|
|
|
L = list(commands)
|
|
|
|
L.sort()
|
|
|
|
return L
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-02-17 23:39:44 +01:00
|
|
|
def callCommand(self, command, irc, msg, *args, **kwargs):
|
2013-08-12 21:48:56 +02:00
|
|
|
# We run all callbacks before checking if one of them returned True
|
2013-11-26 16:57:33 +01:00
|
|
|
if any(bool, list(cb(self, command, irc, msg, *args, **kwargs)
|
2013-08-12 21:48:56 +02:00
|
|
|
for cb in self.pre_command_callbacks)):
|
|
|
|
return
|
2005-02-17 23:39:44 +01:00
|
|
|
method = self.getCommandMethod(command)
|
2005-02-16 03:17:05 +01:00
|
|
|
method(irc, msg, *args, **kwargs)
|
|
|
|
|
|
|
|
def _callCommand(self, command, irc, msg, *args, **kwargs):
|
2012-03-18 20:45:17 +01:00
|
|
|
if irc.nick == msg.args[0]:
|
|
|
|
self.log.info('%s called in private by %q.', formatCommand(command),
|
|
|
|
msg.prefix)
|
|
|
|
else:
|
|
|
|
self.log.info('%s called on %s by %q.', formatCommand(command),
|
|
|
|
msg.args[0], msg.prefix)
|
2005-01-19 14:14:38 +01:00
|
|
|
try:
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
if len(command) == 1 or command[0] != self.canonicalName():
|
|
|
|
fullCommandName = [self.canonicalName()] + command
|
|
|
|
else:
|
|
|
|
fullCommandName = command
|
|
|
|
# Let "P" be the plugin and "X Y" the command name. The
|
|
|
|
# fullCommandName is "P X Y"
|
|
|
|
|
|
|
|
# check "Y"
|
|
|
|
cap = checkCommandCapability(msg, self, command[-1])
|
2015-09-23 12:00:18 +02:00
|
|
|
if cap:
|
|
|
|
irc.errorNoCapability(cap)
|
|
|
|
return
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
|
|
|
|
# check "P", "P.X", and "P.X.Y"
|
|
|
|
prefix = []
|
|
|
|
for name in fullCommandName:
|
|
|
|
prefix.append(name)
|
|
|
|
cap = checkCommandCapability(msg, self, prefix)
|
2005-02-19 00:53:11 +01:00
|
|
|
if cap:
|
|
|
|
irc.errorNoCapability(cap)
|
|
|
|
return
|
Limit the number of combinations of capabilities required for command names with spaces.
To call a command named 'X Y Z' in plugin 'P', we used to require lots of capabilities,
like 'P.X', 'P.Y', 'P.Z', 'P.X.Y', 'X.Y', 'P.Y', ...
Now, we only require 'P', 'P.X', 'P.X.Y', 'P.X.Y.Z', and 'Z'.
It makes it a lot easier to work with command names with a space when
supybot.capabilities.default is False.
2018-02-02 18:20:05 +01:00
|
|
|
|
2005-02-17 23:39:44 +01:00
|
|
|
try:
|
|
|
|
self.callingCommand = command
|
|
|
|
self.callCommand(command, irc, msg, *args, **kwargs)
|
|
|
|
finally:
|
|
|
|
self.callingCommand = None
|
2012-06-09 18:54:10 +02:00
|
|
|
except SilentError:
|
|
|
|
pass
|
2014-01-20 15:49:15 +01:00
|
|
|
except (getopt.GetoptError, ArgumentError) as e:
|
2005-02-18 07:13:47 +01:00
|
|
|
self.log.debug('Got %s, giving argument error.',
|
|
|
|
utils.exnToString(e))
|
2008-12-22 03:20:37 +01:00
|
|
|
help = self.getCommandHelp(command)
|
2015-03-03 08:55:00 +01:00
|
|
|
if 'command has no help.' in help:
|
|
|
|
# Note: this case will never happen, unless 'checkDoc' is set
|
|
|
|
# to False.
|
2015-05-15 14:41:08 +02:00
|
|
|
irc.error(_('Invalid arguments for %s.') % formatCommand(command))
|
2008-12-22 03:20:37 +01:00
|
|
|
else:
|
|
|
|
irc.reply(help)
|
2014-01-20 15:49:15 +01:00
|
|
|
except (SyntaxError, Error) as e:
|
2005-02-15 14:57:57 +01:00
|
|
|
self.log.debug('Error return: %s', utils.exnToString(e))
|
2005-01-19 14:14:38 +01:00
|
|
|
irc.error(str(e))
|
2014-01-20 15:49:15 +01:00
|
|
|
except Exception as e:
|
2005-02-16 03:17:05 +01:00
|
|
|
self.log.exception('Uncaught exception in %s.', command)
|
|
|
|
if conf.supybot.reply.error.detailed():
|
|
|
|
irc.error(utils.exnToString(e))
|
|
|
|
else:
|
2011-06-22 21:37:34 +02:00
|
|
|
irc.replyError(msg=msg)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2009-05-18 09:12:54 +02:00
|
|
|
def getCommandHelp(self, command, simpleSyntax=None):
|
2005-02-18 06:17:23 +01:00
|
|
|
method = self.getCommandMethod(command)
|
2008-12-22 03:20:07 +01:00
|
|
|
help = getHelp
|
2009-04-28 14:03:21 +02:00
|
|
|
chan = None
|
2019-12-05 21:11:01 +01:00
|
|
|
net = None
|
2009-04-28 14:03:21 +02:00
|
|
|
if dynamic.msg is not None:
|
2019-08-24 14:14:33 +02:00
|
|
|
chan = dynamic.msg.channel
|
2019-12-05 21:11:01 +01:00
|
|
|
if dynamic.irc is not None:
|
|
|
|
net = dynamic.irc.network
|
2009-05-18 09:12:54 +02:00
|
|
|
if simpleSyntax is None:
|
2019-12-05 21:11:01 +01:00
|
|
|
simpleSyntax = conf.get(conf.supybot.reply.showSimpleSyntax,
|
|
|
|
channel=chan, network=net)
|
2009-05-18 09:12:54 +02:00
|
|
|
if simpleSyntax:
|
2009-04-28 14:03:21 +02:00
|
|
|
help = getSyntax
|
2005-02-18 06:17:23 +01:00
|
|
|
if hasattr(method, '__doc__'):
|
2008-12-22 03:20:07 +01:00
|
|
|
return help(method, name=formatCommand(command))
|
2005-01-19 14:14:38 +01:00
|
|
|
else:
|
2010-10-20 18:27:58 +02:00
|
|
|
return format(_('The %q command has no help.'),
|
|
|
|
formatCommand(command))
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2005-02-24 13:49:08 +01:00
|
|
|
class PluginMixin(BasePlugin, irclib.IrcCallback):
|
2005-02-10 03:46:18 +01:00
|
|
|
public = True
|
|
|
|
alwaysCall = ()
|
|
|
|
threaded = False
|
|
|
|
noIgnore = False
|
2005-02-18 07:14:17 +01:00
|
|
|
classModule = None
|
2005-04-13 05:18:17 +02:00
|
|
|
Proxy = NestedCommandsIrcProxy
|
2005-02-10 03:46:18 +01:00
|
|
|
def __init__(self, irc):
|
|
|
|
myName = self.name()
|
|
|
|
self.log = log.getPluginLogger(myName)
|
2005-03-24 17:27:41 +01:00
|
|
|
self.__parent = super(PluginMixin, self)
|
|
|
|
self.__parent.__init__(irc)
|
2005-02-10 03:46:18 +01:00
|
|
|
# We can't do this because of the specialness that Owner and Misc do.
|
|
|
|
# I guess plugin authors will have to get the capitalization right.
|
|
|
|
# self.callAfter = map(str.lower, self.callAfter)
|
|
|
|
# self.callBefore = map(str.lower, self.callBefore)
|
|
|
|
|
2005-02-18 01:16:06 +01:00
|
|
|
def canonicalName(self):
|
|
|
|
return canonicalName(self.name())
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-02-10 03:46:18 +01:00
|
|
|
def __call__(self, irc, msg):
|
2005-03-12 23:27:23 +01:00
|
|
|
irc = SimpleProxy(irc, msg)
|
2005-02-10 03:46:18 +01:00
|
|
|
if msg.command == 'PRIVMSG':
|
2015-05-15 12:38:42 +02:00
|
|
|
if hasattr(self.noIgnore, '__call__'):
|
|
|
|
noIgnore = self.noIgnore(irc, msg)
|
|
|
|
else:
|
|
|
|
noIgnore = self.noIgnore
|
2020-05-07 21:36:54 +02:00
|
|
|
if (noIgnore or
|
|
|
|
not msg.prefix or # simulated echo message
|
|
|
|
not ircdb.checkIgnored(msg.prefix, msg.channel) or
|
|
|
|
not ircutils.isUserHostmask(msg.prefix)): # Some services impl.
|
2005-02-10 03:46:18 +01:00
|
|
|
self.__parent.__call__(irc, msg)
|
|
|
|
else:
|
|
|
|
self.__parent.__call__(irc, msg)
|
|
|
|
|
2019-08-15 12:22:43 +02:00
|
|
|
def registryValue(self, name, channel=None, network=None, value=True):
|
|
|
|
if isinstance(network, bool):
|
|
|
|
# Network-unaware plugin that uses 'value' as a positional
|
|
|
|
# argument.
|
|
|
|
(network, value) = (value, network)
|
2005-01-19 14:14:38 +01:00
|
|
|
plugin = self.name()
|
|
|
|
group = conf.supybot.plugins.get(plugin)
|
|
|
|
names = registry.split(name)
|
|
|
|
for name in names:
|
|
|
|
group = group.get(name)
|
2019-08-24 17:50:05 +02:00
|
|
|
if channel or network:
|
2019-08-15 12:22:43 +02:00
|
|
|
group = group.getSpecific(network=network, channel=channel)
|
2019-08-24 17:50:05 +02:00
|
|
|
if value:
|
2005-01-19 14:14:38 +01:00
|
|
|
return group()
|
|
|
|
else:
|
|
|
|
return group
|
|
|
|
|
2019-08-15 12:22:43 +02:00
|
|
|
def setRegistryValue(self, name, value, channel=None, network=None):
|
2005-01-19 14:14:38 +01:00
|
|
|
plugin = self.name()
|
|
|
|
group = conf.supybot.plugins.get(plugin)
|
|
|
|
names = registry.split(name)
|
|
|
|
for name in names:
|
|
|
|
group = group.get(name)
|
2019-08-15 12:22:43 +02:00
|
|
|
if network:
|
|
|
|
group = group.get(':' + network)
|
|
|
|
if channel:
|
|
|
|
group = group.get(channel)
|
|
|
|
group.setValue(value)
|
2005-01-19 14:14:38 +01:00
|
|
|
|
|
|
|
def userValue(self, name, prefixOrName, default=None):
|
|
|
|
try:
|
|
|
|
id = str(ircdb.users.getUserId(prefixOrName))
|
|
|
|
except KeyError:
|
|
|
|
return None
|
|
|
|
plugin = self.name()
|
|
|
|
group = conf.users.plugins.get(plugin)
|
|
|
|
names = registry.split(name)
|
|
|
|
for name in names:
|
|
|
|
group = group.get(name)
|
|
|
|
return group.get(id)()
|
|
|
|
|
|
|
|
def setUserValue(self, name, prefixOrName, value,
|
|
|
|
ignoreNoUser=True, setValue=True):
|
|
|
|
try:
|
|
|
|
id = str(ircdb.users.getUserId(prefixOrName))
|
|
|
|
except KeyError:
|
|
|
|
if ignoreNoUser:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
plugin = self.name()
|
|
|
|
group = conf.users.plugins.get(plugin)
|
|
|
|
names = registry.split(name)
|
|
|
|
for name in names:
|
|
|
|
group = group.get(name)
|
|
|
|
group = group.get(id)
|
|
|
|
if setValue:
|
|
|
|
group.setValue(value)
|
|
|
|
else:
|
|
|
|
group.set(value)
|
|
|
|
|
2005-03-03 20:34:26 +01:00
|
|
|
def getPluginHelp(self):
|
|
|
|
if hasattr(self, '__doc__'):
|
|
|
|
return self.__doc__
|
|
|
|
else:
|
|
|
|
return None
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2005-02-10 03:46:18 +01:00
|
|
|
class Plugin(PluginMixin, Commands):
|
|
|
|
pass
|
2005-02-18 06:17:23 +01:00
|
|
|
Privmsg = Plugin # Backwards compatibility.
|
2005-02-10 03:46:18 +01:00
|
|
|
|
|
|
|
|
2005-02-09 08:04:04 +01:00
|
|
|
class PluginRegexp(Plugin):
|
2005-02-09 22:50:12 +01:00
|
|
|
"""Same as Plugin, except allows the user to also include regexp-based
|
2009-04-09 16:53:02 +02:00
|
|
|
callbacks. All regexp-based callbacks must be specified in the set (or
|
|
|
|
list) attribute "regexps", "addressedRegexps", or "unaddressedRegexps"
|
|
|
|
depending on whether they should always be triggered, triggered only when
|
|
|
|
the bot is addressed, or triggered only when the bot isn't addressed.
|
2005-01-19 14:14:38 +01:00
|
|
|
"""
|
|
|
|
flags = re.I
|
|
|
|
regexps = ()
|
2014-01-21 13:43:33 +01:00
|
|
|
"""'regexps' methods are called whether the message is addressed or not."""
|
2005-01-19 14:14:38 +01:00
|
|
|
addressedRegexps = ()
|
2014-01-21 13:43:33 +01:00
|
|
|
"""'addressedRegexps' methods are called only when the message is addressed,
|
|
|
|
and then, only with the payload (i.e., what is returned from the
|
|
|
|
'addressed' function."""
|
2005-04-21 19:28:48 +02:00
|
|
|
unaddressedRegexps = ()
|
2014-01-21 13:43:33 +01:00
|
|
|
"""'unaddressedRegexps' methods are called only when the message is *not*
|
|
|
|
addressed."""
|
2005-01-19 14:14:38 +01:00
|
|
|
Proxy = SimpleProxy
|
2005-01-29 20:16:29 +01:00
|
|
|
def __init__(self, irc):
|
2005-02-09 22:50:12 +01:00
|
|
|
self.__parent = super(PluginRegexp, self)
|
2005-01-29 20:16:29 +01:00
|
|
|
self.__parent.__init__(irc)
|
2005-01-19 14:14:38 +01:00
|
|
|
self.res = []
|
|
|
|
self.addressedRes = []
|
2005-04-21 19:28:48 +02:00
|
|
|
self.unaddressedRes = []
|
2005-01-19 14:14:38 +01:00
|
|
|
for name in self.regexps:
|
2005-02-18 09:26:28 +01:00
|
|
|
method = getattr(self, name)
|
2005-01-19 14:14:38 +01:00
|
|
|
r = re.compile(method.__doc__, self.flags)
|
|
|
|
self.res.append((r, name))
|
|
|
|
for name in self.addressedRegexps:
|
2005-02-18 09:26:28 +01:00
|
|
|
method = getattr(self, name)
|
2005-01-19 14:14:38 +01:00
|
|
|
r = re.compile(method.__doc__, self.flags)
|
|
|
|
self.addressedRes.append((r, name))
|
2005-04-21 19:28:48 +02:00
|
|
|
for name in self.unaddressedRegexps:
|
|
|
|
method = getattr(self, name)
|
|
|
|
r = re.compile(method.__doc__, self.flags)
|
|
|
|
self.unaddressedRes.append((r, name))
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2005-02-18 17:19:44 +01:00
|
|
|
def _callRegexp(self, name, irc, msg, m):
|
|
|
|
method = getattr(self, name)
|
|
|
|
try:
|
|
|
|
method(irc, msg, m)
|
2014-01-20 15:49:15 +01:00
|
|
|
except Error as e:
|
2005-02-18 17:19:44 +01:00
|
|
|
irc.error(str(e))
|
2014-01-20 15:49:15 +01:00
|
|
|
except Exception as e:
|
2005-02-18 17:19:44 +01:00
|
|
|
self.log.exception('Uncaught exception in _callRegexp:')
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-02-18 17:19:44 +01:00
|
|
|
def invalidCommand(self, irc, msg, tokens):
|
|
|
|
s = ' '.join(tokens)
|
|
|
|
for (r, name) in self.addressedRes:
|
|
|
|
for m in r.finditer(s):
|
|
|
|
self._callRegexp(name, irc, msg, m)
|
2005-04-21 01:06:50 +02:00
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
def doPrivmsg(self, irc, msg):
|
|
|
|
if msg.isError:
|
|
|
|
return
|
2005-02-18 17:19:44 +01:00
|
|
|
proxy = self.Proxy(irc, msg)
|
2005-04-21 19:28:48 +02:00
|
|
|
if not msg.addressed:
|
|
|
|
for (r, name) in self.unaddressedRes:
|
|
|
|
for m in r.finditer(msg.args[1]):
|
|
|
|
self._callRegexp(name, proxy, msg, m)
|
2005-01-19 14:14:38 +01:00
|
|
|
for (r, name) in self.res:
|
|
|
|
for m in r.finditer(msg.args[1]):
|
2005-02-18 17:19:44 +01:00
|
|
|
self._callRegexp(name, proxy, msg, m)
|
2005-02-09 08:04:04 +01:00
|
|
|
PrivmsgCommandAndRegexp = PluginRegexp
|
|
|
|
|
2005-01-19 14:14:38 +01:00
|
|
|
|
2006-02-11 16:52:51 +01:00
|
|
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|