sopel.modules.admin   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 71
eloc 236
dl 0
loc 402
rs 2.7199
c 0
b 0
f 0

21 Functions

Rating   Name   Duplication   Size   Complexity  
A setup() 0 2 1
A configure() 0 12 1
B _join() 0 15 6
A _set_config_channels() 0 6 1
A restart() 0 11 2
A part() 0 9 1
A _part() 0 8 3
A quit() 0 11 2
A temporary_part() 0 13 1
A temporary_join() 0 13 1
A join() 0 10 1
A _get_config_channels() 0 7 3
A me() 0 18 4
A say() 0 19 4
B unset_config() 0 33 5
C set_config() 0 52 11
A invite_join() 0 7 3
F parse_section_option_value() 0 45 14
A mode() 0 8 1
A save_config() 0 7 1
A hold_ground() 0 14 3

2 Methods

Rating   Name   Duplication   Size   Complexity  
A InvalidSection.__init__() 0 3 1
A InvalidSectionOption.__init__() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like sopel.modules.admin 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
"""
3
admin.py - Sopel Admin Module
4
Copyright 2010-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich
5
(yanovich.net)
6
Copyright © 2012, Elad Alfassa, <[email protected]>
7
Copyright 2013, Ari Koivula <[email protected]>
8
Copyright 2019, Florian Strzelecki, https://github.com/Exirel
9
Licensed under the Eiffel Forum License 2.
10
11
https://sopel.chat
12
"""
13
from __future__ import unicode_literals, absolute_import, print_function, division
14
15
from sopel.config.types import (
16
    StaticSection, ValidatedAttribute, FilenameAttribute
17
)
18
import sopel.module
19
20
21
class AdminSection(StaticSection):
22
    hold_ground = ValidatedAttribute('hold_ground', bool, default=False)
23
    """Auto re-join on kick"""
24
    auto_accept_invite = ValidatedAttribute('auto_accept_invite', bool,
25
                                            default=True)
26
    """Auto-join channels when invited"""
27
28
29
def configure(config):
30
    """
31
    | name | example | purpose |
32
    | ---- | ------- | ------- |
33
    | hold\\_ground | False | Auto-rejoin the channel after being kicked. |
34
    | auto\\_accept\\_invite | True | Auto-join channels when invited. |
35
    """
36
    config.define_section('admin', AdminSection)
37
    config.admin.configure_setting('hold_ground',
38
                                   "Automatically re-join after being kicked?")
39
    config.admin.configure_setting('auto_accept_invite',
40
                                   'Automatically join channels when invited?')
41
42
43
def setup(bot):
44
    bot.config.define_section('admin', AdminSection)
45
46
47
class InvalidSection(Exception):
48
    def __init__(self, section):
49
        super(InvalidSection, self).__init__(self, 'Section [{}] does not exist.'.format(section))
50
        self.section = section
51
52
53
class InvalidSectionOption(Exception):
54
    def __init__(self, section, option):
55
        super(InvalidSectionOption, self).__init__(self, 'Section [{}] does not have option \'{}\'.'.format(section, option))
56
        self.section = section
57
        self.option = option
58
59
60
def _get_config_channels(channels):
61
    """List"""
62
    for channel_info in channels:
63
        if ' ' in channel_info:
64
            yield channel_info.split(' ', 1)
65
        else:
66
            yield (channel_info, None)
67
68
69
def _set_config_channels(bot, channels):
70
    bot.config.core.channels = [
71
        ' '.join([part for part in items if part])
72
        for items in channels.items()
73
    ]
74
    bot.config.save()
75
76
77
def _join(bot, channel, key=None, save=True):
78
    if not channel:
79
        return
80
81
    if not key:
82
        bot.join(channel)
83
    else:
84
        bot.join(channel, key)
85
86
    if save:
87
        channels = dict(_get_config_channels(bot.config.core.channels))
88
        # save only if channel is new or key has been changed
89
        if channel not in channels or channels[channel] != key:
90
            channels[channel] = key
91
            _set_config_channels(bot, channels)
92
93
94
def _part(bot, channel, msg=None, save=True):
95
    bot.part(channel, msg or None)
96
97
    if save:
98
        channels = dict(_get_config_channels(bot.config.core.channels))
99
        if channel in channels:
100
            del channels[channel]
101
            _set_config_channels(bot, channels)
102
103
104
@sopel.module.require_privmsg
105
@sopel.module.require_admin
106
@sopel.module.commands('join')
107
@sopel.module.priority('low')
108
@sopel.module.example('.join #example key', user_help=True)
109
@sopel.module.example('.join #example', user_help=True)
110
def join(bot, trigger):
111
    """Join the specified channel. This is an admin-only command."""
112
    channel, key = trigger.group(3), trigger.group(4)
113
    _join(bot, channel, key)
114
115
116
@sopel.module.require_privmsg
117
@sopel.module.require_admin
118
@sopel.module.commands('tmpjoin')
119
@sopel.module.priority('low')
120
@sopel.module.example('.tmpjoin #example or .tmpjoin #example key')
121
def temporary_join(bot, trigger):
122
    """Like ``join``, without saving. This is an admin-only command.
123
124
    Unlike the ``join`` command, ``tmpjoin`` won't remember the channel upon
125
    restarting the bot.
126
    """
127
    channel, key = trigger.group(3), trigger.group(4)
128
    _join(bot, channel, key, save=False)
129
130
131
@sopel.module.require_privmsg
132
@sopel.module.require_admin
133
@sopel.module.commands('part')
134
@sopel.module.priority('low')
135
@sopel.module.example('.part #example')
136
def part(bot, trigger):
137
    """Part the specified channel. This is an admin-only command."""
138
    channel, _sep, part_msg = trigger.group(2).partition(' ')
139
    _part(bot, channel, part_msg)
140
141
142
@sopel.module.require_privmsg
143
@sopel.module.require_admin
144
@sopel.module.commands('tmppart')
145
@sopel.module.priority('low')
146
@sopel.module.example('.tmppart #example')
147
def temporary_part(bot, trigger):
148
    """Like ``part``, without saving. This is an admin-only command.
149
150
    Unlike the ``part`` command, ``tmppart`` will rejoin the channel upon
151
    restarting the bot.
152
    """
153
    channel, _sep, part_msg = trigger.group(2).partition(' ')
154
    _part(bot, channel, part_msg, save=False)
155
156
157
@sopel.module.require_privmsg
158
@sopel.module.require_owner
159
@sopel.module.commands('restart')
160
@sopel.module.priority('low')
161
def restart(bot, trigger):
162
    """Restart the bot. This is an owner-only command."""
163
    quit_message = trigger.group(2)
164
    if not quit_message:
165
        quit_message = 'Restart on command from %s' % trigger.nick
166
167
    bot.restart(quit_message)
168
169
170
@sopel.module.require_privmsg
171
@sopel.module.require_owner
172
@sopel.module.commands('quit')
173
@sopel.module.priority('low')
174
def quit(bot, trigger):
175
    """Quit from the server. This is an owner-only command."""
176
    quit_message = trigger.group(2)
177
    if not quit_message:
178
        quit_message = 'Quitting on command from %s' % trigger.nick
179
180
    bot.quit(quit_message)
181
182
183
@sopel.module.require_privmsg
184
@sopel.module.require_admin
185
@sopel.module.commands('say', 'msg')
186
@sopel.module.priority('low')
187
@sopel.module.example('.say #YourPants Does anyone else smell neurotoxin?')
188
def say(bot, trigger):
189
    """
190
    Send a message to a given channel or nick. Can only be done in privmsg by
191
    an admin.
192
    """
193
    if trigger.group(2) is None:
194
        return
195
196
    channel, _sep, message = trigger.group(2).partition(' ')
197
    message = message.strip()
198
    if not channel or not message:
199
        return
200
201
    bot.say(message, channel)
202
203
204
@sopel.module.require_privmsg
205
@sopel.module.require_admin
206
@sopel.module.commands('me')
207
@sopel.module.priority('low')
208
def me(bot, trigger):
209
    """
210
    Send an ACTION (/me) to a given channel or nick. Can only be done in
211
    privmsg by an admin.
212
    """
213
    if trigger.group(2) is None:
214
        return
215
216
    channel, _sep, action = trigger.group(2).partition(' ')
217
    action = action.strip()
218
    if not channel or not action:
219
        return
220
221
    bot.action(action, channel)
222
223
224
@sopel.module.event('INVITE')
225
@sopel.module.priority('low')
226
def invite_join(bot, trigger):
227
    """Join a channel Sopel is invited to, if the inviter is an admin."""
228
    if trigger.admin or bot.config.admin.auto_accept_invite:
229
        bot.join(trigger.args[1])
230
        return
231
232
233
@sopel.module.event('KICK')
234
@sopel.module.priority('low')
235
def hold_ground(bot, trigger):
236
    """
237
    This function monitors all kicks across all channels Sopel is in. If it
238
    detects that it is the one kicked it'll automatically join that channel.
239
240
    WARNING: This may not be needed and could cause problems if Sopel becomes
241
    annoying. Please use this with caution.
242
    """
243
    if bot.config.admin.hold_ground:
244
        channel = trigger.sender
245
        if trigger.args[1] == bot.nick:
246
            bot.join(channel)
247
248
249
@sopel.module.require_privmsg
250
@sopel.module.require_admin
251
@sopel.module.commands('mode')
252
@sopel.module.priority('low')
253
def mode(bot, trigger):
254
    """Set a user mode on Sopel. Can only be done in privmsg by an admin."""
255
    mode = trigger.group(3)
256
    bot.write(('MODE', bot.nick + ' ' + mode))
257
258
259
def parse_section_option_value(config, trigger):
260
    """Parse trigger for set/unset to get relevant config elements.
261
262
    :param config: Sopel's config
263
    :param trigger: IRC line trigger
264
    :return: A tuple with ``(section, section_name, static_sec, option, value)``
265
    :raises InvalidSection: section does not exist
266
    :raises InvalidSectionOption: option does not exist for section
267
268
    The ``value`` is optional and can be returned as ``None`` if omitted from command.
269
    """
270
    match = trigger.group(3)
271
    if match is None:
272
        raise ValueError  # Invalid command
273
274
    # Get section and option from first argument.
275
    arg1 = match.split('.')
276
    if len(arg1) == 1:
277
        section_name, option = "core", arg1[0]
278
    elif len(arg1) == 2:
279
        section_name, option = arg1
280
    else:
281
        raise ValueError  # invalid command format
282
283
    section = getattr(config, section_name, False)
284
    if not section:
285
        raise InvalidSection(section_name)
286
    static_sec = isinstance(section, StaticSection)
287
288
    if static_sec and not hasattr(section, option):
289
        raise InvalidSectionOption(section_name, option)  # Option not found in section
290
291
    if not static_sec and not config.parser.has_option(section_name, option):
292
        raise InvalidSectionOption(section_name, option)  # Option not found in section
293
294
    delim = trigger.group(2).find(' ')
295
    # Skip preceding whitespaces, if any.
296
    while delim > 0 and delim < len(trigger.group(2)) and trigger.group(2)[delim] == ' ':
297
        delim = delim + 1
298
299
    value = trigger.group(2)[delim:]
300
    if delim == -1 or delim == len(trigger.group(2)):
301
        value = None
302
303
    return (section, section_name, static_sec, option, value)
304
305
306
@sopel.module.require_privmsg("This command only works as a private message.")
307
@sopel.module.require_admin("This command requires admin privileges.")
308
@sopel.module.commands('set')
309
@sopel.module.example('.set core.owner Me')
310
def set_config(bot, trigger):
311
    """See and modify values of Sopel's config object.
312
313
    Trigger args:
314
        arg1 - section and option, in the form "section.option"
315
        arg2 - value
316
317
    If there is no section, section will default to "core".
318
    If value is not provided, the current value will be displayed.
319
    """
320
    try:
321
        section, section_name, static_sec, option, value = parse_section_option_value(bot.config, trigger)
322
    except ValueError:
323
        bot.reply('Usage: {}set section.option [value]'.format(bot.config.core.help_prefix))
324
        return
325
    except (InvalidSection, InvalidSectionOption) as exc:
326
        bot.say(exc.args[1])
327
        return
328
329
    # Display current value if no value is given
330
    if not value:
331
        if option.endswith("password") or option.endswith("pass"):
332
            value = "(password censored)"
333
        else:
334
            value = getattr(section, option)
335
        bot.reply("%s.%s = %s (%s)" % (section_name, option, value, type(value).__name__))
336
        return
337
338
    # Owner-related settings cannot be modified interactively. Any changes to these
339
    # settings must be made directly in the config file.
340
    if section_name == 'core' and option in ['owner', 'owner_account']:
341
        bot.say("Changing '{}.{}' requires manually editing the configuration file."
342
                .format(section_name, option))
343
        return
344
345
    # Otherwise, set the value to one given
346
    if static_sec:
347
        descriptor = getattr(section.__class__, option)
348
        try:
349
            if isinstance(descriptor, FilenameAttribute):
350
                value = descriptor.parse(bot.config, descriptor, value)
351
            else:
352
                value = descriptor.parse(value)
353
        except ValueError as exc:
354
            bot.say("Can't set attribute: " + str(exc))
355
            return
356
    setattr(section, option, value)
357
    bot.say("OK. Set '{}.{}' successfully.".format(section_name, option))
358
359
360
@sopel.module.require_privmsg("This command only works as a private message.")
361
@sopel.module.require_admin("This command requires admin privileges.")
362
@sopel.module.commands('unset')
363
@sopel.module.example('.unset core.owner')
364
def unset_config(bot, trigger):
365
    """Unset value of Sopel's config object.
366
367
    Unsetting a value will reset it to the default specified in the config
368
    definition.
369
370
    Trigger args:
371
        arg1 - section and option, in the form "section.option"
372
373
    If there is no section, section will default to "core".
374
    """
375
    try:
376
        section, section_name, static_sec, option, value = parse_section_option_value(bot.config, trigger)
377
    except ValueError:
378
        bot.reply('Usage: {}unset section.option [value]'.format(bot.config.core.help_prefix))
379
        return
380
    except (InvalidSection, InvalidSectionOption) as exc:
381
        bot.say(exc.args[1])
382
        return
383
384
    if value:
385
        bot.reply('Invalid command; no value should be provided to unset.')
386
        return
387
388
    try:
389
        setattr(section, option, None)
390
        bot.say("OK. Unset '{}.{}' successfully.".format(section_name, option))
391
    except ValueError:
392
        bot.reply('Cannot unset {}.{}; it is a required option.'.format(section_name, option))
393
394
395
@sopel.module.require_privmsg
396
@sopel.module.require_admin
397
@sopel.module.commands('save')
398
@sopel.module.example('.save')
399
def save_config(bot, trigger):
400
    """Save state of Sopel's config object to the configuration file."""
401
    bot.config.save()
402