Skip to content

Commit f671428

Browse files
Exireldgw
andcommitted
help: replace built-in help by sopel-help
The help built-in plugin is obsolete, and replaced by sopel-help which can be installed with pip install sopel-help (as a standalone install) or when installing Sopel through pip. A warning has been added to the sopel.modules.help.setup() in case someone install Sopel without its dependencies. All rules/commands have been removed. Thank you for your service, help, you will be remembered. Co-authored-by: dgw <[email protected]>
1 parent 2e90817 commit f671428

File tree

2 files changed

+8
-339
lines changed

2 files changed

+8
-339
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
"sqlalchemy>=1.4,<1.5",
4343
"importlib_metadata>=3.6",
4444
"packaging",
45+
"sopel-help>=0.4.0",
4546
]
4647

4748
[project.urls]

sopel/modules/help.py

Lines changed: 7 additions & 339 deletions
Original file line numberDiff line numberDiff line change
@@ -1,350 +1,18 @@
1-
"""
2-
help.py - Sopel Help Plugin
3-
Copyright 2008, Sean B. Palmer, inamidst.com
4-
Copyright © 2013, Elad Alfassa, <[email protected]>
5-
Copyright © 2018, Adam Erdman, pandorah.org
6-
Copyright © 2019, Tomasz Kurcz, github.com/uint
7-
Copyright © 2019, dgw, technobabbl.es
8-
Licensed under the Eiffel Forum License 2.
1+
"""help.py - Obsolete Sopel Help Plugin
92
10-
https://sopel.chat
3+
Install ``sopel-help`` with ``pip install sopel-help`` to get the official
4+
help plugin for Sopel.
115
"""
126
from __future__ import annotations
137

14-
import collections
158
import logging
16-
import re
17-
import socket
18-
import textwrap
19-
20-
import requests
219

22-
from sopel import plugin, tools
23-
from sopel.config import types
2410

25-
26-
SETTING_CACHE_NAMESPACE = 'help-setting-cache' # Set top-level memory key name
2711
LOGGER = logging.getLogger(__name__)
2812

29-
# Settings that should require the help listing to be regenerated, or
30-
# re-POSTed to paste, if they are changed during runtime.
31-
# Keys are plugin names, and values are lists of setting names
32-
# specific to that plugin.
33-
TRACKED_SETTINGS = {
34-
'help': [
35-
'output',
36-
'show_server_host',
37-
]
38-
}
39-
40-
41-
class PostingException(Exception):
42-
"""Custom exception type for errors posting help to the chosen pastebin."""
43-
pass
44-
45-
46-
# Pastebin handlers
47-
48-
49-
def _requests_post_catch_errors(*args, **kwargs):
50-
try:
51-
response = requests.post(*args, **kwargs)
52-
response.raise_for_status()
53-
except (
54-
requests.exceptions.Timeout,
55-
requests.exceptions.TooManyRedirects,
56-
requests.exceptions.RequestException,
57-
requests.exceptions.HTTPError
58-
):
59-
# We re-raise all expected exception types to a generic "posting error"
60-
# that's easy for callers to expect, and then we pass the original
61-
# exception through to provide some debugging info
62-
LOGGER.exception('Error during POST request')
63-
raise PostingException('Could not communicate with remote service')
64-
65-
# remaining handling (e.g. errors inside the response) is left to the caller
66-
return response
67-
68-
69-
def post_to_clbin(msg):
70-
try:
71-
result = _requests_post_catch_errors('https://clbin.com/', data={'clbin': msg})
72-
except PostingException:
73-
raise
74-
75-
result = result.text
76-
if re.match(r'https?://clbin\.com/', result):
77-
# find/replace just in case the site tries to be sneaky and save on SSL overhead,
78-
# though it will probably send us an HTTPS link without any tricks.
79-
return result.replace('http://', 'https://', 1)
80-
else:
81-
LOGGER.error("Invalid result %s", result)
82-
raise PostingException('clbin result did not contain expected URL base.')
83-
84-
85-
def post_to_0x0(msg):
86-
try:
87-
result = _requests_post_catch_errors('https://0x0.st', files={'file': msg})
88-
except PostingException:
89-
raise
90-
91-
result = result.text
92-
if re.match(r'https?://0x0\.st/', result):
93-
# find/replace just in case the site tries to be sneaky and save on SSL overhead,
94-
# though it will probably send us an HTTPS link without any tricks.
95-
return result.replace('http://', 'https://', 1)
96-
else:
97-
LOGGER.error('Invalid result %s', result)
98-
raise PostingException('0x0.st result did not contain expected URL base.')
99-
100-
101-
def post_to_hastebin(msg):
102-
try:
103-
result = _requests_post_catch_errors('https://hastebin.com/documents', data=msg)
104-
except PostingException:
105-
raise
106-
107-
try:
108-
result = result.json()
109-
except ValueError:
110-
LOGGER.error("Invalid Hastebin response %s", result)
111-
raise PostingException('Could not parse response from Hastebin!')
112-
113-
if 'key' not in result:
114-
LOGGER.error("Invalid result %s", result)
115-
raise PostingException('Hastebin result did not contain expected URL base.')
116-
return "https://hastebin.com/" + result['key']
117-
118-
119-
def post_to_termbin(msg):
120-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
121-
sock.settimeout(10) # the bot may NOT wait forever for a response; that would be bad
122-
try:
123-
sock.connect(('termbin.com', 9999))
124-
sock.sendall(msg)
125-
sock.shutdown(socket.SHUT_WR)
126-
response = ""
127-
while 1:
128-
data = sock.recv(1024)
129-
if data == "":
130-
break
131-
response += data
132-
sock.close()
133-
except socket.error:
134-
LOGGER.exception('Error during communication with termbin')
135-
raise PostingException('Error uploading to termbin')
136-
137-
# find/replace just in case the site tries to be sneaky and save on SSL overhead,
138-
# though it will probably send us an HTTPS link without any tricks.
139-
return response.strip('\x00\n').replace('http://', 'https://', 1)
140-
141-
142-
def post_to_ubuntu(msg):
143-
data = {
144-
'poster': 'sopel',
145-
'syntax': 'text',
146-
'expiration': '',
147-
'content': msg,
148-
}
149-
result = _requests_post_catch_errors(
150-
'https://pastebin.ubuntu.com/', data=data)
151-
152-
if not re.match(r'https://pastebin\.ubuntu\.com/p/[^/]+/', result.url):
153-
LOGGER.error("Invalid Ubuntu pastebin response url %s", result.url)
154-
raise PostingException(
155-
'Invalid response from Ubuntu pastebin: %s' % result.url)
156-
157-
return result.url
158-
159-
160-
PASTEBIN_PROVIDERS = {
161-
'clbin': post_to_clbin,
162-
'0x0': post_to_0x0,
163-
'hastebin': post_to_hastebin,
164-
'termbin': post_to_termbin,
165-
'ubuntu': post_to_ubuntu,
166-
}
167-
REPLY_METHODS = [
168-
'channel',
169-
'query',
170-
'notice',
171-
]
172-
173-
174-
class HelpSection(types.StaticSection):
175-
"""Configuration section for this plugin."""
176-
output = types.ChoiceAttribute('output',
177-
list(PASTEBIN_PROVIDERS),
178-
default='clbin')
179-
"""The pastebin provider to use for help output."""
180-
reply_method = types.ChoiceAttribute('reply_method',
181-
REPLY_METHODS,
182-
default='channel')
183-
"""Where/how to reply to help commands (public/private)."""
184-
show_server_host = types.BooleanAttribute('show_server_host',
185-
default=True)
186-
"""Show the IRC server's hostname/IP in the first line of the help listing?"""
187-
188-
189-
def configure(config):
190-
"""
191-
| name | example | purpose |
192-
| ---- | ------- | ------- |
193-
| output | clbin | The pastebin provider to use for help output |
194-
| reply\\_method | channel | How/where help output should be sent |
195-
| show\\_server\\_host | True | Whether to show the IRC server's hostname/IP at the top of command listings |
196-
"""
197-
config.define_section('help', HelpSection)
198-
provider_list = ', '.join(PASTEBIN_PROVIDERS)
199-
reply_method_list = ', '.join(REPLY_METHODS)
200-
config.help.configure_setting(
201-
'output',
202-
'Pick a pastebin provider: {}: '.format(provider_list)
203-
)
204-
config.help.configure_setting(
205-
'reply_method',
206-
'How/where should help command replies be sent: {}? '.format(reply_method_list)
207-
)
208-
config.help.configure_setting(
209-
'show_server_host',
210-
'Should the help command show the IRC server\'s hostname/IP in the listing?'
211-
)
212-
21313

21414
def setup(bot):
215-
bot.config.define_section('help', HelpSection)
216-
217-
# Initialize memory
218-
if SETTING_CACHE_NAMESPACE not in bot.memory:
219-
bot.memory[SETTING_CACHE_NAMESPACE] = tools.SopelMemory()
220-
221-
# Initialize settings cache
222-
for section in TRACKED_SETTINGS:
223-
if section not in bot.memory[SETTING_CACHE_NAMESPACE]:
224-
bot.memory[SETTING_CACHE_NAMESPACE][section] = tools.SopelMemory()
225-
226-
update_cache(bot) # Populate cache
227-
228-
bot.config.define_section('help', HelpSection)
229-
230-
231-
def update_cache(bot):
232-
for section, setting_names_list in TRACKED_SETTINGS.items():
233-
for setting_name in setting_names_list:
234-
bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] = getattr(getattr(bot.config, section), setting_name)
235-
236-
237-
def is_cache_valid(bot):
238-
for section, setting_names_list in TRACKED_SETTINGS.items():
239-
for setting_name in setting_names_list:
240-
if bot.memory[SETTING_CACHE_NAMESPACE][section][setting_name] != getattr(getattr(bot.config, section), setting_name):
241-
return False
242-
return True
243-
244-
245-
@plugin.rule('$nick' r'(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
246-
@plugin.example('.help tell')
247-
@plugin.command('help', 'commands')
248-
@plugin.priority('low')
249-
def help(bot, trigger):
250-
"""Shows a command's documentation, and an example if available. With no arguments, lists all commands."""
251-
if bot.config.help.reply_method == 'query':
252-
def respond(text):
253-
bot.say(text, trigger.nick)
254-
elif bot.config.help.reply_method == 'notice':
255-
def respond(text):
256-
bot.notice(text, trigger.nick)
257-
else:
258-
def respond(text):
259-
bot.say(text, trigger.sender)
260-
261-
if trigger.group(2):
262-
name = trigger.group(2)
263-
name = name.lower()
264-
265-
# number of lines of help to show
266-
threshold = 3
267-
268-
if name in bot.doc:
269-
# count lines we're going to send
270-
# lines in command docstring, plus one line for example(s) if present (they're sent all on one line)
271-
if len(bot.doc[name][0]) + int(bool(bot.doc[name][1])) > threshold:
272-
if trigger.nick != trigger.sender: # don't say that if asked in private
273-
bot.reply('The documentation for this command is too long; '
274-
'I\'m sending it to you in a private message.')
275-
276-
def msgfun(message):
277-
bot.say(message, trigger.nick)
278-
else:
279-
msgfun = respond
280-
281-
for line in bot.doc[name][0]:
282-
msgfun(line)
283-
if bot.doc[name][1]:
284-
# Build a nice, grammatically-correct list of examples
285-
examples = ', '.join(bot.doc[name][1][:-2] + [' or '.join(bot.doc[name][1][-2:])])
286-
msgfun('e.g. ' + examples)
287-
else:
288-
# This'll probably catch most cases, without having to spend the time
289-
# actually creating the list first. Maybe worth storing the link and a
290-
# heuristic in the DB, too, so it persists across restarts. Would need a
291-
# command to regenerate, too...
292-
if (
293-
'command-list' in bot.memory and
294-
bot.memory['command-list'][0] == len(bot.command_groups) and
295-
is_cache_valid(bot)
296-
):
297-
url = bot.memory['command-list'][1]
298-
else:
299-
respond("Hang on, I'm creating a list.")
300-
msgs = []
301-
302-
name_length = max(6, max(len(k) for k in bot.command_groups.keys()))
303-
for category, cmds in collections.OrderedDict(sorted(bot.command_groups.items())).items():
304-
category = category.upper().ljust(name_length)
305-
cmds = set(cmds) # remove duplicates
306-
cmds = ' '.join(cmds)
307-
msg = category + ' ' + cmds
308-
indent = ' ' * (name_length + 2)
309-
# Honestly not sure why this is a list here
310-
msgs.append('\n'.join(textwrap.wrap(msg, subsequent_indent=indent)))
311-
312-
url = create_list(bot, '\n\n'.join(msgs))
313-
if not url:
314-
return
315-
bot.memory['command-list'] = (len(bot.command_groups), url)
316-
update_cache(bot)
317-
respond("I've posted a list of my commands at {0} - You can see "
318-
"more info about any of these commands by doing {1}help "
319-
"<command> (e.g. {1}help time)"
320-
.format(url, bot.config.core.help_prefix))
321-
322-
323-
def create_list(bot, msg):
324-
"""Creates & uploads the command list.
325-
326-
Returns the URL from the chosen pastebin provider.
327-
"""
328-
msg = 'Command listing for {}{}\n\n{}'.format(
329-
bot.nick,
330-
('@' + bot.config.core.host) if bot.config.help.show_server_host else '',
331-
msg)
332-
333-
try:
334-
result = PASTEBIN_PROVIDERS[bot.config.help.output](msg)
335-
except PostingException:
336-
bot.say("Sorry! Something went wrong.")
337-
LOGGER.exception("Error posting commands")
338-
return
339-
return result
340-
341-
342-
@plugin.rule('$nick' r'(?i)help(?:[?!]+)?$')
343-
@plugin.priority('low')
344-
def help2(bot, trigger):
345-
response = (
346-
"Hi, I'm a bot. Say {1}commands to me in private for a list "
347-
"of my commands, or see https://sopel.chat for more "
348-
"general details. My owner is {0}."
349-
.format(bot.config.core.owner, bot.config.core.help_prefix))
350-
bot.reply(response)
15+
LOGGER.warning(
16+
'Sopel's built-in help plugin is obsolete. '
17+
'Install sopel-help as the official help plugin for Sopel.\n'
18+
'You can install sopel-help with "pip install sopel-help".')

0 commit comments

Comments
 (0)