|
|
@@ -2,8 +2,9 @@
|
|
2
|
2
|
import re
|
|
3
|
3
|
from typing import Union, Optional
|
|
4
|
4
|
|
|
5
|
|
-from discord import Interaction, Permissions, AppCommandType
|
|
|
5
|
+from discord import Interaction, Permissions, AppCommandType, ButtonStyle, InteractionResponse
|
|
6
|
6
|
from discord.app_commands import Group, Command, autocomplete, guild_only, command, Choice
|
|
|
7
|
+from discord.ui import ActionRow, Button, LayoutView, TextDisplay
|
|
7
|
8
|
|
|
8
|
9
|
from config import CONFIG
|
|
9
|
10
|
from rocketbot.bot import Rocketbot
|
|
|
@@ -12,6 +13,9 @@ from rocketbot.utils import MOD_PERMISSIONS, dump_stacktrace
|
|
12
|
13
|
|
|
13
|
14
|
HelpTopic = Union[Command, Group, BaseCog]
|
|
14
|
15
|
|
|
|
16
|
+# Potential place to break text neatly in large help content
|
|
|
17
|
+PAGE_BREAK = '\f'
|
|
|
18
|
+
|
|
15
|
19
|
def choice_from_topic(topic: HelpTopic, include_full_command: bool = False) -> Choice:
|
|
16
|
20
|
if isinstance(topic, BaseCog):
|
|
17
|
21
|
return Choice(name=f'⚙ {topic.qualified_name}', value=f'cog:{topic.qualified_name}')
|
|
|
@@ -272,6 +276,7 @@ class HelpCog(BaseCog, name='Help'):
|
|
272
|
276
|
if isinstance(cmd, Group):
|
|
273
|
277
|
subcommand_count = len(cmd.commands)
|
|
274
|
278
|
text += f' ({subcommand_count} subcommands)'
|
|
|
279
|
+ text += PAGE_BREAK
|
|
275
|
280
|
|
|
276
|
281
|
if len(all_cog_tuples) > 0:
|
|
277
|
282
|
text += '\n### Module Configuration'
|
|
|
@@ -284,10 +289,9 @@ class HelpCog(BaseCog, name='Help'):
|
|
284
|
289
|
if setting.name == 'enabled':
|
|
285
|
290
|
continue
|
|
286
|
291
|
text += f'\n - `/get` or `/set {cog.config_prefix}_{setting.name}`'
|
|
287
|
|
- await interaction.response.send_message(
|
|
288
|
|
- text,
|
|
289
|
|
- ephemeral=True,
|
|
290
|
|
- )
|
|
|
292
|
+ text += PAGE_BREAK
|
|
|
293
|
+
|
|
|
294
|
+ await self.__send_paged_help(interaction, text)
|
|
291
|
295
|
|
|
292
|
296
|
async def __send_keyword_help(self, interaction: Interaction, matching_topics: Optional[list[HelpTopic]]) -> None:
|
|
293
|
297
|
matching_commands = [
|
|
|
@@ -324,10 +328,8 @@ class HelpCog(BaseCog, name='Help'):
|
|
324
|
328
|
text += '\n### Modules'
|
|
325
|
329
|
for cog in matching_cogs:
|
|
326
|
330
|
text += f'\n- {cog.qualified_name}'
|
|
327
|
|
- await interaction.response.send_message(
|
|
328
|
|
- text,
|
|
329
|
|
- ephemeral=True,
|
|
330
|
|
- )
|
|
|
331
|
+
|
|
|
332
|
+ await self.__send_paged_help(interaction, text)
|
|
331
|
333
|
|
|
332
|
334
|
async def __send_command_help(self, interaction: Interaction, command_or_group: Union[Command, Group], addendum: Optional[str] = None) -> None:
|
|
333
|
335
|
text = ''
|
|
|
@@ -371,7 +373,8 @@ class HelpCog(BaseCog, name='Help'):
|
|
371
|
373
|
text += f'\n- `{param.name}`: {param.description}'
|
|
372
|
374
|
if not param.required:
|
|
373
|
375
|
text += ' (optional)'
|
|
374
|
|
- await interaction.response.send_message(text, ephemeral=True)
|
|
|
376
|
+
|
|
|
377
|
+ await self.__send_paged_help(interaction, text)
|
|
375
|
378
|
|
|
376
|
379
|
async def __send_cog_help(self, interaction: Interaction, cog: BaseCog) -> None:
|
|
377
|
380
|
text = f'## :information_source: Module Help'
|
|
|
@@ -405,10 +408,92 @@ class HelpCog(BaseCog, name='Help'):
|
|
405
|
408
|
if setting.name == 'enabled':
|
|
406
|
409
|
continue
|
|
407
|
410
|
text += f'\n- `/get` or `/set {cog.config_prefix}_{setting.name}` - {setting.brief}'
|
|
408
|
|
- await interaction.response.send_message(
|
|
409
|
|
- text,
|
|
410
|
|
- ephemeral=True,
|
|
411
|
|
- )
|
|
|
411
|
+
|
|
|
412
|
+ await self.__send_paged_help(interaction, text)
|
|
|
413
|
+
|
|
|
414
|
+ async def __send_paged_help(self, interaction: Interaction, text: str) -> None:
|
|
|
415
|
+ pages = _paginate(text)
|
|
|
416
|
+ if len(pages) == 1:
|
|
|
417
|
+ await interaction.response.send_message(pages[0], ephemeral=True)
|
|
|
418
|
+ else:
|
|
|
419
|
+ await _update_paged_help(interaction, None, 0, pages)
|
|
|
420
|
+
|
|
|
421
|
+def _paginate(text: str) -> list[str]:
|
|
|
422
|
+ max_page_size = 2000
|
|
|
423
|
+ chunks = text.split(PAGE_BREAK)
|
|
|
424
|
+ pages = [ '' ]
|
|
|
425
|
+ for chunk in chunks:
|
|
|
426
|
+ if len(chunk) > max_page_size:
|
|
|
427
|
+ raise ValueError('Help content needs more page breaks! One chunk is too big for message.')
|
|
|
428
|
+ if len(pages[-1] + chunk) < max_page_size:
|
|
|
429
|
+ pages[-1] += chunk
|
|
|
430
|
+ else:
|
|
|
431
|
+ pages.append(chunk)
|
|
|
432
|
+ page_count = len(pages)
|
|
|
433
|
+ if page_count == 1:
|
|
|
434
|
+ return pages
|
|
|
435
|
+
|
|
|
436
|
+ # Do another pass and try to even out the page lengths
|
|
|
437
|
+ indices = [ i * len(chunks) // page_count for i in range(page_count + 1) ]
|
|
|
438
|
+ even_pages = [
|
|
|
439
|
+ ''.join(chunks[indices[i]:indices[i + 1]])
|
|
|
440
|
+ for i in range(page_count)
|
|
|
441
|
+ ]
|
|
|
442
|
+ for page in even_pages:
|
|
|
443
|
+ if len(page) > max_page_size:
|
|
|
444
|
+ # We made a page too big. Give up.
|
|
|
445
|
+ return pages
|
|
|
446
|
+ return even_pages
|
|
|
447
|
+
|
|
|
448
|
+async def _update_paged_help(interaction: Interaction, original_interaction: Optional[Interaction], current_page: int, pages: list[str]) -> None:
|
|
|
449
|
+ try:
|
|
|
450
|
+ view = _PagingLayoutView(current_page, pages, original_interaction or interaction)
|
|
|
451
|
+ resolved = interaction
|
|
|
452
|
+ if original_interaction is not None:
|
|
|
453
|
+ # We have an original interaction from the initial command and a
|
|
|
454
|
+ # new one from the button press. Use the original to swap in the
|
|
|
455
|
+ # new page in place, then acknowledge the new one to satisfy the
|
|
|
456
|
+ # API that we didn't fail.
|
|
|
457
|
+ await original_interaction.edit_original_response(
|
|
|
458
|
+ view=view,
|
|
|
459
|
+ )
|
|
|
460
|
+ if interaction is not original_interaction:
|
|
|
461
|
+ await interaction.response.defer(ephemeral=True, thinking=False)
|
|
|
462
|
+ else:
|
|
|
463
|
+ # Initial send
|
|
|
464
|
+ await resolved.response.send_message(
|
|
|
465
|
+ view=view,
|
|
|
466
|
+ ephemeral=True,
|
|
|
467
|
+ delete_message_after=60,
|
|
|
468
|
+ )
|
|
|
469
|
+ except BaseException as e:
|
|
|
470
|
+ dump_stacktrace(e)
|
|
|
471
|
+
|
|
|
472
|
+class _PagingLayoutView(LayoutView):
|
|
|
473
|
+ def __init__(self, current_page: int, pages: list[str], original_interaction: Optional[Interaction]):
|
|
|
474
|
+ super().__init__()
|
|
|
475
|
+ self.current_page: int = current_page
|
|
|
476
|
+ self.pages: list[str] = pages
|
|
|
477
|
+ self.text.content = self.pages[self.current_page]
|
|
|
478
|
+ self.original_interaction = original_interaction
|
|
|
479
|
+ if current_page <= 0:
|
|
|
480
|
+ self.handle_prev_button.disabled = True
|
|
|
481
|
+ if current_page >= len(self.pages) - 1:
|
|
|
482
|
+ self.handle_next_button.disabled = True
|
|
|
483
|
+
|
|
|
484
|
+ text = TextDisplay('')
|
|
|
485
|
+
|
|
|
486
|
+ row = ActionRow()
|
|
|
487
|
+
|
|
|
488
|
+ @row.button(label='< Prev')
|
|
|
489
|
+ async def handle_prev_button(self, interaction: Interaction, button: Button) -> None:
|
|
|
490
|
+ new_page = max(0, self.current_page - 1)
|
|
|
491
|
+ await _update_paged_help(interaction, self.original_interaction, new_page, self.pages)
|
|
|
492
|
+
|
|
|
493
|
+ @row.button(label='Next >')
|
|
|
494
|
+ async def handle_next_button(self, interaction: Interaction, button: Button) -> None:
|
|
|
495
|
+ new_page = min(len(self.pages) - 1, self.current_page + 1)
|
|
|
496
|
+ await _update_paged_help(interaction, self.original_interaction, new_page, self.pages)
|
|
412
|
497
|
|
|
413
|
498
|
# Exclusions from keyword indexing
|
|
414
|
499
|
trivial_words = {
|