We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Save joined channels in the configuration
Right now when you add a channel with !join it does not save the channel to the config and the channel is not rejoined after restarting the bot.

Personally I would like to see this added, however also with the option for a delay joining channels so that bots who join a lot of channels don't flood the server.

Chances are, that I'll write a module to do this as it's something I need personally.
However in my case it would not make sense to write this into the default.cfg, as more than 10 channels would already be too difficult to maintain in the config file.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sopel/modules/admin.py`
Content:
```
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, <elad@fedoraproject.org>
7 Copyright 2013, Ari Koivula <ari@koivu.la>
8 
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 @sopel.module.require_privmsg
48 @sopel.module.require_admin
49 @sopel.module.commands('join')
50 @sopel.module.priority('low')
51 @sopel.module.example('.join #example or .join #example key')
52 def join(bot, trigger):
53     """Join the specified channel. This is an admin-only command."""
54     channel, key = trigger.group(3), trigger.group(4)
55     if not channel:
56         return
57     elif not key:
58         bot.join(channel)
59     else:
60         bot.join(channel, key)
61 
62 
63 @sopel.module.require_privmsg
64 @sopel.module.require_admin
65 @sopel.module.commands('part')
66 @sopel.module.priority('low')
67 @sopel.module.example('.part #example')
68 def part(bot, trigger):
69     """Part the specified channel. This is an admin-only command."""
70     channel, _sep, part_msg = trigger.group(2).partition(' ')
71     if part_msg:
72         bot.part(channel, part_msg)
73     else:
74         bot.part(channel)
75 
76 
77 @sopel.module.require_privmsg
78 @sopel.module.require_owner
79 @sopel.module.commands('restart')
80 @sopel.module.priority('low')
81 def restart(bot, trigger):
82     """Restart the bot. This is an owner-only command."""
83     quit_message = trigger.group(2)
84     if not quit_message:
85         quit_message = 'Restart on command from %s' % trigger.nick
86 
87     bot.restart(quit_message)
88 
89 
90 @sopel.module.require_privmsg
91 @sopel.module.require_owner
92 @sopel.module.commands('quit')
93 @sopel.module.priority('low')
94 def quit(bot, trigger):
95     """Quit from the server. This is an owner-only command."""
96     quit_message = trigger.group(2)
97     if not quit_message:
98         quit_message = 'Quitting on command from %s' % trigger.nick
99 
100     bot.quit(quit_message)
101 
102 
103 @sopel.module.require_privmsg
104 @sopel.module.require_admin
105 @sopel.module.commands('msg')
106 @sopel.module.priority('low')
107 @sopel.module.example('.msg #YourPants Does anyone else smell neurotoxin?')
108 def msg(bot, trigger):
109     """
110     Send a message to a given channel or nick. Can only be done in privmsg by
111     an admin.
112     """
113     if trigger.group(2) is None:
114         return
115 
116     channel, _sep, message = trigger.group(2).partition(' ')
117     message = message.strip()
118     if not channel or not message:
119         return
120 
121     bot.msg(channel, message)
122 
123 
124 @sopel.module.require_privmsg
125 @sopel.module.require_admin
126 @sopel.module.commands('me')
127 @sopel.module.priority('low')
128 def me(bot, trigger):
129     """
130     Send an ACTION (/me) to a given channel or nick. Can only be done in
131     privmsg by an admin.
132     """
133     if trigger.group(2) is None:
134         return
135 
136     channel, _sep, action = trigger.group(2).partition(' ')
137     action = action.strip()
138     if not channel or not action:
139         return
140 
141     msg = '\x01ACTION %s\x01' % action
142     bot.msg(channel, msg)
143 
144 
145 @sopel.module.event('INVITE')
146 @sopel.module.rule('.*')
147 @sopel.module.priority('low')
148 def invite_join(bot, trigger):
149     """
150     Join a channel Sopel is invited to, if the inviter is an admin.
151     """
152     if trigger.admin or bot.config.admin.auto_accept_invite:
153         bot.join(trigger.args[1])
154         return
155 
156 
157 @sopel.module.event('KICK')
158 @sopel.module.rule(r'.*')
159 @sopel.module.priority('low')
160 def hold_ground(bot, trigger):
161     """
162     This function monitors all kicks across all channels Sopel is in. If it
163     detects that it is the one kicked it'll automatically join that channel.
164 
165     WARNING: This may not be needed and could cause problems if Sopel becomes
166     annoying. Please use this with caution.
167     """
168     if bot.config.admin.hold_ground:
169         channel = trigger.sender
170         if trigger.args[1] == bot.nick:
171             bot.join(channel)
172 
173 
174 @sopel.module.require_privmsg
175 @sopel.module.require_admin
176 @sopel.module.commands('mode')
177 @sopel.module.priority('low')
178 def mode(bot, trigger):
179     """Set a user mode on Sopel. Can only be done in privmsg by an admin."""
180     mode = trigger.group(3)
181     bot.write(('MODE ', bot.nick + ' ' + mode))
182 
183 
184 @sopel.module.require_privmsg("This command only works as a private message.")
185 @sopel.module.require_admin("This command requires admin privileges.")
186 @sopel.module.commands('set')
187 @sopel.module.example('.set core.owner Me')
188 def set_config(bot, trigger):
189     """See and modify values of Sopel's config object.
190 
191     Trigger args:
192         arg1 - section and option, in the form "section.option"
193         arg2 - value
194 
195     If there is no section, section will default to "core".
196     If value is None, the option will be deleted.
197     """
198     # Get section and option from first argument.
199     arg1 = trigger.group(3).split('.')
200     if len(arg1) == 1:
201         section_name, option = "core", arg1[0]
202     elif len(arg1) == 2:
203         section_name, option = arg1
204     else:
205         bot.reply("Usage: .set section.option value")
206         return
207     section = getattr(bot.config, section_name)
208     static_sec = isinstance(section, StaticSection)
209 
210     if static_sec and not hasattr(section, option):
211         bot.say('[{}] section has no option {}.'.format(section_name, option))
212         return
213 
214     delim = trigger.group(2).find(' ')
215     # Skip preceding whitespaces, if any.
216     while delim > 0 and delim < len(trigger.group(2)) and trigger.group(2)[delim] == ' ':
217         delim = delim + 1
218 
219     # Display current value if no value is given.
220     if delim == -1 or delim == len(trigger.group(2)):
221         if not static_sec and bot.config.parser.has_option(section, option):
222             bot.reply("Option %s.%s does not exist." % (section_name, option))
223             return
224         # Except if the option looks like a password. Censor those to stop them
225         # from being put on log files.
226         if option.endswith("password") or option.endswith("pass"):
227             value = "(password censored)"
228         else:
229             value = getattr(section, option)
230         bot.reply("%s.%s = %s" % (section_name, option, value))
231         return
232 
233     # Otherwise, set the value to one given as argument 2.
234     value = trigger.group(2)[delim:]
235     if static_sec:
236         descriptor = getattr(section.__class__, option)
237         try:
238             if isinstance(descriptor, FilenameAttribute):
239                 value = descriptor.parse(bot.config, descriptor, value)
240             else:
241                 value = descriptor.parse(value)
242         except ValueError as exc:
243             bot.say("Can't set attribute: " + str(exc))
244             return
245     setattr(section, option, value)
246 
247 
248 @sopel.module.require_privmsg
249 @sopel.module.require_admin
250 @sopel.module.commands('save')
251 @sopel.module.example('.save')
252 def save_config(bot, trigger):
253     """Save state of Sopel's config object to the configuration file."""
254     bot.config.save()
255
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/sopel/modules/admin.py b/sopel/modules/admin.py
--- a/sopel/modules/admin.py
+++ b/sopel/modules/admin.py
@@ -44,6 +44,50 @@
     bot.config.define_section('admin', AdminSection)
 
 
+def _get_config_channels(channels):
+    """List"""
+    for channel_info in channels:
+        if ' ' in channel_info:
+            yield channel_info.split(' ', 1)
+        else:
+            yield (channel_info, None)
+
+
+def _set_config_channels(bot, channels):
+    bot.config.core.channels = [
+        ' '.join([part for part in items if part])
+        for items in channels.items()
+    ]
+    bot.config.save()
+
+
+def _join(bot, channel, key=None, save=True):
+    if not channel:
+        return
+
+    if not key:
+        bot.join(channel)
+    else:
+        bot.join(channel, key)
+
+    if save:
+        channels = dict(_get_config_channels(bot.config.core.channels))
+        # save only if channel is new or key has been changed
+        if channel not in channels or channels[channel] != key:
+            channels[channel] = key
+            _set_config_channels(bot, channels)
+
+
+def _part(bot, channel, msg=None, save=True):
+    bot.part(channel, msg or None)
+
+    if save:
+        channels = dict(_get_config_channels(bot.config.core.channels))
+        if channel in channels:
+            del channels[channel]
+            _set_config_channels(bot, channels)
+
+
 @sopel.module.require_privmsg
 @sopel.module.require_admin
 @sopel.module.commands('join')
@@ -52,12 +96,22 @@
 def join(bot, trigger):
     """Join the specified channel. This is an admin-only command."""
     channel, key = trigger.group(3), trigger.group(4)
-    if not channel:
-        return
-    elif not key:
-        bot.join(channel)
-    else:
-        bot.join(channel, key)
+    _join(bot, channel, key)
+
+
+@sopel.module.require_privmsg
+@sopel.module.require_admin
+@sopel.module.commands('tmpjoin')
+@sopel.module.priority('low')
+@sopel.module.example('.tmpjoin #example or .tmpjoin #example key')
+def temporary_join(bot, trigger):
+    """Like ``join``, without saving. This is an admin-only command.
+
+    Unlike the ``join`` command, ``tmpjoin`` won't remember the channel upon
+    restarting the bot.
+    """
+    channel, key = trigger.group(3), trigger.group(4)
+    _join(bot, channel, key, save=False)
 
 
 @sopel.module.require_privmsg
@@ -68,10 +122,22 @@
 def part(bot, trigger):
     """Part the specified channel. This is an admin-only command."""
     channel, _sep, part_msg = trigger.group(2).partition(' ')
-    if part_msg:
-        bot.part(channel, part_msg)
-    else:
-        bot.part(channel)
+    _part(bot, channel, part_msg)
+
+
+@sopel.module.require_privmsg
+@sopel.module.require_admin
+@sopel.module.commands('tmppart')
+@sopel.module.priority('low')
+@sopel.module.example('.tmppart #example')
+def temporary_part(bot, trigger):
+    """Like ``part``, without saving. This is an admin-only command.
+
+    Unlike the ``part`` command, ``tmppart`` will rejoin the channel upon
+    restarting the bot.
+    """
+    channel, _sep, part_msg = trigger.group(2).partition(' ')
+    _part(bot, channel, part_msg, save=False)
 
 
 @sopel.module.require_privmsg

{"golden_diff": "diff --git a/sopel/modules/admin.py b/sopel/modules/admin.py\n--- a/sopel/modules/admin.py\n+++ b/sopel/modules/admin.py\n@@ -44,6 +44,50 @@\n     bot.config.define_section('admin', AdminSection)\n \n \n+def _get_config_channels(channels):\n+    \"\"\"List\"\"\"\n+    for channel_info in channels:\n+        if ' ' in channel_info:\n+            yield channel_info.split(' ', 1)\n+        else:\n+            yield (channel_info, None)\n+\n+\n+def _set_config_channels(bot, channels):\n+    bot.config.core.channels = [\n+        ' '.join([part for part in items if part])\n+        for items in channels.items()\n+    ]\n+    bot.config.save()\n+\n+\n+def _join(bot, channel, key=None, save=True):\n+    if not channel:\n+        return\n+\n+    if not key:\n+        bot.join(channel)\n+    else:\n+        bot.join(channel, key)\n+\n+    if save:\n+        channels = dict(_get_config_channels(bot.config.core.channels))\n+        # save only if channel is new or key has been changed\n+        if channel not in channels or channels[channel] != key:\n+            channels[channel] = key\n+            _set_config_channels(bot, channels)\n+\n+\n+def _part(bot, channel, msg=None, save=True):\n+    bot.part(channel, msg or None)\n+\n+    if save:\n+        channels = dict(_get_config_channels(bot.config.core.channels))\n+        if channel in channels:\n+            del channels[channel]\n+            _set_config_channels(bot, channels)\n+\n+\n @sopel.module.require_privmsg\n @sopel.module.require_admin\n @sopel.module.commands('join')\n@@ -52,12 +96,22 @@\n def join(bot, trigger):\n     \"\"\"Join the specified channel. This is an admin-only command.\"\"\"\n     channel, key = trigger.group(3), trigger.group(4)\n-    if not channel:\n-        return\n-    elif not key:\n-        bot.join(channel)\n-    else:\n-        bot.join(channel, key)\n+    _join(bot, channel, key)\n+\n+\n+@sopel.module.require_privmsg\n+@sopel.module.require_admin\n+@sopel.module.commands('tmpjoin')\n+@sopel.module.priority('low')\n+@sopel.module.example('.tmpjoin #example or .tmpjoin #example key')\n+def temporary_join(bot, trigger):\n+    \"\"\"Like ``join``, without saving. This is an admin-only command.\n+\n+    Unlike the ``join`` command, ``tmpjoin`` won't remember the channel upon\n+    restarting the bot.\n+    \"\"\"\n+    channel, key = trigger.group(3), trigger.group(4)\n+    _join(bot, channel, key, save=False)\n \n \n @sopel.module.require_privmsg\n@@ -68,10 +122,22 @@\n def part(bot, trigger):\n     \"\"\"Part the specified channel. This is an admin-only command.\"\"\"\n     channel, _sep, part_msg = trigger.group(2).partition(' ')\n-    if part_msg:\n-        bot.part(channel, part_msg)\n-    else:\n-        bot.part(channel)\n+    _part(bot, channel, part_msg)\n+\n+\n+@sopel.module.require_privmsg\n+@sopel.module.require_admin\n+@sopel.module.commands('tmppart')\n+@sopel.module.priority('low')\n+@sopel.module.example('.tmppart #example')\n+def temporary_part(bot, trigger):\n+    \"\"\"Like ``part``, without saving. This is an admin-only command.\n+\n+    Unlike the ``part`` command, ``tmppart`` will rejoin the channel upon\n+    restarting the bot.\n+    \"\"\"\n+    channel, _sep, part_msg = trigger.group(2).partition(' ')\n+    _part(bot, channel, part_msg, save=False)\n \n \n @sopel.module.require_privmsg\n", "issue": "Save joined channels in the configuration\nRight now when you add a channel with !join it does not save the channel to the config and the channel is not rejoined after restarting the bot.\n\nPersonally I would like to see this added, however also with the option for a delay joining channels so that bots who join a lot of channels don't flood the server.\n\nChances are, that I'll write a module to do this as it's something I need personally.\nHowever in my case it would not make sense to write this into the default.cfg, as more than 10 channels would already be too difficult to maintain in the config file.\n\n", "before_files": [{"content": "# coding=utf-8\n\"\"\"\nadmin.py - Sopel Admin Module\nCopyright 2010-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich\n(yanovich.net)\nCopyright \u00a9 2012, Elad Alfassa, <elad@fedoraproject.org>\nCopyright 2013, Ari Koivula <ari@koivu.la>\n\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nfrom sopel.config.types import (\n    StaticSection, ValidatedAttribute, FilenameAttribute\n)\nimport sopel.module\n\n\nclass AdminSection(StaticSection):\n    hold_ground = ValidatedAttribute('hold_ground', bool, default=False)\n    \"\"\"Auto re-join on kick\"\"\"\n    auto_accept_invite = ValidatedAttribute('auto_accept_invite', bool,\n                                            default=True)\n    \"\"\"Auto-join channels when invited\"\"\"\n\n\ndef configure(config):\n    \"\"\"\n    | name | example | purpose |\n    | ---- | ------- | ------- |\n    | hold\\\\_ground | False | Auto-rejoin the channel after being kicked. |\n    | auto\\\\_accept\\\\_invite | True | Auto-join channels when invited. |\n    \"\"\"\n    config.define_section('admin', AdminSection)\n    config.admin.configure_setting('hold_ground',\n                                   \"Automatically re-join after being kicked?\")\n    config.admin.configure_setting('auto_accept_invite',\n                                   'Automatically join channels when invited?')\n\n\ndef setup(bot):\n    bot.config.define_section('admin', AdminSection)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('join')\n@sopel.module.priority('low')\n@sopel.module.example('.join #example or .join #example key')\ndef join(bot, trigger):\n    \"\"\"Join the specified channel. This is an admin-only command.\"\"\"\n    channel, key = trigger.group(3), trigger.group(4)\n    if not channel:\n        return\n    elif not key:\n        bot.join(channel)\n    else:\n        bot.join(channel, key)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('part')\n@sopel.module.priority('low')\n@sopel.module.example('.part #example')\ndef part(bot, trigger):\n    \"\"\"Part the specified channel. This is an admin-only command.\"\"\"\n    channel, _sep, part_msg = trigger.group(2).partition(' ')\n    if part_msg:\n        bot.part(channel, part_msg)\n    else:\n        bot.part(channel)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_owner\n@sopel.module.commands('restart')\n@sopel.module.priority('low')\ndef restart(bot, trigger):\n    \"\"\"Restart the bot. This is an owner-only command.\"\"\"\n    quit_message = trigger.group(2)\n    if not quit_message:\n        quit_message = 'Restart on command from %s' % trigger.nick\n\n    bot.restart(quit_message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_owner\n@sopel.module.commands('quit')\n@sopel.module.priority('low')\ndef quit(bot, trigger):\n    \"\"\"Quit from the server. This is an owner-only command.\"\"\"\n    quit_message = trigger.group(2)\n    if not quit_message:\n        quit_message = 'Quitting on command from %s' % trigger.nick\n\n    bot.quit(quit_message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('msg')\n@sopel.module.priority('low')\n@sopel.module.example('.msg #YourPants Does anyone else smell neurotoxin?')\ndef msg(bot, trigger):\n    \"\"\"\n    Send a message to a given channel or nick. Can only be done in privmsg by\n    an admin.\n    \"\"\"\n    if trigger.group(2) is None:\n        return\n\n    channel, _sep, message = trigger.group(2).partition(' ')\n    message = message.strip()\n    if not channel or not message:\n        return\n\n    bot.msg(channel, message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('me')\n@sopel.module.priority('low')\ndef me(bot, trigger):\n    \"\"\"\n    Send an ACTION (/me) to a given channel or nick. Can only be done in\n    privmsg by an admin.\n    \"\"\"\n    if trigger.group(2) is None:\n        return\n\n    channel, _sep, action = trigger.group(2).partition(' ')\n    action = action.strip()\n    if not channel or not action:\n        return\n\n    msg = '\\x01ACTION %s\\x01' % action\n    bot.msg(channel, msg)\n\n\n@sopel.module.event('INVITE')\n@sopel.module.rule('.*')\n@sopel.module.priority('low')\ndef invite_join(bot, trigger):\n    \"\"\"\n    Join a channel Sopel is invited to, if the inviter is an admin.\n    \"\"\"\n    if trigger.admin or bot.config.admin.auto_accept_invite:\n        bot.join(trigger.args[1])\n        return\n\n\n@sopel.module.event('KICK')\n@sopel.module.rule(r'.*')\n@sopel.module.priority('low')\ndef hold_ground(bot, trigger):\n    \"\"\"\n    This function monitors all kicks across all channels Sopel is in. If it\n    detects that it is the one kicked it'll automatically join that channel.\n\n    WARNING: This may not be needed and could cause problems if Sopel becomes\n    annoying. Please use this with caution.\n    \"\"\"\n    if bot.config.admin.hold_ground:\n        channel = trigger.sender\n        if trigger.args[1] == bot.nick:\n            bot.join(channel)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('mode')\n@sopel.module.priority('low')\ndef mode(bot, trigger):\n    \"\"\"Set a user mode on Sopel. Can only be done in privmsg by an admin.\"\"\"\n    mode = trigger.group(3)\n    bot.write(('MODE ', bot.nick + ' ' + mode))\n\n\n@sopel.module.require_privmsg(\"This command only works as a private message.\")\n@sopel.module.require_admin(\"This command requires admin privileges.\")\n@sopel.module.commands('set')\n@sopel.module.example('.set core.owner Me')\ndef set_config(bot, trigger):\n    \"\"\"See and modify values of Sopel's config object.\n\n    Trigger args:\n        arg1 - section and option, in the form \"section.option\"\n        arg2 - value\n\n    If there is no section, section will default to \"core\".\n    If value is None, the option will be deleted.\n    \"\"\"\n    # Get section and option from first argument.\n    arg1 = trigger.group(3).split('.')\n    if len(arg1) == 1:\n        section_name, option = \"core\", arg1[0]\n    elif len(arg1) == 2:\n        section_name, option = arg1\n    else:\n        bot.reply(\"Usage: .set section.option value\")\n        return\n    section = getattr(bot.config, section_name)\n    static_sec = isinstance(section, StaticSection)\n\n    if static_sec and not hasattr(section, option):\n        bot.say('[{}] section has no option {}.'.format(section_name, option))\n        return\n\n    delim = trigger.group(2).find(' ')\n    # Skip preceding whitespaces, if any.\n    while delim > 0 and delim < len(trigger.group(2)) and trigger.group(2)[delim] == ' ':\n        delim = delim + 1\n\n    # Display current value if no value is given.\n    if delim == -1 or delim == len(trigger.group(2)):\n        if not static_sec and bot.config.parser.has_option(section, option):\n            bot.reply(\"Option %s.%s does not exist.\" % (section_name, option))\n            return\n        # Except if the option looks like a password. Censor those to stop them\n        # from being put on log files.\n        if option.endswith(\"password\") or option.endswith(\"pass\"):\n            value = \"(password censored)\"\n        else:\n            value = getattr(section, option)\n        bot.reply(\"%s.%s = %s\" % (section_name, option, value))\n        return\n\n    # Otherwise, set the value to one given as argument 2.\n    value = trigger.group(2)[delim:]\n    if static_sec:\n        descriptor = getattr(section.__class__, option)\n        try:\n            if isinstance(descriptor, FilenameAttribute):\n                value = descriptor.parse(bot.config, descriptor, value)\n            else:\n                value = descriptor.parse(value)\n        except ValueError as exc:\n            bot.say(\"Can't set attribute: \" + str(exc))\n            return\n    setattr(section, option, value)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('save')\n@sopel.module.example('.save')\ndef save_config(bot, trigger):\n    \"\"\"Save state of Sopel's config object to the configuration file.\"\"\"\n    bot.config.save()\n", "path": "sopel/modules/admin.py"}], "after_files": [{"content": "# coding=utf-8\n\"\"\"\nadmin.py - Sopel Admin Module\nCopyright 2010-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich\n(yanovich.net)\nCopyright \u00a9 2012, Elad Alfassa, <elad@fedoraproject.org>\nCopyright 2013, Ari Koivula <ari@koivu.la>\n\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nfrom sopel.config.types import (\n    StaticSection, ValidatedAttribute, FilenameAttribute\n)\nimport sopel.module\n\n\nclass AdminSection(StaticSection):\n    hold_ground = ValidatedAttribute('hold_ground', bool, default=False)\n    \"\"\"Auto re-join on kick\"\"\"\n    auto_accept_invite = ValidatedAttribute('auto_accept_invite', bool,\n                                            default=True)\n    \"\"\"Auto-join channels when invited\"\"\"\n\n\ndef configure(config):\n    \"\"\"\n    | name | example | purpose |\n    | ---- | ------- | ------- |\n    | hold\\\\_ground | False | Auto-rejoin the channel after being kicked. |\n    | auto\\\\_accept\\\\_invite | True | Auto-join channels when invited. |\n    \"\"\"\n    config.define_section('admin', AdminSection)\n    config.admin.configure_setting('hold_ground',\n                                   \"Automatically re-join after being kicked?\")\n    config.admin.configure_setting('auto_accept_invite',\n                                   'Automatically join channels when invited?')\n\n\ndef setup(bot):\n    bot.config.define_section('admin', AdminSection)\n\n\ndef _get_config_channels(channels):\n    \"\"\"List\"\"\"\n    for channel_info in channels:\n        if ' ' in channel_info:\n            yield channel_info.split(' ', 1)\n        else:\n            yield (channel_info, None)\n\n\ndef _set_config_channels(bot, channels):\n    bot.config.core.channels = [\n        ' '.join([part for part in items if part])\n        for items in channels.items()\n    ]\n    bot.config.save()\n\n\ndef _join(bot, channel, key=None, save=True):\n    if not channel:\n        return\n\n    if not key:\n        bot.join(channel)\n    else:\n        bot.join(channel, key)\n\n    if save:\n        channels = dict(_get_config_channels(bot.config.core.channels))\n        # save only if channel is new or key has been changed\n        if channel not in channels or channels[channel] != key:\n            channels[channel] = key\n            _set_config_channels(bot, channels)\n\n\ndef _part(bot, channel, msg=None, save=True):\n    bot.part(channel, msg or None)\n\n    if save:\n        channels = dict(_get_config_channels(bot.config.core.channels))\n        if channel in channels:\n            del channels[channel]\n            _set_config_channels(bot, channels)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('join')\n@sopel.module.priority('low')\n@sopel.module.example('.join #example or .join #example key')\ndef join(bot, trigger):\n    \"\"\"Join the specified channel. This is an admin-only command.\"\"\"\n    channel, key = trigger.group(3), trigger.group(4)\n    _join(bot, channel, key)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('tmpjoin')\n@sopel.module.priority('low')\n@sopel.module.example('.tmpjoin #example or .tmpjoin #example key')\ndef temporary_join(bot, trigger):\n    \"\"\"Like ``join``, without saving. This is an admin-only command.\n\n    Unlike the ``join`` command, ``tmpjoin`` won't remember the channel upon\n    restarting the bot.\n    \"\"\"\n    channel, key = trigger.group(3), trigger.group(4)\n    _join(bot, channel, key, save=False)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('part')\n@sopel.module.priority('low')\n@sopel.module.example('.part #example')\ndef part(bot, trigger):\n    \"\"\"Part the specified channel. This is an admin-only command.\"\"\"\n    channel, _sep, part_msg = trigger.group(2).partition(' ')\n    _part(bot, channel, part_msg)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('tmppart')\n@sopel.module.priority('low')\n@sopel.module.example('.tmppart #example')\ndef temporary_part(bot, trigger):\n    \"\"\"Like ``part``, without saving. This is an admin-only command.\n\n    Unlike the ``part`` command, ``tmppart`` will rejoin the channel upon\n    restarting the bot.\n    \"\"\"\n    channel, _sep, part_msg = trigger.group(2).partition(' ')\n    _part(bot, channel, part_msg, save=False)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_owner\n@sopel.module.commands('restart')\n@sopel.module.priority('low')\ndef restart(bot, trigger):\n    \"\"\"Restart the bot. This is an owner-only command.\"\"\"\n    quit_message = trigger.group(2)\n    if not quit_message:\n        quit_message = 'Restart on command from %s' % trigger.nick\n\n    bot.restart(quit_message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_owner\n@sopel.module.commands('quit')\n@sopel.module.priority('low')\ndef quit(bot, trigger):\n    \"\"\"Quit from the server. This is an owner-only command.\"\"\"\n    quit_message = trigger.group(2)\n    if not quit_message:\n        quit_message = 'Quitting on command from %s' % trigger.nick\n\n    bot.quit(quit_message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('msg')\n@sopel.module.priority('low')\n@sopel.module.example('.msg #YourPants Does anyone else smell neurotoxin?')\ndef msg(bot, trigger):\n    \"\"\"\n    Send a message to a given channel or nick. Can only be done in privmsg by\n    an admin.\n    \"\"\"\n    if trigger.group(2) is None:\n        return\n\n    channel, _sep, message = trigger.group(2).partition(' ')\n    message = message.strip()\n    if not channel or not message:\n        return\n\n    bot.msg(channel, message)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('me')\n@sopel.module.priority('low')\ndef me(bot, trigger):\n    \"\"\"\n    Send an ACTION (/me) to a given channel or nick. Can only be done in\n    privmsg by an admin.\n    \"\"\"\n    if trigger.group(2) is None:\n        return\n\n    channel, _sep, action = trigger.group(2).partition(' ')\n    action = action.strip()\n    if not channel or not action:\n        return\n\n    msg = '\\x01ACTION %s\\x01' % action\n    bot.msg(channel, msg)\n\n\n@sopel.module.event('INVITE')\n@sopel.module.rule('.*')\n@sopel.module.priority('low')\ndef invite_join(bot, trigger):\n    \"\"\"\n    Join a channel Sopel is invited to, if the inviter is an admin.\n    \"\"\"\n    if trigger.admin or bot.config.admin.auto_accept_invite:\n        bot.join(trigger.args[1])\n        return\n\n\n@sopel.module.event('KICK')\n@sopel.module.rule(r'.*')\n@sopel.module.priority('low')\ndef hold_ground(bot, trigger):\n    \"\"\"\n    This function monitors all kicks across all channels Sopel is in. If it\n    detects that it is the one kicked it'll automatically join that channel.\n\n    WARNING: This may not be needed and could cause problems if Sopel becomes\n    annoying. Please use this with caution.\n    \"\"\"\n    if bot.config.admin.hold_ground:\n        channel = trigger.sender\n        if trigger.args[1] == bot.nick:\n            bot.join(channel)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('mode')\n@sopel.module.priority('low')\ndef mode(bot, trigger):\n    \"\"\"Set a user mode on Sopel. Can only be done in privmsg by an admin.\"\"\"\n    mode = trigger.group(3)\n    bot.write(('MODE ', bot.nick + ' ' + mode))\n\n\n@sopel.module.require_privmsg(\"This command only works as a private message.\")\n@sopel.module.require_admin(\"This command requires admin privileges.\")\n@sopel.module.commands('set')\n@sopel.module.example('.set core.owner Me')\ndef set_config(bot, trigger):\n    \"\"\"See and modify values of Sopel's config object.\n\n    Trigger args:\n        arg1 - section and option, in the form \"section.option\"\n        arg2 - value\n\n    If there is no section, section will default to \"core\".\n    If value is None, the option will be deleted.\n    \"\"\"\n    # Get section and option from first argument.\n    arg1 = trigger.group(3).split('.')\n    if len(arg1) == 1:\n        section_name, option = \"core\", arg1[0]\n    elif len(arg1) == 2:\n        section_name, option = arg1\n    else:\n        bot.reply(\"Usage: .set section.option value\")\n        return\n    section = getattr(bot.config, section_name)\n    static_sec = isinstance(section, StaticSection)\n\n    if static_sec and not hasattr(section, option):\n        bot.say('[{}] section has no option {}.'.format(section_name, option))\n        return\n\n    delim = trigger.group(2).find(' ')\n    # Skip preceding whitespaces, if any.\n    while delim > 0 and delim < len(trigger.group(2)) and trigger.group(2)[delim] == ' ':\n        delim = delim + 1\n\n    # Display current value if no value is given.\n    if delim == -1 or delim == len(trigger.group(2)):\n        if not static_sec and bot.config.parser.has_option(section, option):\n            bot.reply(\"Option %s.%s does not exist.\" % (section_name, option))\n            return\n        # Except if the option looks like a password. Censor those to stop them\n        # from being put on log files.\n        if option.endswith(\"password\") or option.endswith(\"pass\"):\n            value = \"(password censored)\"\n        else:\n            value = getattr(section, option)\n        bot.reply(\"%s.%s = %s\" % (section_name, option, value))\n        return\n\n    # Otherwise, set the value to one given as argument 2.\n    value = trigger.group(2)[delim:]\n    if static_sec:\n        descriptor = getattr(section.__class__, option)\n        try:\n            if isinstance(descriptor, FilenameAttribute):\n                value = descriptor.parse(bot.config, descriptor, value)\n            else:\n                value = descriptor.parse(value)\n        except ValueError as exc:\n            bot.say(\"Can't set attribute: \" + str(exc))\n            return\n    setattr(section, option, value)\n\n\n@sopel.module.require_privmsg\n@sopel.module.require_admin\n@sopel.module.commands('save')\n@sopel.module.example('.save')\ndef save_config(bot, trigger):\n    \"\"\"Save state of Sopel's config object to the configuration file.\"\"\"\n    bot.config.save()\n", "path": "sopel/modules/admin.py"}]}