sopel.module   F
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 708
Duplicated Lines 15.96 %

Importance

Changes 0
Metric Value
wmc 79
eloc 252
dl 113
loc 708
rs 2.08
c 0
b 0
f 0

18 Functions

Rating   Name   Duplication   Size   Complexity  
A rule() 0 31 4
A echo() 0 14 2
A commands() 0 33 4
A interval() 0 34 4
A nickname_commands() 0 34 4
A unblockable() 0 7 1
A thread() 0 18 1
A action_commands() 0 28 4
A rate() 0 17 1
A intent() 0 13 4
A url() 0 48 4
A priority() 0 15 1
B require_chanmsg() 29 35 6
B require_owner() 27 33 6
B require_privilege() 0 38 7
B require_admin() 28 34 6
A event() 0 20 4
B require_privmsg() 29 35 6

2 Methods

Rating   Name   Duplication   Size   Complexity  
B example.__init__() 0 26 5
B example.__call__() 0 39 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like sopel.module often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# coding=utf-8
2
"""This contains decorators and tools for creating callable plugin functions.
3
"""
4
# Copyright 2013, Ari Koivula, <[email protected]>
5
# Copyright © 2013, Elad Alfassa <[email protected]>
6
# Copyright 2013, Lior Ramati <[email protected]>
7
# Licensed under the Eiffel Forum License 2.
8
9
from __future__ import unicode_literals, absolute_import, print_function, division
10
11
import functools
12
import re
13
14
__all__ = [
15
    # constants
16
    'NOLIMIT', 'VOICE', 'HALFOP', 'OP', 'ADMIN', 'OWNER',
17
    # decorators
18
    'action_commands',
19
    'commands',
20
    'echo',
21
    'example',
22
    'intent',
23
    'interval',
24
    'nickname_commands',
25
    'priority',
26
    'rate',
27
    'require_admin',
28
    'require_chanmsg',
29
    'require_owner',
30
    'require_privilege',
31
    'require_privmsg',
32
    'rule',
33
    'thread',
34
    'unblockable',
35
    'url',
36
]
37
38
39
NOLIMIT = 1
40
"""Return value for ``callable``\\s, which suppresses rate limiting for the call.
41
42
Returning this value means the triggering user will not be
43
prevented from triggering the command again within the rate limit. This can
44
be used, for example, to allow a user to retry a failed command immediately.
45
46
.. versionadded:: 4.0
47
"""
48
49
VOICE = 1
50
"""Privilege level for the +v channel permission
51
52
.. versionadded:: 4.1
53
"""
54
55
HALFOP = 2
56
"""Privilege level for the +h channel permission
57
58
.. versionadded:: 4.1
59
"""
60
61
OP = 4
62
"""Privilege level for the +o channel permission
63
64
.. versionadded:: 4.1
65
"""
66
67
ADMIN = 8
68
"""Privilege level for the +a channel permission
69
70
.. versionadded:: 4.1
71
"""
72
73
OWNER = 16
74
"""Privilege level for the +q channel permission
75
76
.. versionadded:: 4.1
77
"""
78
79
80
def unblockable(function):
81
    """Decorator which exempts the function from nickname and hostname blocking.
82
83
    This can be used to ensure events such as JOIN are always recorded.
84
    """
85
    function.unblockable = True
86
    return function
87
88
89
def interval(*intervals):
90
    """Decorates a function to be called by the bot every X seconds.
91
92
    This decorator can be used multiple times for multiple intervals, or all
93
    intervals can be given at once as arguments. The first time the function
94
    will be called is X seconds after the bot was started.
95
96
    Unlike other plugin functions, ones decorated by interval must only take a
97
    :class:`sopel.bot.Sopel` as their argument; they do not get a trigger. The
98
    bot argument will not have a context, so functions like ``bot.say()`` will
99
    not have a default destination.
100
101
    There is no guarantee that the bot is connected to a server or joined a
102
    channel when the function is called, so care must be taken.
103
104
    Example::
105
106
        from sopel import module
107
108
        @module.interval(5)
109
        def spam_every_5s(bot):
110
            if "#here" in bot.channels:
111
                bot.say("It has been five seconds!", "#here")
112
113
    """
114
    def add_attribute(function):
115
        if not hasattr(function, "interval"):
116
            function.interval = []
117
        for arg in intervals:
118
            if arg not in function.interval:
119
                function.interval.append(arg)
120
        return function
121
122
    return add_attribute
123
124
125
def rule(*patterns):
126
    """Decorate a function to be called when a line matches the given pattern
127
128
    Each argument is a regular expression which will trigger the function.
129
130
    This decorator can be used multiple times to add more rules.
131
132
    If the Sopel instance is in a channel, or sent a PRIVMSG, where a string
133
    matching this expression is said, the function will execute. Note that
134
    captured groups here will be retrievable through the Trigger object later.
135
136
    Inside the regular expression, some special directives can be used. $nick
137
    will be replaced with the nick of the bot and , or :, and $nickname will be
138
    replaced with the nick of the bot.
139
140
    .. versionchanged:: 7.0
141
142
        The :func:`rule` decorator can be called with multiple positional
143
        arguments, each used to add a rule. This is equivalent to decorating
144
        the same function multiple times with this decorator.
145
146
    """
147
    def add_attribute(function):
148
        if not hasattr(function, "rule"):
149
            function.rule = []
150
        for value in patterns:
151
            if value not in function.rule:
152
                function.rule.append(value)
153
        return function
154
155
    return add_attribute
156
157
158
def thread(value):
159
    """Decorate a function to specify if it should be run in a separate thread.
160
161
    :param bool value: if true, the function is called in a separate thread;
162
                       otherwise from the bot's main thread
163
164
    Functions run in a separate thread (as is the default) will not prevent the
165
    bot from executing other functions at the same time. Functions not run in a
166
    separate thread may be started while other functions are still running, but
167
    additional functions will not start until it is completed.
168
    """
169
    threaded = bool(value)
170
171
    def add_attribute(function):
172
        function.thread = threaded
173
        return function
174
175
    return add_attribute
176
177
178
def echo(function=None):
179
    """Decorate a function to specify if it should receive echo messages.
180
181
    This decorator can be used to listen in on the messages that Sopel is
182
    sending and react accordingly.
183
    """
184
    def add_attribute(function):
185
        function.echo = True
186
        return function
187
188
    # hack to allow both @echo and @echo() to work
189
    if callable(function):
190
        return add_attribute(function)
191
    return add_attribute
192
193
194
def commands(*command_list):
195
    """Decorate a function to set one or more commands to trigger it.
196
197
    This decorator can be used to add multiple commands to one callable in a
198
    single line. The resulting match object will have the command as the first
199
    group, rest of the line, excluding leading whitespace, as the second group.
200
    Parameters 1 through 4, separated by whitespace, will be groups 3-6.
201
202
    Args:
203
        command: A string, which can be a regular expression.
204
205
    Returns:
206
        A function with a new command appended to the commands
207
        attribute. If there is no commands attribute, it is added.
208
209
    Example:
210
        @commands("hello"):
211
            If the command prefix is "\\.", this would trigger on lines starting
212
            with ".hello".
213
214
        @commands('j', 'join')
215
            If the command prefix is "\\.", this would trigger on lines starting
216
            with either ".j" or ".join".
217
218
    """
219
    def add_attribute(function):
220
        if not hasattr(function, "commands"):
221
            function.commands = []
222
        for command in command_list:
223
            if command not in function.commands:
224
                function.commands.append(command)
225
        return function
226
    return add_attribute
227
228
229
def nickname_commands(*command_list):
230
    """Decorate a function to trigger on lines starting with "$nickname: command".
231
232
    This decorator can be used multiple times to add multiple rules. The
233
    resulting match object will have the command as the first group, rest of
234
    the line, excluding leading whitespace, as the second group. Parameters 1
235
    through 4, separated by whitespace, will be groups 3-6.
236
237
    Args:
238
        command: A string, which can be a regular expression.
239
240
    Returns:
241
        A function with a new regular expression appended to the rule
242
        attribute. If there is no rule attribute, it is added.
243
244
    Example:
245
        @nickname_commands("hello!"):
246
            Would trigger on "$nickname: hello!", "$nickname,   hello!",
247
            "$nickname hello!", "$nickname hello! parameter1" and
248
            "$nickname hello! p1 p2 p3 p4 p5 p6 p7 p8 p9".
249
        @nickname_commands(".*"):
250
            Would trigger on anything starting with "$nickname[:,]? ", and
251
            would never have any additional parameters, as the command would
252
            match the rest of the line.
253
254
    """
255
    def add_attribute(function):
256
        if not hasattr(function, 'nickname_commands'):
257
            function.nickname_commands = []
258
        for cmd in command_list:
259
            if cmd not in function.nickname_commands:
260
                function.nickname_commands.append(cmd)
261
        return function
262
    return add_attribute
263
264
265
def action_commands(*command_list):
266
    """Decorate a function to trigger on CTCP ACTION lines.
267
268
    This decorator can be used multiple times to add multiple rules. The
269
    resulting match object will have the command as the first group, rest of
270
    the line, excluding leading whitespace, as the second group. Parameters 1
271
    through 4, separated by whitespace, will be groups 3-6.
272
273
    Args:
274
        command: A string, which can be a regular expression.
275
276
    Returns:
277
        A function with a new regular expression appended to the rule
278
        attribute. If there is no rule attribute, it is added.
279
280
    Example:
281
        @action_commands("hello!"):
282
            Would trigger on "/me hello!"
283
    """
284
    def add_attribute(function):
285
        function.intents = ['ACTION']
286
        if not hasattr(function, 'action_commands'):
287
            function.action_commands = []
288
        for cmd in command_list:
289
            if cmd not in function.action_commands:
290
                function.action_commands.append(cmd)
291
        return function
292
    return add_attribute
293
294
295
def priority(value):
296
    """Decorate a function to be executed with higher or lower priority.
297
298
    Args:
299
        value: Priority can be one of "high", "medium", "low". Defaults to
300
            medium.
301
302
    Priority allows you to control the order of callable execution, if your
303
    module needs it.
304
305
    """
306
    def add_attribute(function):
307
        function.priority = value
308
        return function
309
    return add_attribute
310
311
312
def event(*event_list):
313
    """Decorate a function to be triggered on specific IRC events.
314
315
    This is one of a number of events, such as 'JOIN', 'PART', 'QUIT', etc.
316
    (More details can be found in RFC 1459.) When the Sopel bot is sent one of
317
    these events, the function will execute. Note that functions with an event
318
    must also be given a rule to match (though it may be '.*', which will
319
    always match) or they will not be triggered.
320
321
    :class:`sopel.tools.events` provides human-readable names for many of the
322
    numeric events, which may help your code be clearer.
323
    """
324
    def add_attribute(function):
325
        if not hasattr(function, "event"):
326
            function.event = []
327
        for name in event_list:
328
            if name not in function.event:
329
                function.event.append(name)
330
        return function
331
    return add_attribute
332
333
334
def intent(*intent_list):
335
    """Decorate a callable trigger on a message with any of the given intents.
336
337
    .. versionadded:: 5.2.0
338
    """
339
    def add_attribute(function):
340
        if not hasattr(function, "intents"):
341
            function.intents = []
342
        for name in intent_list:
343
            if name not in function.intents:
344
                function.intents.append(name)
345
        return function
346
    return add_attribute
347
348
349
def rate(user=0, channel=0, server=0):
350
    """Decorate a function to limit how often it can be triggered on a per-user
351
    basis, in a channel, or across the server (bot). A value of zero means no
352
    limit. If a function is given a rate of 20, that function may only be used
353
    once every 20 seconds in the scope corresponding to the parameter.
354
    Users on the admin list in Sopel’s configuration are exempted from rate
355
    limits.
356
357
    Rate-limited functions that use scheduled future commands should import
358
    threading.Timer() instead of sched, or rate limiting will not work properly.
359
    """
360
    def add_attribute(function):
361
        function.rate = user
362
        function.channel_rate = channel
363
        function.global_rate = server
364
        return function
365
    return add_attribute
366
367
368 View Code Duplication
def require_privmsg(message=None, reply=False):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
369
    """Decorate a function to only be triggerable from a private message.
370
371
    :param str message: optional message said if triggered in a channel
372
    :param bool reply: use :meth:`~sopel.bot.Sopel.reply` instead of
373
                       :meth:`~sopel.bot.Sopel.say` when ``True``; defaults to
374
                       ``False``
375
376
    If it is triggered in a channel message, ``message`` will be said if
377
    given. By default, it uses :meth:`bot.say() <.bot.Sopel.say>`, but when
378
    ``reply`` is true, then it     uses :meth:`bot.reply() <.bot.Sopel.reply>`
379
    instead.
380
381
    .. versionchanged:: 7.0.0
382
        Added the ``reply`` parameter.
383
    """
384
    def actual_decorator(function):
385
        @functools.wraps(function)
386
        def _nop(*args, **kwargs):
387
            # Assign trigger and bot for easy access later
388
            bot, trigger = args[0:2]
389
            if trigger.is_privmsg:
390
                return function(*args, **kwargs)
391
            else:
392
                if message and not callable(message):
393
                    if reply:
394
                        bot.reply(message)
395
                    else:
396
                        bot.say(message)
397
        return _nop
398
399
    # Hack to allow decorator without parens
400
    if callable(message):
401
        return actual_decorator(message)
402
    return actual_decorator
403
404
405 View Code Duplication
def require_chanmsg(message=None, reply=False):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
406
    """Decorate a function to only be triggerable from a channel message.
407
408
    :param str message: optional message said if triggered in private message
409
    :param bool reply: use :meth:`~.bot.Sopel.reply` instead of
410
                       :meth:`~.bot.Sopel.say` when ``True``; defaults to
411
                       ``False``
412
413
    If it is triggered in a private message, ``message`` will be said if
414
    given. By default, it uses :meth:`bot.say() <.bot.Sopel.say>`, but when
415
    ``reply`` is true, then it uses :meth:`bot.reply() <.bot.Sopel.reply>`
416
    instead.
417
418
    .. versionchanged:: 7.0.0
419
        Added the ``reply`` parameter.
420
    """
421
    def actual_decorator(function):
422
        @functools.wraps(function)
423
        def _nop(*args, **kwargs):
424
            # Assign trigger and bot for easy access later
425
            bot, trigger = args[0:2]
426
            if not trigger.is_privmsg:
427
                return function(*args, **kwargs)
428
            else:
429
                if message and not callable(message):
430
                    if reply:
431
                        bot.reply(message)
432
                    else:
433
                        bot.say(message)
434
        return _nop
435
436
    # Hack to allow decorator without parens
437
    if callable(message):
438
        return actual_decorator(message)
439
    return actual_decorator
440
441
442
def require_privilege(level, message=None, reply=False):
443
    """Decorate a function to require at least the given channel permission.
444
445
    :param int level: required privilege level to use this command
446
    :param str message: optional message said to insufficiently privileged user
447
    :param bool reply: use :meth:`~.bot.Sopel.reply` instead of
448
                       :meth:`~.bot.Sopel.say` when ``True``; defaults to
449
                       ``False``
450
451
    ``level`` can be one of the privilege level constants defined in this
452
    module. If the user does not have the privilege, the bot will say
453
    ``message`` if given. By default, it uses :meth:`bot.say()
454
    <.bot.Sopel.say>`, but when ``reply`` is true, then it uses
455
    :meth:`bot.reply() <.bot.Sopel.reply>` instead.
456
457
    Privilege requirements are ignored in private messages.
458
459
    .. versionchanged:: 7.0.0
460
        Added the ``reply`` parameter.
461
    """
462
    def actual_decorator(function):
463
        @functools.wraps(function)
464
        def guarded(bot, trigger, *args, **kwargs):
465
            # If this is a privmsg, ignore privilege requirements
466
            if trigger.is_privmsg:
467
                return function(bot, trigger, *args, **kwargs)
468
            channel_privs = bot.channels[trigger.sender].privileges
469
            allowed = channel_privs.get(trigger.nick, 0) >= level
470
            if not trigger.is_privmsg and not allowed:
471
                if message and not callable(message):
472
                    if reply:
473
                        bot.reply(message)
474
                    else:
475
                        bot.say(message)
476
            else:
477
                return function(bot, trigger, *args, **kwargs)
478
        return guarded
479
    return actual_decorator
480
481
482 View Code Duplication
def require_admin(message=None, reply=False):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
483
    """Decorate a function to require the triggering user to be a bot admin.
484
485
    :param str message: optional message said to non-admin user
486
    :param bool reply: use :meth:`~.bot.Sopel.reply` instead of
487
                       :meth:`~.bot.Sopel.say` when ``True``; defaults to
488
                       ``False``
489
490
    When the triggering user is not an admin, the command is not run, and the
491
    bot will say the ``message`` if given. By default, it uses
492
    :meth:`bot.say() <.bot.Sopel.say>`, but when ``reply`` is true, then it
493
    uses :meth:`bot.reply() <.bot.Sopel.reply>` instead.
494
495
    .. versionchanged:: 7.0.0
496
        Added the ``reply`` parameter.
497
    """
498
    def actual_decorator(function):
499
        @functools.wraps(function)
500
        def guarded(bot, trigger, *args, **kwargs):
501
            if not trigger.admin:
502
                if message and not callable(message):
503
                    if reply:
504
                        bot.reply(message)
505
                    else:
506
                        bot.say(message)
507
            else:
508
                return function(bot, trigger, *args, **kwargs)
509
        return guarded
510
511
    # Hack to allow decorator without parens
512
    if callable(message):
513
        return actual_decorator(message)
514
515
    return actual_decorator
516
517
518 View Code Duplication
def require_owner(message=None, reply=False):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
519
    """Decorate a function to require the triggering user to be the bot owner.
520
521
    :param str message: optional message said to non-owner user
522
    :param bool reply: use :meth:`~.bot.Sopel.reply` instead of
523
                       :meth:`~.bot.Sopel.say` when ``True``; defaults to
524
                       ``False``
525
526
    When the triggering user is not the bot's owner, the command is not run,
527
    and the bot will say ``message`` if given. By default, it uses
528
    :meth:`bot.say() <.bot.Sopel.say>`, but when ``reply`` is true, then it
529
    uses :meth:`bot.reply() <.bot.Sopel.reply>` instead.
530
531
    .. versionchanged:: 7.0.0
532
        Added the ``reply`` parameter.
533
    """
534
    def actual_decorator(function):
535
        @functools.wraps(function)
536
        def guarded(bot, trigger, *args, **kwargs):
537
            if not trigger.owner:
538
                if message and not callable(message):
539
                    if reply:
540
                        bot.reply(message)
541
                    else:
542
                        bot.say(message)
543
            else:
544
                return function(bot, trigger, *args, **kwargs)
545
        return guarded
546
547
    # Hack to allow decorator without parens
548
    if callable(message):
549
        return actual_decorator(message)
550
    return actual_decorator
551
552
553
def url(*url_rules):
554
    """Decorate a function to handle URLs.
555
556
    :param str url_rule: regex pattern to match URLs
557
558
    This decorator takes a regex string that will be matched against URLs in a
559
    message. The function it decorates, in addition to the bot and trigger,
560
    must take a third argument ``match``, which is the regular expression match
561
    of the URL::
562
563
        from sopel import module
564
565
        @module.url(r'https://example.com/bugs/([a-z0-9]+)')
566
        @module.url(r'https://short.com/([a-z0-9]+)')
567
        def handle_example_bugs(bot, trigger, match):
568
            bot.reply('Found bug ID #%s' % match.group(1))
569
570
    This should be used rather than the matching in trigger, in order to
571
    support e.g. the ``.title`` command.
572
573
    Under the hood, when Sopel collects the decorated handler it uses
574
    :meth:`sopel.bot.Sopel.register_url_callback` to register the handler.
575
576
    .. versionchanged:: 7.0
577
578
        The same function can be decorated multiple times with :func:`url`
579
        to register different URL patterns.
580
581
    .. versionchanged:: 7.0
582
583
        More than one pattern can be provided as positional argument at once.
584
585
    .. seealso::
586
587
        To detect URLs, Sopel uses a matching pattern built from a list of URL
588
        schemes, configured by
589
        :attr:`~sopel.config.core_section.CoreSection.auto_url_schemes`.
590
591
    """
592
    def actual_decorator(function):
593
        if not hasattr(function, 'url_regex'):
594
            function.url_regex = []
595
        for url_rule in url_rules:
596
            url_regex = re.compile(url_rule)
597
            if url_regex not in function.url_regex:
598
                function.url_regex.append(url_regex)
599
        return function
600
    return actual_decorator
601
602
603
class example(object):
604
    """Decorate a function with an example.
605
606
    Args:
607
        msg:
608
            (required) The example command as sent by a user on IRC. If it is
609
            a prefixed command, the command prefix used in the example must
610
            match the default `config.core.help_prefix` for compatibility with
611
            the built-in help module.
612
        result:
613
            What the example command is expected to output. If given, a test is
614
            generated using `msg` as input. The test behavior can be modified
615
            by the remaining optional arguments.
616
        privmsg:
617
            If true, the test will behave as if the input was sent to the bot
618
            in a private message. If false (default), the test will treat the
619
            input as having come from a channel.
620
        admin:
621
            Whether to treat the test message as having been sent by a bot
622
            admin (`trigger.admin == True`).
623
        owner:
624
            Whether to treat the test message as having been sent by the bot's
625
            owner (`trigger.owner == True`).
626
        repeat:
627
            Integer number of times to repeat the test. Useful for commands
628
            that return random results.
629
        re:
630
            If true, `result` is parsed as a regular expression. Also useful
631
            for commands that return random results, or that call an external
632
            API that doesn't always return the same value.
633
        ignore:
634
            List of outputs to ignore. Strings in this list are always
635
            interpreted as regular expressions.
636
        user_help:
637
            Whether this example should be displayed in user-facing help output
638
            such as `.help command`.
639
        online:
640
            If true, pytest will mark it as "online".
641
    """
642
    def __init__(self, msg, result=None, privmsg=False, admin=False,
643
                 owner=False, repeat=1, re=False, ignore=None,
644
                 user_help=False, online=False):
645
        # Wrap result into a list for get_example_test
646
        if isinstance(result, list):
647
            self.result = result
648
        elif result is not None:
649
            self.result = [result]
650
        else:
651
            self.result = None
652
        self.use_re = re
653
        self.msg = msg
654
        self.privmsg = privmsg
655
        self.admin = admin
656
        self.owner = owner
657
        self.repeat = repeat
658
        self.online = online
659
660
        if isinstance(ignore, list):
661
            self.ignore = ignore
662
        elif ignore is not None:
663
            self.ignore = [ignore]
664
        else:
665
            self.ignore = []
666
667
        self.user_help = user_help
668
669
    def __call__(self, func):
670
        if not hasattr(func, "example"):
671
            func.example = []
672
673
        import sys
674
675
        import sopel.test_tools  # TODO: fix circular import with sopel.bot and sopel.test_tools
676
677
        # only inject test-related stuff if we're running tests
678
        # see https://stackoverflow.com/a/44595269/5991
679
        if 'pytest' in sys.modules and self.result:
680
            # avoids doing `import pytest` and causing errors when
681
            # dev-dependencies aren't installed
682
            pytest = sys.modules['pytest']
683
684
            test = sopel.test_tools.get_example_test(
685
                func, self.msg, self.result, self.privmsg, self.admin,
686
                self.owner, self.repeat, self.use_re, self.ignore
687
            )
688
689
            if self.online:
690
                test = pytest.mark.online(test)
691
692
            sopel.test_tools.insert_into_module(
693
                test, func.__module__, func.__name__, 'test_example'
694
            )
695
            sopel.test_tools.insert_into_module(
696
                sopel.test_tools.get_disable_setup(), func.__module__, func.__name__, 'disable_setup'
697
            )
698
699
        record = {
700
            "example": self.msg,
701
            "result": self.result,
702
            "privmsg": self.privmsg,
703
            "admin": self.admin,
704
            "help": self.user_help,
705
        }
706
        func.example.append(record)
707
        return func
708