We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
help: output should be sent to requester privately
IIRC, the older version of Sopel (or Willie?) used to send the long help results privately to the user who requested the help command list results. Or at least make it optional to admin who wants gisthub or privately.

Thank you in advance. :)

--- 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/help.py`
Content:
```
1 # coding=utf-8
2 """
3 help.py - Sopel Help Module
4 Copyright 2008, Sean B. Palmer, inamidst.com
5 Copyright © 2013, Elad Alfassa, <elad@fedoraproject.org>
6 Copyright © 2018, Adam Erdman, pandorah.org
7 Copyright © 2019, Tomasz Kurcz, github.com/uint
8 Copyright © 2019, dgw, technobabbl.es
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 import re
16 import collections
17 import socket
18 import textwrap
19 
20 import requests
21 
22 from sopel.config.types import ChoiceAttribute, ValidatedAttribute, StaticSection
23 from sopel.logger import get_logger
24 from sopel.module import commands, rule, example, priority
25 from sopel.tools import SopelMemory
26 
27 
28 SETTING_CACHE_NAMESPACE = 'help-setting-cache'  # Set top-level memory key name
29 LOGGER = get_logger(__name__)
30 
31 # Settings that should require the help listing to be regenerated, or
32 # re-POSTed to paste, if they are changed during runtime.
33 # Keys are module names, and values are lists of setting names
34 # specific to that module.
35 TRACKED_SETTINGS = {
36     'help': [
37         'output',
38         'show_server_host',
39     ]
40 }
41 
42 
43 class PostingException(Exception):
44     """Custom exception type for errors posting help to the chosen pastebin."""
45     pass
46 
47 
48 # Pastebin handlers
49 
50 
51 def _requests_post_catch_errors(*args, **kwargs):
52     try:
53         response = requests.post(*args, **kwargs)
54         response.raise_for_status()
55     except (
56             requests.exceptions.Timeout,
57             requests.exceptions.TooManyRedirects,
58             requests.exceptions.RequestException,
59             requests.exceptions.HTTPError
60     ):
61         # We re-raise all expected exception types to a generic "posting error"
62         # that's easy for callers to expect, and then we pass the original
63         # exception through to provide some debugging info
64         LOGGER.exception('Error during POST request')
65         raise PostingException('Could not communicate with remote service')
66 
67     # remaining handling (e.g. errors inside the response) is left to the caller
68     return response
69 
70 
71 def post_to_clbin(msg):
72     try:
73         result = _requests_post_catch_errors('https://clbin.com/', data={'clbin': msg})
74     except PostingException:
75         raise
76 
77     result = result.text
78     if '://clbin.com/' in result:
79         # find/replace just in case the site tries to be sneaky and save on SSL overhead,
80         # though it will probably send us an HTTPS link without any tricks.
81         return result.replace('http://', 'https://', 1)
82     else:
83         LOGGER.error("Invalid result %s", result)
84         raise PostingException('clbin result did not contain expected URL base.')
85 
86 
87 def post_to_0x0(msg):
88     try:
89         result = _requests_post_catch_errors('https://0x0.st', files={'file': msg})
90     except PostingException:
91         raise
92 
93     result = result.text
94     if '://0x0.st' in result:
95         # find/replace just in case the site tries to be sneaky and save on SSL overhead,
96         # though it will probably send us an HTTPS link without any tricks.
97         return result.replace('http://', 'https://', 1)
98     else:
99         LOGGER.error('Invalid result %s', result)
100         raise PostingException('0x0.st result did not contain expected URL base.')
101 
102 
103 def post_to_hastebin(msg):
104     try:
105         result = _requests_post_catch_errors('https://hastebin.com/documents', data=msg)
106     except PostingException:
107         raise
108 
109     try:
110         result = result.json()
111     except ValueError:
112         LOGGER.error("Invalid Hastebin response %s", result)
113         raise PostingException('Could not parse response from Hastebin!')
114 
115     if 'key' not in result:
116         LOGGER.error("Invalid result %s", result)
117         raise PostingException('Hastebin result did not contain expected URL base.')
118     return "https://hastebin.com/" + result['key']
119 
120 
121 def post_to_termbin(msg):
122     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
123     sock.settimeout(10)  # the bot may NOT wait forever for a response; that would be bad
124     try:
125         sock.connect(('termbin.com', 9999))
126         sock.sendall(msg)
127         sock.shutdown(socket.SHUT_WR)
128         response = ""
129         while 1:
130             data = sock.recv(1024)
131             if data == "":
132                 break
133             response += data
134         sock.close()
135     except socket.error:
136         LOGGER.exception('Error during communication with termbin')
137         raise PostingException('Error uploading to termbin')
138 
139     # find/replace just in case the site tries to be sneaky and save on SSL overhead,
140     # though it will probably send us an HTTPS link without any tricks.
141     return response.strip('\x00\n').replace('http://', 'https://', 1)
142 
143 
144 def post_to_ubuntu(msg):
145     data = {
146         'poster': 'sopel',
147         'syntax': 'text',
148         'expiration': '',
149         'content': msg,
150     }
151     try:
152         result = _requests_post_catch_errors('https://pastebin.ubuntu.com/', data=data)
153     except PostingException:
154         raise
155 
156     if not re.match(r'https://pastebin.ubuntu.com/p/[^/]+/', result.url):
157         LOGGER.error("Invalid Ubuntu pastebin response url %s", result.url)
158         raise PostingException('Invalid response from Ubuntu pastebin: %s' % result.url)
159 
160     return result.url
161 
162 
163 PASTEBIN_PROVIDERS = {
164     'clbin': post_to_clbin,
165     '0x0': post_to_0x0,
166     'hastebin': post_to_hastebin,
167     'termbin': post_to_termbin,
168     'ubuntu': post_to_ubuntu,
169 }
170 
171 
172 class HelpSection(StaticSection):
173     """Configuration section for this module."""
174     output = ChoiceAttribute('output',
175                              list(PASTEBIN_PROVIDERS),
176                              default='clbin')
177     """The pastebin provider to use for help output."""
178     show_server_host = ValidatedAttribute('show_server_host', bool, default=True)
179     """Show the IRC server's hostname/IP in the first line of the help listing?"""
180 
181 
182 def configure(config):
183     """
184     | name | example | purpose |
185     | ---- | ------- | ------- |
186     | output | clbin | The pastebin provider to use for help output |
187     | show\\_server\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |
188     """
189     config.define_section('help', HelpSection)
190     provider_list = ', '.join(PASTEBIN_PROVIDERS)
191     config.help.configure_setting(
192         'output',
193         'Pick a pastebin provider: {}: '.format(provider_list)
194     )
195     config.help.configure_setting(
196         'show_server_host',
197         'Should the help command show the IRC server\'s hostname/IP in the listing?'
198     )
199 
200 
201 def setup(bot):
202     bot.config.define_section('help', HelpSection)
203 
204     # Initialize memory
205     if SETTING_CACHE_NAMESPACE not in bot.memory:
206         bot.memory[SETTING_CACHE_NAMESPACE] = SopelMemory()
207 
208     # Initialize settings cache
209     for section in TRACKED_SETTINGS:
210         if section not in bot.memory[SETTING_CACHE_NAMESPACE]:
211             bot.memory[SETTING_CACHE_NAMESPACE][section] = SopelMemory()
212 
213     update_cache(bot)  # Populate cache
214 
215     bot.config.define_section('help', HelpSection)
216 
217 
218 def update_cache(bot):
219     for section, setting_names_list in TRACKED_SETTINGS.items():
220         for setting_name in setting_names_list:
221             bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] = getattr(getattr(bot.config, section), setting_name)
222 
223 
224 def is_cache_valid(bot):
225     for section, setting_names_list in TRACKED_SETTINGS.items():
226         for setting_name in setting_names_list:
227             if bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] != getattr(getattr(bot.config, section), setting_name):
228                 return False
229     return True
230 
231 
232 @rule('$nick' r'(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
233 @example('.help tell')
234 @commands('help', 'commands')
235 @priority('low')
236 def help(bot, trigger):
237     """Shows a command's documentation, and an example if available. With no arguments, lists all commands."""
238     if trigger.group(2):
239         name = trigger.group(2)
240         name = name.lower()
241 
242         # number of lines of help to show
243         threshold = 3
244 
245         if name in bot.doc:
246             # count lines we're going to send
247             # lines in command docstring, plus one line for example(s) if present (they're sent all on one line)
248             if len(bot.doc[name][0]) + int(bool(bot.doc[name][1])) > threshold:
249                 if trigger.nick != trigger.sender:  # don't say that if asked in private
250                     bot.reply('The documentation for this command is too long; '
251                               'I\'m sending it to you in a private message.')
252 
253                 def msgfun(l):
254                     bot.say(l, trigger.nick)
255             else:
256                 msgfun = bot.reply
257 
258             for line in bot.doc[name][0]:
259                 msgfun(line)
260             if bot.doc[name][1]:
261                 # Build a nice, grammatically-correct list of examples
262                 examples = ', '.join(bot.doc[name][1][:-2] + [' or '.join(bot.doc[name][1][-2:])])
263                 msgfun('e.g. ' + examples)
264     else:
265         # This'll probably catch most cases, without having to spend the time
266         # actually creating the list first. Maybe worth storing the link and a
267         # heuristic in the DB, too, so it persists across restarts. Would need a
268         # command to regenerate, too...
269         if (
270             'command-list' in bot.memory and
271             bot.memory['command-list'][0] == len(bot.command_groups) and
272             is_cache_valid(bot)
273         ):
274             url = bot.memory['command-list'][1]
275         else:
276             bot.say("Hang on, I'm creating a list.")
277             msgs = []
278 
279             name_length = max(6, max(len(k) for k in bot.command_groups.keys()))
280             for category, cmds in collections.OrderedDict(sorted(bot.command_groups.items())).items():
281                 category = category.upper().ljust(name_length)
282                 cmds = set(cmds)  # remove duplicates
283                 cmds = '  '.join(cmds)
284                 msg = category + '  ' + cmds
285                 indent = ' ' * (name_length + 2)
286                 # Honestly not sure why this is a list here
287                 msgs.append('\n'.join(textwrap.wrap(msg, subsequent_indent=indent)))
288 
289             url = create_list(bot, '\n\n'.join(msgs))
290             if not url:
291                 return
292             bot.memory['command-list'] = (len(bot.command_groups), url)
293             update_cache(bot)
294         bot.say("I've posted a list of my commands at {0} - You can see "
295                 "more info about any of these commands by doing {1}help "
296                 "<command> (e.g. {1}help time)"
297                 .format(url, bot.config.core.help_prefix))
298 
299 
300 def create_list(bot, msg):
301     """Creates & uploads the command list.
302 
303     Returns the URL from the chosen pastebin provider.
304     """
305     msg = 'Command listing for {}{}\n\n{}'.format(
306         bot.nick,
307         ('@' + bot.config.core.host) if bot.config.help.show_server_host else '',
308         msg)
309 
310     try:
311         result = PASTEBIN_PROVIDERS[bot.config.help.output](msg)
312     except PostingException:
313         bot.say("Sorry! Something went wrong.")
314         LOGGER.exception("Error posting commands")
315         return
316     return result
317 
318 
319 @rule('$nick' r'(?i)help(?:[?!]+)?$')
320 @priority('low')
321 def help2(bot, trigger):
322     response = (
323         "Hi, I'm a bot. Say {1}commands to me in private for a list "
324         "of my commands, or see https://sopel.chat for more "
325         "general details. My owner is {0}."
326         .format(bot.config.core.owner, bot.config.core.help_prefix))
327     bot.reply(response)
328
```
--- 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/help.py b/sopel/modules/help.py
--- a/sopel/modules/help.py
+++ b/sopel/modules/help.py
@@ -167,6 +167,11 @@
     'termbin': post_to_termbin,
     'ubuntu': post_to_ubuntu,
 }
+REPLY_METHODS = [
+    'channel',
+    'query',
+    'notice',
+]
 
 
 class HelpSection(StaticSection):
@@ -175,6 +180,10 @@
                              list(PASTEBIN_PROVIDERS),
                              default='clbin')
     """The pastebin provider to use for help output."""
+    reply_method = ChoiceAttribute('reply_method',
+                                   REPLY_METHODS,
+                                   default='channel')
+    """Where/how to reply to help commands (public/private)."""
     show_server_host = ValidatedAttribute('show_server_host', bool, default=True)
     """Show the IRC server's hostname/IP in the first line of the help listing?"""
 
@@ -184,14 +193,20 @@
     | name | example | purpose |
     | ---- | ------- | ------- |
     | output | clbin | The pastebin provider to use for help output |
+    | reply\\_method | channel | How/where help output should be sent |
     | show\\_server\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |
     """
     config.define_section('help', HelpSection)
     provider_list = ', '.join(PASTEBIN_PROVIDERS)
+    reply_method_list = ', '.join(REPLY_METHODS)
     config.help.configure_setting(
         'output',
         'Pick a pastebin provider: {}: '.format(provider_list)
     )
+    config.help.configure_setting(
+        'reply_method',
+        'How/where should help command replies be sent: {}? '.format(reply_method_list)
+    )
     config.help.configure_setting(
         'show_server_host',
         'Should the help command show the IRC server\'s hostname/IP in the listing?'
@@ -235,6 +250,16 @@
 @priority('low')
 def help(bot, trigger):
     """Shows a command's documentation, and an example if available. With no arguments, lists all commands."""
+    if bot.config.help.reply_method == 'query':
+        def respond(text):
+            bot.say(text, trigger.nick)
+    elif bot.config.help.reply_method == 'notice':
+        def respond(text):
+            bot.notice(text, trigger.nick)
+    else:
+        def respond(text):
+            bot.say(text, trigger.sender)
+
     if trigger.group(2):
         name = trigger.group(2)
         name = name.lower()
@@ -253,7 +278,7 @@
                 def msgfun(l):
                     bot.say(l, trigger.nick)
             else:
-                msgfun = bot.reply
+                msgfun = respond
 
             for line in bot.doc[name][0]:
                 msgfun(line)
@@ -273,7 +298,7 @@
         ):
             url = bot.memory['command-list'][1]
         else:
-            bot.say("Hang on, I'm creating a list.")
+            respond("Hang on, I'm creating a list.")
             msgs = []
 
             name_length = max(6, max(len(k) for k in bot.command_groups.keys()))
@@ -291,7 +316,7 @@
                 return
             bot.memory['command-list'] = (len(bot.command_groups), url)
             update_cache(bot)
-        bot.say("I've posted a list of my commands at {0} - You can see "
+        respond("I've posted a list of my commands at {0} - You can see "
                 "more info about any of these commands by doing {1}help "
                 "<command> (e.g. {1}help time)"
                 .format(url, bot.config.core.help_prefix))

{"golden_diff": "diff --git a/sopel/modules/help.py b/sopel/modules/help.py\n--- a/sopel/modules/help.py\n+++ b/sopel/modules/help.py\n@@ -167,6 +167,11 @@\n     'termbin': post_to_termbin,\n     'ubuntu': post_to_ubuntu,\n }\n+REPLY_METHODS = [\n+    'channel',\n+    'query',\n+    'notice',\n+]\n \n \n class HelpSection(StaticSection):\n@@ -175,6 +180,10 @@\n                              list(PASTEBIN_PROVIDERS),\n                              default='clbin')\n     \"\"\"The pastebin provider to use for help output.\"\"\"\n+    reply_method = ChoiceAttribute('reply_method',\n+                                   REPLY_METHODS,\n+                                   default='channel')\n+    \"\"\"Where/how to reply to help commands (public/private).\"\"\"\n     show_server_host = ValidatedAttribute('show_server_host', bool, default=True)\n     \"\"\"Show the IRC server's hostname/IP in the first line of the help listing?\"\"\"\n \n@@ -184,14 +193,20 @@\n     | name | example | purpose |\n     | ---- | ------- | ------- |\n     | output | clbin | The pastebin provider to use for help output |\n+    | reply\\\\_method | channel | How/where help output should be sent |\n     | show\\\\_server\\\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |\n     \"\"\"\n     config.define_section('help', HelpSection)\n     provider_list = ', '.join(PASTEBIN_PROVIDERS)\n+    reply_method_list = ', '.join(REPLY_METHODS)\n     config.help.configure_setting(\n         'output',\n         'Pick a pastebin provider: {}: '.format(provider_list)\n     )\n+    config.help.configure_setting(\n+        'reply_method',\n+        'How/where should help command replies be sent: {}? '.format(reply_method_list)\n+    )\n     config.help.configure_setting(\n         'show_server_host',\n         'Should the help command show the IRC server\\'s hostname/IP in the listing?'\n@@ -235,6 +250,16 @@\n @priority('low')\n def help(bot, trigger):\n     \"\"\"Shows a command's documentation, and an example if available. With no arguments, lists all commands.\"\"\"\n+    if bot.config.help.reply_method == 'query':\n+        def respond(text):\n+            bot.say(text, trigger.nick)\n+    elif bot.config.help.reply_method == 'notice':\n+        def respond(text):\n+            bot.notice(text, trigger.nick)\n+    else:\n+        def respond(text):\n+            bot.say(text, trigger.sender)\n+\n     if trigger.group(2):\n         name = trigger.group(2)\n         name = name.lower()\n@@ -253,7 +278,7 @@\n                 def msgfun(l):\n                     bot.say(l, trigger.nick)\n             else:\n-                msgfun = bot.reply\n+                msgfun = respond\n \n             for line in bot.doc[name][0]:\n                 msgfun(line)\n@@ -273,7 +298,7 @@\n         ):\n             url = bot.memory['command-list'][1]\n         else:\n-            bot.say(\"Hang on, I'm creating a list.\")\n+            respond(\"Hang on, I'm creating a list.\")\n             msgs = []\n \n             name_length = max(6, max(len(k) for k in bot.command_groups.keys()))\n@@ -291,7 +316,7 @@\n                 return\n             bot.memory['command-list'] = (len(bot.command_groups), url)\n             update_cache(bot)\n-        bot.say(\"I've posted a list of my commands at {0} - You can see \"\n+        respond(\"I've posted a list of my commands at {0} - You can see \"\n                 \"more info about any of these commands by doing {1}help \"\n                 \"<command> (e.g. {1}help time)\"\n                 .format(url, bot.config.core.help_prefix))\n", "issue": "help: output should be sent to requester privately\nIIRC, the older version of Sopel (or Willie?) used to send the long help results privately to the user who requested the help command list results. Or at least make it optional to admin who wants gisthub or privately.\r\n\r\nThank you in advance. :)\n", "before_files": [{"content": "# coding=utf-8\n\"\"\"\nhelp.py - Sopel Help Module\nCopyright 2008, Sean B. Palmer, inamidst.com\nCopyright \u00a9 2013, Elad Alfassa, <elad@fedoraproject.org>\nCopyright \u00a9 2018, Adam Erdman, pandorah.org\nCopyright \u00a9 2019, Tomasz Kurcz, github.com/uint\nCopyright \u00a9 2019, dgw, technobabbl.es\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nimport collections\nimport socket\nimport textwrap\n\nimport requests\n\nfrom sopel.config.types import ChoiceAttribute, ValidatedAttribute, StaticSection\nfrom sopel.logger import get_logger\nfrom sopel.module import commands, rule, example, priority\nfrom sopel.tools import SopelMemory\n\n\nSETTING_CACHE_NAMESPACE = 'help-setting-cache'  # Set top-level memory key name\nLOGGER = get_logger(__name__)\n\n# Settings that should require the help listing to be regenerated, or\n# re-POSTed to paste, if they are changed during runtime.\n# Keys are module names, and values are lists of setting names\n# specific to that module.\nTRACKED_SETTINGS = {\n    'help': [\n        'output',\n        'show_server_host',\n    ]\n}\n\n\nclass PostingException(Exception):\n    \"\"\"Custom exception type for errors posting help to the chosen pastebin.\"\"\"\n    pass\n\n\n# Pastebin handlers\n\n\ndef _requests_post_catch_errors(*args, **kwargs):\n    try:\n        response = requests.post(*args, **kwargs)\n        response.raise_for_status()\n    except (\n            requests.exceptions.Timeout,\n            requests.exceptions.TooManyRedirects,\n            requests.exceptions.RequestException,\n            requests.exceptions.HTTPError\n    ):\n        # We re-raise all expected exception types to a generic \"posting error\"\n        # that's easy for callers to expect, and then we pass the original\n        # exception through to provide some debugging info\n        LOGGER.exception('Error during POST request')\n        raise PostingException('Could not communicate with remote service')\n\n    # remaining handling (e.g. errors inside the response) is left to the caller\n    return response\n\n\ndef post_to_clbin(msg):\n    try:\n        result = _requests_post_catch_errors('https://clbin.com/', data={'clbin': msg})\n    except PostingException:\n        raise\n\n    result = result.text\n    if '://clbin.com/' in result:\n        # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n        # though it will probably send us an HTTPS link without any tricks.\n        return result.replace('http://', 'https://', 1)\n    else:\n        LOGGER.error(\"Invalid result %s\", result)\n        raise PostingException('clbin result did not contain expected URL base.')\n\n\ndef post_to_0x0(msg):\n    try:\n        result = _requests_post_catch_errors('https://0x0.st', files={'file': msg})\n    except PostingException:\n        raise\n\n    result = result.text\n    if '://0x0.st' in result:\n        # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n        # though it will probably send us an HTTPS link without any tricks.\n        return result.replace('http://', 'https://', 1)\n    else:\n        LOGGER.error('Invalid result %s', result)\n        raise PostingException('0x0.st result did not contain expected URL base.')\n\n\ndef post_to_hastebin(msg):\n    try:\n        result = _requests_post_catch_errors('https://hastebin.com/documents', data=msg)\n    except PostingException:\n        raise\n\n    try:\n        result = result.json()\n    except ValueError:\n        LOGGER.error(\"Invalid Hastebin response %s\", result)\n        raise PostingException('Could not parse response from Hastebin!')\n\n    if 'key' not in result:\n        LOGGER.error(\"Invalid result %s\", result)\n        raise PostingException('Hastebin result did not contain expected URL base.')\n    return \"https://hastebin.com/\" + result['key']\n\n\ndef post_to_termbin(msg):\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.settimeout(10)  # the bot may NOT wait forever for a response; that would be bad\n    try:\n        sock.connect(('termbin.com', 9999))\n        sock.sendall(msg)\n        sock.shutdown(socket.SHUT_WR)\n        response = \"\"\n        while 1:\n            data = sock.recv(1024)\n            if data == \"\":\n                break\n            response += data\n        sock.close()\n    except socket.error:\n        LOGGER.exception('Error during communication with termbin')\n        raise PostingException('Error uploading to termbin')\n\n    # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n    # though it will probably send us an HTTPS link without any tricks.\n    return response.strip('\\x00\\n').replace('http://', 'https://', 1)\n\n\ndef post_to_ubuntu(msg):\n    data = {\n        'poster': 'sopel',\n        'syntax': 'text',\n        'expiration': '',\n        'content': msg,\n    }\n    try:\n        result = _requests_post_catch_errors('https://pastebin.ubuntu.com/', data=data)\n    except PostingException:\n        raise\n\n    if not re.match(r'https://pastebin.ubuntu.com/p/[^/]+/', result.url):\n        LOGGER.error(\"Invalid Ubuntu pastebin response url %s\", result.url)\n        raise PostingException('Invalid response from Ubuntu pastebin: %s' % result.url)\n\n    return result.url\n\n\nPASTEBIN_PROVIDERS = {\n    'clbin': post_to_clbin,\n    '0x0': post_to_0x0,\n    'hastebin': post_to_hastebin,\n    'termbin': post_to_termbin,\n    'ubuntu': post_to_ubuntu,\n}\n\n\nclass HelpSection(StaticSection):\n    \"\"\"Configuration section for this module.\"\"\"\n    output = ChoiceAttribute('output',\n                             list(PASTEBIN_PROVIDERS),\n                             default='clbin')\n    \"\"\"The pastebin provider to use for help output.\"\"\"\n    show_server_host = ValidatedAttribute('show_server_host', bool, default=True)\n    \"\"\"Show the IRC server's hostname/IP in the first line of the help listing?\"\"\"\n\n\ndef configure(config):\n    \"\"\"\n    | name | example | purpose |\n    | ---- | ------- | ------- |\n    | output | clbin | The pastebin provider to use for help output |\n    | show\\\\_server\\\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |\n    \"\"\"\n    config.define_section('help', HelpSection)\n    provider_list = ', '.join(PASTEBIN_PROVIDERS)\n    config.help.configure_setting(\n        'output',\n        'Pick a pastebin provider: {}: '.format(provider_list)\n    )\n    config.help.configure_setting(\n        'show_server_host',\n        'Should the help command show the IRC server\\'s hostname/IP in the listing?'\n    )\n\n\ndef setup(bot):\n    bot.config.define_section('help', HelpSection)\n\n    # Initialize memory\n    if SETTING_CACHE_NAMESPACE not in bot.memory:\n        bot.memory[SETTING_CACHE_NAMESPACE] = SopelMemory()\n\n    # Initialize settings cache\n    for section in TRACKED_SETTINGS:\n        if section not in bot.memory[SETTING_CACHE_NAMESPACE]:\n            bot.memory[SETTING_CACHE_NAMESPACE][section] = SopelMemory()\n\n    update_cache(bot)  # Populate cache\n\n    bot.config.define_section('help', HelpSection)\n\n\ndef update_cache(bot):\n    for section, setting_names_list in TRACKED_SETTINGS.items():\n        for setting_name in setting_names_list:\n            bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] = getattr(getattr(bot.config, section), setting_name)\n\n\ndef is_cache_valid(bot):\n    for section, setting_names_list in TRACKED_SETTINGS.items():\n        for setting_name in setting_names_list:\n            if bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] != getattr(getattr(bot.config, section), setting_name):\n                return False\n    return True\n\n\n@rule('$nick' r'(?i)(help|doc) +([A-Za-z]+)(?:\\?+)?$')\n@example('.help tell')\n@commands('help', 'commands')\n@priority('low')\ndef help(bot, trigger):\n    \"\"\"Shows a command's documentation, and an example if available. With no arguments, lists all commands.\"\"\"\n    if trigger.group(2):\n        name = trigger.group(2)\n        name = name.lower()\n\n        # number of lines of help to show\n        threshold = 3\n\n        if name in bot.doc:\n            # count lines we're going to send\n            # lines in command docstring, plus one line for example(s) if present (they're sent all on one line)\n            if len(bot.doc[name][0]) + int(bool(bot.doc[name][1])) > threshold:\n                if trigger.nick != trigger.sender:  # don't say that if asked in private\n                    bot.reply('The documentation for this command is too long; '\n                              'I\\'m sending it to you in a private message.')\n\n                def msgfun(l):\n                    bot.say(l, trigger.nick)\n            else:\n                msgfun = bot.reply\n\n            for line in bot.doc[name][0]:\n                msgfun(line)\n            if bot.doc[name][1]:\n                # Build a nice, grammatically-correct list of examples\n                examples = ', '.join(bot.doc[name][1][:-2] + [' or '.join(bot.doc[name][1][-2:])])\n                msgfun('e.g. ' + examples)\n    else:\n        # This'll probably catch most cases, without having to spend the time\n        # actually creating the list first. Maybe worth storing the link and a\n        # heuristic in the DB, too, so it persists across restarts. Would need a\n        # command to regenerate, too...\n        if (\n            'command-list' in bot.memory and\n            bot.memory['command-list'][0] == len(bot.command_groups) and\n            is_cache_valid(bot)\n        ):\n            url = bot.memory['command-list'][1]\n        else:\n            bot.say(\"Hang on, I'm creating a list.\")\n            msgs = []\n\n            name_length = max(6, max(len(k) for k in bot.command_groups.keys()))\n            for category, cmds in collections.OrderedDict(sorted(bot.command_groups.items())).items():\n                category = category.upper().ljust(name_length)\n                cmds = set(cmds)  # remove duplicates\n                cmds = '  '.join(cmds)\n                msg = category + '  ' + cmds\n                indent = ' ' * (name_length + 2)\n                # Honestly not sure why this is a list here\n                msgs.append('\\n'.join(textwrap.wrap(msg, subsequent_indent=indent)))\n\n            url = create_list(bot, '\\n\\n'.join(msgs))\n            if not url:\n                return\n            bot.memory['command-list'] = (len(bot.command_groups), url)\n            update_cache(bot)\n        bot.say(\"I've posted a list of my commands at {0} - You can see \"\n                \"more info about any of these commands by doing {1}help \"\n                \"<command> (e.g. {1}help time)\"\n                .format(url, bot.config.core.help_prefix))\n\n\ndef create_list(bot, msg):\n    \"\"\"Creates & uploads the command list.\n\n    Returns the URL from the chosen pastebin provider.\n    \"\"\"\n    msg = 'Command listing for {}{}\\n\\n{}'.format(\n        bot.nick,\n        ('@' + bot.config.core.host) if bot.config.help.show_server_host else '',\n        msg)\n\n    try:\n        result = PASTEBIN_PROVIDERS[bot.config.help.output](msg)\n    except PostingException:\n        bot.say(\"Sorry! Something went wrong.\")\n        LOGGER.exception(\"Error posting commands\")\n        return\n    return result\n\n\n@rule('$nick' r'(?i)help(?:[?!]+)?$')\n@priority('low')\ndef help2(bot, trigger):\n    response = (\n        \"Hi, I'm a bot. Say {1}commands to me in private for a list \"\n        \"of my commands, or see https://sopel.chat for more \"\n        \"general details. My owner is {0}.\"\n        .format(bot.config.core.owner, bot.config.core.help_prefix))\n    bot.reply(response)\n", "path": "sopel/modules/help.py"}], "after_files": [{"content": "# coding=utf-8\n\"\"\"\nhelp.py - Sopel Help Module\nCopyright 2008, Sean B. Palmer, inamidst.com\nCopyright \u00a9 2013, Elad Alfassa, <elad@fedoraproject.org>\nCopyright \u00a9 2018, Adam Erdman, pandorah.org\nCopyright \u00a9 2019, Tomasz Kurcz, github.com/uint\nCopyright \u00a9 2019, dgw, technobabbl.es\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nimport collections\nimport socket\nimport textwrap\n\nimport requests\n\nfrom sopel.config.types import ChoiceAttribute, ValidatedAttribute, StaticSection\nfrom sopel.logger import get_logger\nfrom sopel.module import commands, rule, example, priority\nfrom sopel.tools import SopelMemory\n\n\nSETTING_CACHE_NAMESPACE = 'help-setting-cache'  # Set top-level memory key name\nLOGGER = get_logger(__name__)\n\n# Settings that should require the help listing to be regenerated, or\n# re-POSTed to paste, if they are changed during runtime.\n# Keys are module names, and values are lists of setting names\n# specific to that module.\nTRACKED_SETTINGS = {\n    'help': [\n        'output',\n        'show_server_host',\n    ]\n}\n\n\nclass PostingException(Exception):\n    \"\"\"Custom exception type for errors posting help to the chosen pastebin.\"\"\"\n    pass\n\n\n# Pastebin handlers\n\n\ndef _requests_post_catch_errors(*args, **kwargs):\n    try:\n        response = requests.post(*args, **kwargs)\n        response.raise_for_status()\n    except (\n            requests.exceptions.Timeout,\n            requests.exceptions.TooManyRedirects,\n            requests.exceptions.RequestException,\n            requests.exceptions.HTTPError\n    ):\n        # We re-raise all expected exception types to a generic \"posting error\"\n        # that's easy for callers to expect, and then we pass the original\n        # exception through to provide some debugging info\n        LOGGER.exception('Error during POST request')\n        raise PostingException('Could not communicate with remote service')\n\n    # remaining handling (e.g. errors inside the response) is left to the caller\n    return response\n\n\ndef post_to_clbin(msg):\n    try:\n        result = _requests_post_catch_errors('https://clbin.com/', data={'clbin': msg})\n    except PostingException:\n        raise\n\n    result = result.text\n    if '://clbin.com/' in result:\n        # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n        # though it will probably send us an HTTPS link without any tricks.\n        return result.replace('http://', 'https://', 1)\n    else:\n        LOGGER.error(\"Invalid result %s\", result)\n        raise PostingException('clbin result did not contain expected URL base.')\n\n\ndef post_to_0x0(msg):\n    try:\n        result = _requests_post_catch_errors('https://0x0.st', files={'file': msg})\n    except PostingException:\n        raise\n\n    result = result.text\n    if '://0x0.st' in result:\n        # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n        # though it will probably send us an HTTPS link without any tricks.\n        return result.replace('http://', 'https://', 1)\n    else:\n        LOGGER.error('Invalid result %s', result)\n        raise PostingException('0x0.st result did not contain expected URL base.')\n\n\ndef post_to_hastebin(msg):\n    try:\n        result = _requests_post_catch_errors('https://hastebin.com/documents', data=msg)\n    except PostingException:\n        raise\n\n    try:\n        result = result.json()\n    except ValueError:\n        LOGGER.error(\"Invalid Hastebin response %s\", result)\n        raise PostingException('Could not parse response from Hastebin!')\n\n    if 'key' not in result:\n        LOGGER.error(\"Invalid result %s\", result)\n        raise PostingException('Hastebin result did not contain expected URL base.')\n    return \"https://hastebin.com/\" + result['key']\n\n\ndef post_to_termbin(msg):\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.settimeout(10)  # the bot may NOT wait forever for a response; that would be bad\n    try:\n        sock.connect(('termbin.com', 9999))\n        sock.sendall(msg)\n        sock.shutdown(socket.SHUT_WR)\n        response = \"\"\n        while 1:\n            data = sock.recv(1024)\n            if data == \"\":\n                break\n            response += data\n        sock.close()\n    except socket.error:\n        LOGGER.exception('Error during communication with termbin')\n        raise PostingException('Error uploading to termbin')\n\n    # find/replace just in case the site tries to be sneaky and save on SSL overhead,\n    # though it will probably send us an HTTPS link without any tricks.\n    return response.strip('\\x00\\n').replace('http://', 'https://', 1)\n\n\ndef post_to_ubuntu(msg):\n    data = {\n        'poster': 'sopel',\n        'syntax': 'text',\n        'expiration': '',\n        'content': msg,\n    }\n    try:\n        result = _requests_post_catch_errors('https://pastebin.ubuntu.com/', data=data)\n    except PostingException:\n        raise\n\n    if not re.match(r'https://pastebin.ubuntu.com/p/[^/]+/', result.url):\n        LOGGER.error(\"Invalid Ubuntu pastebin response url %s\", result.url)\n        raise PostingException('Invalid response from Ubuntu pastebin: %s' % result.url)\n\n    return result.url\n\n\nPASTEBIN_PROVIDERS = {\n    'clbin': post_to_clbin,\n    '0x0': post_to_0x0,\n    'hastebin': post_to_hastebin,\n    'termbin': post_to_termbin,\n    'ubuntu': post_to_ubuntu,\n}\nREPLY_METHODS = [\n    'channel',\n    'query',\n    'notice',\n]\n\n\nclass HelpSection(StaticSection):\n    \"\"\"Configuration section for this module.\"\"\"\n    output = ChoiceAttribute('output',\n                             list(PASTEBIN_PROVIDERS),\n                             default='clbin')\n    \"\"\"The pastebin provider to use for help output.\"\"\"\n    reply_method = ChoiceAttribute('reply_method',\n                                   REPLY_METHODS,\n                                   default='channel')\n    \"\"\"Where/how to reply to help commands (public/private).\"\"\"\n    show_server_host = ValidatedAttribute('show_server_host', bool, default=True)\n    \"\"\"Show the IRC server's hostname/IP in the first line of the help listing?\"\"\"\n\n\ndef configure(config):\n    \"\"\"\n    | name | example | purpose |\n    | ---- | ------- | ------- |\n    | output | clbin | The pastebin provider to use for help output |\n    | reply\\\\_method | channel | How/where help output should be sent |\n    | show\\\\_server\\\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |\n    \"\"\"\n    config.define_section('help', HelpSection)\n    provider_list = ', '.join(PASTEBIN_PROVIDERS)\n    reply_method_list = ', '.join(REPLY_METHODS)\n    config.help.configure_setting(\n        'output',\n        'Pick a pastebin provider: {}: '.format(provider_list)\n    )\n    config.help.configure_setting(\n        'reply_method',\n        'How/where should help command replies be sent: {}? '.format(reply_method_list)\n    )\n    config.help.configure_setting(\n        'show_server_host',\n        'Should the help command show the IRC server\\'s hostname/IP in the listing?'\n    )\n\n\ndef setup(bot):\n    bot.config.define_section('help', HelpSection)\n\n    # Initialize memory\n    if SETTING_CACHE_NAMESPACE not in bot.memory:\n        bot.memory[SETTING_CACHE_NAMESPACE] = SopelMemory()\n\n    # Initialize settings cache\n    for section in TRACKED_SETTINGS:\n        if section not in bot.memory[SETTING_CACHE_NAMESPACE]:\n            bot.memory[SETTING_CACHE_NAMESPACE][section] = SopelMemory()\n\n    update_cache(bot)  # Populate cache\n\n    bot.config.define_section('help', HelpSection)\n\n\ndef update_cache(bot):\n    for section, setting_names_list in TRACKED_SETTINGS.items():\n        for setting_name in setting_names_list:\n            bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] = getattr(getattr(bot.config, section), setting_name)\n\n\ndef is_cache_valid(bot):\n    for section, setting_names_list in TRACKED_SETTINGS.items():\n        for setting_name in setting_names_list:\n            if bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] != getattr(getattr(bot.config, section), setting_name):\n                return False\n    return True\n\n\n@rule('$nick' r'(?i)(help|doc) +([A-Za-z]+)(?:\\?+)?$')\n@example('.help tell')\n@commands('help', 'commands')\n@priority('low')\ndef help(bot, trigger):\n    \"\"\"Shows a command's documentation, and an example if available. With no arguments, lists all commands.\"\"\"\n    if bot.config.help.reply_method == 'query':\n        def respond(text):\n            bot.say(text, trigger.nick)\n    elif bot.config.help.reply_method == 'notice':\n        def respond(text):\n            bot.notice(text, trigger.nick)\n    else:\n        def respond(text):\n            bot.say(text, trigger.sender)\n\n    if trigger.group(2):\n        name = trigger.group(2)\n        name = name.lower()\n\n        # number of lines of help to show\n        threshold = 3\n\n        if name in bot.doc:\n            # count lines we're going to send\n            # lines in command docstring, plus one line for example(s) if present (they're sent all on one line)\n            if len(bot.doc[name][0]) + int(bool(bot.doc[name][1])) > threshold:\n                if trigger.nick != trigger.sender:  # don't say that if asked in private\n                    bot.reply('The documentation for this command is too long; '\n                              'I\\'m sending it to you in a private message.')\n\n                def msgfun(l):\n                    bot.say(l, trigger.nick)\n            else:\n                msgfun = respond\n\n            for line in bot.doc[name][0]:\n                msgfun(line)\n            if bot.doc[name][1]:\n                # Build a nice, grammatically-correct list of examples\n                examples = ', '.join(bot.doc[name][1][:-2] + [' or '.join(bot.doc[name][1][-2:])])\n                msgfun('e.g. ' + examples)\n    else:\n        # This'll probably catch most cases, without having to spend the time\n        # actually creating the list first. Maybe worth storing the link and a\n        # heuristic in the DB, too, so it persists across restarts. Would need a\n        # command to regenerate, too...\n        if (\n            'command-list' in bot.memory and\n            bot.memory['command-list'][0] == len(bot.command_groups) and\n            is_cache_valid(bot)\n        ):\n            url = bot.memory['command-list'][1]\n        else:\n            respond(\"Hang on, I'm creating a list.\")\n            msgs = []\n\n            name_length = max(6, max(len(k) for k in bot.command_groups.keys()))\n            for category, cmds in collections.OrderedDict(sorted(bot.command_groups.items())).items():\n                category = category.upper().ljust(name_length)\n                cmds = set(cmds)  # remove duplicates\n                cmds = '  '.join(cmds)\n                msg = category + '  ' + cmds\n                indent = ' ' * (name_length + 2)\n                # Honestly not sure why this is a list here\n                msgs.append('\\n'.join(textwrap.wrap(msg, subsequent_indent=indent)))\n\n            url = create_list(bot, '\\n\\n'.join(msgs))\n            if not url:\n                return\n            bot.memory['command-list'] = (len(bot.command_groups), url)\n            update_cache(bot)\n        respond(\"I've posted a list of my commands at {0} - You can see \"\n                \"more info about any of these commands by doing {1}help \"\n                \"<command> (e.g. {1}help time)\"\n                .format(url, bot.config.core.help_prefix))\n\n\ndef create_list(bot, msg):\n    \"\"\"Creates & uploads the command list.\n\n    Returns the URL from the chosen pastebin provider.\n    \"\"\"\n    msg = 'Command listing for {}{}\\n\\n{}'.format(\n        bot.nick,\n        ('@' + bot.config.core.host) if bot.config.help.show_server_host else '',\n        msg)\n\n    try:\n        result = PASTEBIN_PROVIDERS[bot.config.help.output](msg)\n    except PostingException:\n        bot.say(\"Sorry! Something went wrong.\")\n        LOGGER.exception(\"Error posting commands\")\n        return\n    return result\n\n\n@rule('$nick' r'(?i)help(?:[?!]+)?$')\n@priority('low')\ndef help2(bot, trigger):\n    response = (\n        \"Hi, I'm a bot. Say {1}commands to me in private for a list \"\n        \"of my commands, or see https://sopel.chat for more \"\n        \"general details. My owner is {0}.\"\n        .format(bot.config.core.owner, bot.config.core.help_prefix))\n    bot.reply(response)\n", "path": "sopel/modules/help.py"}]}