460 lines
16 KiB
Python
Executable File
460 lines
16 KiB
Python
Executable File
"""This contains decorators and tools for creating callable plugin functions.
|
||
"""
|
||
# Copyright 2013, Ari Koivula, <ari@koivu.la>
|
||
# Copyright © 2013, Elad Alfassa <elad@fedoraproject.org>
|
||
# Copyright 2013, Lior Ramati <firerogue517@gmail.com>
|
||
# Licensed under the Eiffel Forum License 2.
|
||
|
||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||
|
||
import re
|
||
import test_tools
|
||
import functools
|
||
|
||
NOLIMIT = 1
|
||
"""Return value for ``callable``\s, which supresses rate limiting for the call.
|
||
|
||
Returning this value means the triggering user will not be
|
||
prevented from triggering the command again within the rate limit. This can
|
||
be used, for example, to allow a user to rety a failed command immediately.
|
||
|
||
.. versionadded:: 4.0
|
||
"""
|
||
|
||
VOICE = 1
|
||
HALFOP = 2
|
||
OP = 4
|
||
ADMIN = 8
|
||
OWNER = 16
|
||
|
||
|
||
def unblockable(function):
|
||
"""Decorator which exempts the function from nickname and hostname blocking.
|
||
|
||
This can be used to ensure events such as JOIN are always recorded.
|
||
"""
|
||
function.unblockable = True
|
||
return function
|
||
|
||
|
||
def interval(*args):
|
||
"""Decorates a function to be called by the bot every X seconds.
|
||
|
||
This decorator can be used multiple times for multiple intervals, or all
|
||
intervals can be given at once as arguments. The first time the function
|
||
will be called is X seconds after the bot was started.
|
||
|
||
Unlike other plugin functions, ones decorated by interval must only take a
|
||
:class:`sopel.bot.Sopel` as their argument; they do not get a trigger. The
|
||
bot argument will not have a context, so functions like ``bot.say()`` will
|
||
not have a default destination.
|
||
|
||
There is no guarantee that the bot is connected to a server or joined a
|
||
channel when the function is called, so care must be taken.
|
||
|
||
Example:::
|
||
|
||
import sopel.module
|
||
@sopel.module.interval(5)
|
||
def spam_every_5s(bot):
|
||
if "#here" in bot.channels:
|
||
bot.say("It has been five seconds!", "#here")
|
||
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "interval"):
|
||
function.interval = []
|
||
for arg in args:
|
||
function.interval.append(arg)
|
||
return function
|
||
|
||
return add_attribute
|
||
|
||
|
||
def rule(value):
|
||
"""Decorate a function to be called when a line matches the given pattern
|
||
|
||
This decorator can be used multiple times to add more rules.
|
||
|
||
Args:
|
||
value: A regular expression which will trigger the function.
|
||
|
||
If the Sopel instance is in a channel, or sent a PRIVMSG, where a string
|
||
matching this expression is said, the function will execute. Note that
|
||
captured groups here will be retrievable through the Trigger object later.
|
||
|
||
Inside the regular expression, some special directives can be used. $nick
|
||
will be replaced with the nick of the bot and , or :, and $nickname will be
|
||
replaced with the nick of the bot.
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "rule"):
|
||
function.rule = []
|
||
function.rule.append(value)
|
||
return function
|
||
|
||
return add_attribute
|
||
|
||
|
||
def thread(value):
|
||
"""Decorate a function to specify if it should be run in a separate thread.
|
||
|
||
Functions run in a separate thread (as is the default) will not prevent the
|
||
bot from executing other functions at the same time. Functions not run in a
|
||
separate thread may be started while other functions are still running, but
|
||
additional functions will not start until it is completed.
|
||
|
||
Args:
|
||
value: Either True or False. If True the function is called in
|
||
a separate thread. If False from the main thread.
|
||
|
||
"""
|
||
def add_attribute(function):
|
||
function.thread = value
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def commands(*command_list):
|
||
"""Decorate a function to set one or more commands to trigger it.
|
||
|
||
This decorator can be used to add multiple commands to one callable in a
|
||
single line. The resulting match object will have the command as the first
|
||
group, rest of the line, excluding leading whitespace, as the second group.
|
||
Parameters 1 through 4, seperated by whitespace, will be groups 3-6.
|
||
|
||
Args:
|
||
command: A string, which can be a regular expression.
|
||
|
||
Returns:
|
||
A function with a new command appended to the commands
|
||
attribute. If there is no commands attribute, it is added.
|
||
|
||
Example:
|
||
@commands("hello"):
|
||
If the command prefix is "\.", this would trigger on lines starting
|
||
with ".hello".
|
||
|
||
@commands('j', 'join')
|
||
If the command prefix is "\.", this would trigger on lines starting
|
||
with either ".j" or ".join".
|
||
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "commands"):
|
||
function.commands = []
|
||
function.commands.extend(command_list)
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def nickname_commands(*command_list):
|
||
"""Decorate a function to trigger on lines starting with "$nickname: command".
|
||
|
||
This decorator can be used multiple times to add multiple rules. The
|
||
resulting match object will have the command as the first group, rest of
|
||
the line, excluding leading whitespace, as the second group. Parameters 1
|
||
through 4, seperated by whitespace, will be groups 3-6.
|
||
|
||
Args:
|
||
command: A string, which can be a regular expression.
|
||
|
||
Returns:
|
||
A function with a new regular expression appended to the rule
|
||
attribute. If there is no rule attribute, it is added.
|
||
|
||
Example:
|
||
@nickname_commands("hello!"):
|
||
Would trigger on "$nickname: hello!", "$nickname, hello!",
|
||
"$nickname hello!", "$nickname hello! parameter1" and
|
||
"$nickname hello! p1 p2 p3 p4 p5 p6 p7 p8 p9".
|
||
@nickname_commands(".*"):
|
||
Would trigger on anything starting with "$nickname[:,]? ", and
|
||
would have never have any additional parameters, as the command
|
||
would match the rest of the line.
|
||
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "rule"):
|
||
function.rule = []
|
||
rule = r"""
|
||
^
|
||
$nickname[:,]? # Nickname.
|
||
\s+({command}) # Command as group 1.
|
||
(?:\s+ # Whitespace to end command.
|
||
( # Rest of the line as group 2.
|
||
(?:(\S+))? # Parameters 1-4 as groups 3-6.
|
||
(?:\s+(\S+))?
|
||
(?:\s+(\S+))?
|
||
(?:\s+(\S+))?
|
||
.* # Accept anything after the parameters. Leave it up to
|
||
# the module to parse the line.
|
||
))? # Group 1 must be None, if there are no parameters.
|
||
$ # EoL, so there are no partial matches.
|
||
""".format(command='|'.join(command_list))
|
||
function.rule.append(rule)
|
||
return function
|
||
|
||
return add_attribute
|
||
|
||
|
||
def priority(value):
|
||
"""Decorate a function to be executed with higher or lower priority.
|
||
|
||
Args:
|
||
value: Priority can be one of "high", "medium", "low". Defaults to
|
||
medium.
|
||
|
||
Priority allows you to control the order of callable execution, if your
|
||
module needs it.
|
||
|
||
"""
|
||
def add_attribute(function):
|
||
function.priority = value
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def event(*event_list):
|
||
"""Decorate a function to be triggered on specific IRC events.
|
||
|
||
This is one of a number of events, such as 'JOIN', 'PART', 'QUIT', etc.
|
||
(More details can be found in RFC 1459.) When the Sopel bot is sent one of
|
||
these events, the function will execute. Note that functions with an event
|
||
must also be given a rule to match (though it may be '.*', which will
|
||
always match) or they will not be triggered.
|
||
|
||
:class:`sopel.tools.events` provides human-readable names for many of the
|
||
numeric events, which may help your code be clearer.
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "event"):
|
||
function.event = []
|
||
function.event.extend(event_list)
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def intent(*intent_list):
|
||
"""Decorate a callable trigger on a message with any of the given intents.
|
||
|
||
.. versionadded:: 5.2.0
|
||
"""
|
||
def add_attribute(function):
|
||
if not hasattr(function, "intents"):
|
||
function.intents = []
|
||
function.intents.extend(intent_list)
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def rate(user=0, channel=0, server=0):
|
||
"""Decorate a function to limit how often it can be triggered on a per-user
|
||
basis, in a channel, or across the server (bot). A value of zero means no
|
||
limit. If a function is given a rate of 20, that function may only be used
|
||
once every 20 seconds in the scope corresponding to the parameter.
|
||
Users on the admin list in Sopel’s configuration are exempted from rate
|
||
limits.
|
||
|
||
Rate-limited functions that use scheduled future commands should import
|
||
threading.Timer() instead of sched, or rate limiting will not work properly.
|
||
"""
|
||
def add_attribute(function):
|
||
function.rate = user
|
||
function.channel_rate = channel
|
||
function.global_rate = server
|
||
return function
|
||
return add_attribute
|
||
|
||
|
||
def require_privmsg(message=None):
|
||
"""Decorate a function to only be triggerable from a private message.
|
||
|
||
If it is triggered in a channel message, `message` will be said if given.
|
||
"""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def _nop(*args, **kwargs):
|
||
# Assign trigger and bot for easy access later
|
||
bot, trigger = args[0:2]
|
||
if trigger.is_privmsg:
|
||
return function(*args, **kwargs)
|
||
else:
|
||
if message and not callable(message):
|
||
bot.say(message)
|
||
return _nop
|
||
# Hack to allow decorator without parens
|
||
if callable(message):
|
||
return actual_decorator(message)
|
||
return actual_decorator
|
||
|
||
|
||
def require_chanmsg(message=None):
|
||
"""Decorate a function to only be triggerable from a channel message.
|
||
|
||
If it is triggered in a private message, `message` will be said if given.
|
||
"""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def _nop(*args, **kwargs):
|
||
# Assign trigger and bot for easy access later
|
||
bot, trigger = args[0:2]
|
||
if not trigger.is_privmsg:
|
||
return function(*args, **kwargs)
|
||
else:
|
||
if message and not callable(message):
|
||
bot.say(message)
|
||
return _nop
|
||
# Hack to allow decorator without parens
|
||
if callable(message):
|
||
return actual_decorator(message)
|
||
return actual_decorator
|
||
|
||
|
||
def require_privilege(level, message=None):
|
||
"""Decorate a function to require at least the given channel permission.
|
||
|
||
`level` can be one of the privilege levels defined in this module. If the
|
||
user does not have the privilege, `message` will be said if given. If it is
|
||
a private message, no checking will be done."""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def guarded(bot, trigger, *args, **kwargs):
|
||
# If this is a privmsg, ignore privilege requirements
|
||
if trigger.is_privmsg:
|
||
return function(bot, trigger, *args, **kwargs)
|
||
channel_privs = bot.privileges[trigger.sender]
|
||
allowed = channel_privs.get(trigger.nick, 0) >= level
|
||
if not trigger.is_privmsg and not allowed:
|
||
if message and not callable(message):
|
||
bot.say(message)
|
||
else:
|
||
return function(bot, trigger, *args, **kwargs)
|
||
return guarded
|
||
return actual_decorator
|
||
|
||
|
||
def require_admin(message=None):
|
||
"""Decorate a function to require the triggering user to be a bot admin.
|
||
|
||
If they are not, `message` will be said if given."""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def guarded(bot, trigger, *args, **kwargs):
|
||
if not trigger.admin:
|
||
if message and not callable(message):
|
||
bot.say(message)
|
||
else:
|
||
return function(bot, trigger, *args, **kwargs)
|
||
return guarded
|
||
# Hack to allow decorator without parens
|
||
if callable(message):
|
||
return actual_decorator(message)
|
||
return actual_decorator
|
||
|
||
|
||
def require_owner(message=None):
|
||
"""Decorate a function to require the triggering user to be the bot owner.
|
||
|
||
If they are not, `message` will be said if given."""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def guarded(bot, trigger, *args, **kwargs):
|
||
if not trigger.owner:
|
||
if message and not callable(message):
|
||
bot.say(message)
|
||
else:
|
||
return function(bot, trigger, *args, **kwargs)
|
||
return guarded
|
||
# Hack to allow decorator without parens
|
||
if callable(message):
|
||
return actual_decorator(message)
|
||
return actual_decorator
|
||
|
||
|
||
def url(url_rule):
|
||
"""Decorate a function to handle URLs.
|
||
|
||
This decorator takes a regex string that will be matched against URLs in a
|
||
message. The function it decorates, in addition to the bot and trigger,
|
||
must take a third argument ``match``, which is the regular expression match
|
||
of the url. This should be used rather than the matching in trigger, in
|
||
order to support e.g. the ``.title`` command.
|
||
"""
|
||
def actual_decorator(function):
|
||
@functools.wraps(function)
|
||
def helper(bot, trigger, match=None):
|
||
match = match or trigger
|
||
return function(bot, trigger, match)
|
||
helper.url_regex = re.compile(url_rule)
|
||
return helper
|
||
return actual_decorator
|
||
|
||
|
||
class example(object):
|
||
"""Decorate a function with an example.
|
||
|
||
Add an example attribute into a function and generate a test.
|
||
"""
|
||
# TODO dat doc doe >_<
|
||
def __init__(self, msg, result=None, privmsg=False, admin=False,
|
||
owner=False, repeat=1, re=False, ignore=None):
|
||
"""Accepts arguments for the decorator.
|
||
|
||
Args:
|
||
msg - The example message to give to the function as input.
|
||
result - Resulting output from calling the function with msg.
|
||
privmsg - If true, make the message appear to have sent in a
|
||
private message to the bot. If false, make it appear to have
|
||
come from a channel.
|
||
admin - Bool. Make the message appear to have come from an admin.
|
||
owner - Bool. Make the message appear to have come from an owner.
|
||
repeat - How many times to repeat the test. Usefull for tests that
|
||
return random stuff.
|
||
re - Bool. If true, result is interpreted as a regular expression.
|
||
ignore - a list of outputs to ignore.
|
||
|
||
"""
|
||
# Wrap result into a list for get_example_test
|
||
if isinstance(result, list):
|
||
self.result = result
|
||
elif result is not None:
|
||
self.result = [result]
|
||
else:
|
||
self.result = None
|
||
self.use_re = re
|
||
self.msg = msg
|
||
self.privmsg = privmsg
|
||
self.admin = admin
|
||
self.owner = owner
|
||
self.repeat = repeat
|
||
|
||
if isinstance(ignore, list):
|
||
self.ignore = ignore
|
||
elif ignore is not None:
|
||
self.ignore = [ignore]
|
||
else:
|
||
self.ignore = []
|
||
|
||
def __call__(self, func):
|
||
if not hasattr(func, "example"):
|
||
func.example = []
|
||
|
||
if self.result:
|
||
test = test_tools.get_example_test(
|
||
func, self.msg, self.result, self.privmsg, self.admin,
|
||
self.owner, self.repeat, self.use_re, self.ignore
|
||
)
|
||
test_tools.insert_into_module(
|
||
test, func.__module__, func.__name__, 'test_example'
|
||
)
|
||
|
||
record = {
|
||
"example": self.msg,
|
||
"result": self.result,
|
||
"privmsg": self.privmsg,
|
||
"admin": self.admin,
|
||
}
|
||
func.example.append(record)
|
||
return func
|