Passed
Push — master ( bdc66f...91488b )
by dgw
01:56 queued 10s
created

sopel.modules.admin.unset_config()   B

Complexity

Conditions 5

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 33
rs 8.9093
c 0
b 0
f 0
cc 5
nop 2
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.rule('.*')
226
@sopel.module.priority('low')
227
def invite_join(bot, trigger):
228
    """Join a channel Sopel is invited to, if the inviter is an admin."""
229
    if trigger.admin or bot.config.admin.auto_accept_invite:
230
        bot.join(trigger.args[1])
231
        return
232
233
234
@sopel.module.event('KICK')
235
@sopel.module.rule(r'.*')
236
@sopel.module.priority('low')
237
def hold_ground(bot, trigger):
238
    """
239
    This function monitors all kicks across all channels Sopel is in. If it
240
    detects that it is the one kicked it'll automatically join that channel.
241
242
    WARNING: This may not be needed and could cause problems if Sopel becomes
243
    annoying. Please use this with caution.
244
    """
245
    if bot.config.admin.hold_ground:
246
        channel = trigger.sender
247
        if trigger.args[1] == bot.nick:
248
            bot.join(channel)
249
250
251
@sopel.module.require_privmsg
252
@sopel.module.require_admin
253
@sopel.module.commands('mode')
254
@sopel.module.priority('low')
255
def mode(bot, trigger):
256
    """Set a user mode on Sopel. Can only be done in privmsg by an admin."""
257
    mode = trigger.group(3)
258
    bot.write(('MODE', bot.nick + ' ' + mode))
259
260
261
def parse_section_option_value(config, trigger):
262
    """Parse trigger for set/unset to get relevant config elements.
263
264
    :param config: Sopel's config
265
    :param trigger: IRC line trigger
266
    :return: A tuple with ``(section, section_name, static_sec, option, value)``
267
    :raises InvalidSection: section does not exist
268
    :raises InvalidSectionOption: option does not exist for section
269
270
    The ``value`` is optional and can be returned as ``None`` if omitted from command.
271
    """
272
    match = trigger.group(3)
273
    if match is None:
274
        raise ValueError  # Invalid command
275
276
    # Get section and option from first argument.
277
    arg1 = match.split('.')
278
    if len(arg1) == 1:
279
        section_name, option = "core", arg1[0]
280
    elif len(arg1) == 2:
281
        section_name, option = arg1
282
    else:
283
        raise ValueError  # invalid command format
284
285
    section = getattr(config, section_name, False)
286
    if not section:
287
        raise InvalidSection(section_name)
288
    static_sec = isinstance(section, StaticSection)
289
290
    if static_sec and not hasattr(section, option):
291
        raise InvalidSectionOption(section_name, option)  # Option not found in section
292
293
    if not static_sec and not config.parser.has_option(section_name, option):
294
        raise InvalidSectionOption(section_name, option)  # Option not found in section
295
296
    delim = trigger.group(2).find(' ')
297
    # Skip preceding whitespaces, if any.
298
    while delim > 0 and delim < len(trigger.group(2)) and trigger.group(2)[delim] == ' ':
299
        delim = delim + 1
300
301
    value = trigger.group(2)[delim:]
302
    if delim == -1 or delim == len(trigger.group(2)):
303
        value = None
304
305
    return (section, section_name, static_sec, option, value)
306
307
308
@sopel.module.require_privmsg("This command only works as a private message.")
309
@sopel.module.require_admin("This command requires admin privileges.")
310
@sopel.module.commands('set')
311
@sopel.module.example('.set core.owner Me')
312
def set_config(bot, trigger):
313
    """See and modify values of Sopel's config object.
314
315
    Trigger args:
316
        arg1 - section and option, in the form "section.option"
317
        arg2 - value
318
319
    If there is no section, section will default to "core".
320
    If value is not provided, the current value will be displayed.
321
    """
322
    try:
323
        section, section_name, static_sec, option, value = parse_section_option_value(bot.config, trigger)
324
    except ValueError:
325
        bot.reply('Usage: {}set section.option [value]'.format(bot.config.core.help_prefix))
326
        return
327
    except (InvalidSection, InvalidSectionOption) as exc:
328
        bot.say(exc.args[1])
329
        return
330
331
    # Display current value if no value is given
332
    if not value:
333
        if option.endswith("password") or option.endswith("pass"):
334
            value = "(password censored)"
335
        else:
336
            value = getattr(section, option)
337
        bot.reply("%s.%s = %s (%s)" % (section_name, option, value, type(value).__name__))
338
        return
339
340
    # Owner-related settings cannot be modified interactively. Any changes to these
341
    # settings must be made directly in the config file.
342
    if section_name == 'core' and option in ['owner', 'owner_account']:
343
        bot.say("Changing '{}.{}' requires manually editing the configuration file."
344
                .format(section_name, option))
345
        return
346
347
    # Otherwise, set the value to one given
348
    if static_sec:
349
        descriptor = getattr(section.__class__, option)
350
        try:
351
            if isinstance(descriptor, FilenameAttribute):
352
                value = descriptor.parse(bot.config, descriptor, value)
353
            else:
354
                value = descriptor.parse(value)
355
        except ValueError as exc:
356
            bot.say("Can't set attribute: " + str(exc))
357
            return
358
    setattr(section, option, value)
359
    bot.say("OK. Set '{}.{}' successfully.".format(section_name, option))
360
361
362
@sopel.module.require_privmsg("This command only works as a private message.")
363
@sopel.module.require_admin("This command requires admin privileges.")
364
@sopel.module.commands('unset')
365
@sopel.module.example('.unset core.owner')
366
def unset_config(bot, trigger):
367
    """Unset value of Sopel's config object.
368
369
    Unsetting a value will reset it to the default specified in the config
370
    definition.
371
372
    Trigger args:
373
        arg1 - section and option, in the form "section.option"
374
375
    If there is no section, section will default to "core".
376
    """
377
    try:
378
        section, section_name, static_sec, option, value = parse_section_option_value(bot.config, trigger)
379
    except ValueError:
380
        bot.reply('Usage: {}unset section.option [value]'.format(bot.config.core.help_prefix))
381
        return
382
    except (InvalidSection, InvalidSectionOption) as exc:
383
        bot.say(exc.args[1])
384
        return
385
386
    if value:
387
        bot.reply('Invalid command; no value should be provided to unset.')
388
        return
389
390
    try:
391
        setattr(section, option, None)
392
        bot.say("OK. Unset '{}.{}' successfully.".format(section_name, option))
393
    except ValueError:
394
        bot.reply('Cannot unset {}.{}; it is a required option.'.format(section_name, option))
395
396
397
@sopel.module.require_privmsg
398
@sopel.module.require_admin
399
@sopel.module.commands('save')
400
@sopel.module.example('.save')
401
def save_config(bot, trigger):
402
    """Save state of Sopel's config object to the configuration file."""
403
    bot.config.save()
404