Experimental Discord bot written in Python
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

pagedcontent.py 4.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """
  2. Provides a means of presenting long messages by paging them. Paging requires the
  3. source message to insert `PAGE_BREAK` characters at meaningful breaks, preferably
  4. at fairly uniform intervals.
  5. """
  6. from typing import Optional
  7. from discord import Interaction
  8. from discord.ui import LayoutView, TextDisplay, ActionRow, Button
  9. from rocketbot.utils import dump_stacktrace
  10. PAGE_BREAK = '\f'
  11. def paginate(text: str) -> list[str]:
  12. """
  13. Breaks long message text into one or more pages, using page break markers as
  14. potential clean break points.
  15. """
  16. max_page_size = 2000
  17. chunks = text.split(PAGE_BREAK)
  18. pages = [ '' ]
  19. for chunk in chunks:
  20. if len(chunk) > max_page_size:
  21. raise ValueError('Help content needs more page breaks! One chunk is too big for message.')
  22. if len(pages[-1] + chunk) < max_page_size:
  23. pages[-1] += chunk
  24. else:
  25. pages.append(chunk)
  26. page_count = len(pages)
  27. if page_count == 1:
  28. return pages
  29. # Do another pass and try to even out the page lengths
  30. indices = [ i * len(chunks) // page_count for i in range(page_count + 1) ]
  31. even_pages = [
  32. ''.join(chunks[indices[i]:indices[i + 1]])
  33. for i in range(page_count)
  34. ]
  35. for page in even_pages:
  36. if len(page) > max_page_size:
  37. # We made a page too big. Give up.
  38. return pages
  39. return even_pages
  40. async def update_paged_content(
  41. interaction: Interaction,
  42. original_interaction: Optional[Interaction],
  43. current_page: int,
  44. pages: list[str],
  45. **send_args,
  46. ) -> None:
  47. """
  48. Posts and/or updates the content of a message from paged content.
  49. Parameters
  50. ----------
  51. interaction : Interaction
  52. the current interaction, either the initial one or from a button press
  53. original_interaction : Interaction
  54. the first interaction that triggered presentation of the content, or None
  55. if this already is the first interaction
  56. current_page : int
  57. page index to show (assumed to be in bounds)
  58. pages : list[str]
  59. array of page content
  60. **send_args
  61. additional arguments to pass to the send_message method
  62. """
  63. if len(pages) == 1:
  64. # No paging needed
  65. await interaction.response.send_message(
  66. pages[0],
  67. ephemeral=True,
  68. )
  69. return
  70. try:
  71. view = _PagingLayoutView(current_page, pages, original_interaction or interaction)
  72. resolved = interaction
  73. if original_interaction is not None:
  74. # We have an original interaction from the initial command and a
  75. # new one from the button press. Use the original to swap in the
  76. # new page in place, then acknowledge the new one to satisfy the
  77. # API that we didn't fail.
  78. await original_interaction.edit_original_response(
  79. view=view,
  80. )
  81. if interaction is not original_interaction:
  82. await interaction.response.defer(ephemeral=True, thinking=False)
  83. else:
  84. # Initial send
  85. await resolved.response.send_message(
  86. view=view,
  87. ephemeral=True,
  88. **send_args,
  89. )
  90. except BaseException as e:
  91. dump_stacktrace(e)
  92. class _PagingLayoutView(LayoutView):
  93. def __init__(
  94. self,
  95. current_page: int,
  96. pages: list[str],
  97. original_interaction: Optional[Interaction],
  98. **send_args,
  99. ) -> None:
  100. super().__init__()
  101. self.current_page: int = current_page
  102. self.pages: list[str] = pages
  103. self.text.content = self.pages[self.current_page] + f'\n\n_Page {self.current_page + 1} of {len(self.pages)}_'
  104. self.original_interaction = original_interaction
  105. if current_page <= 0:
  106. self.handle_prev_button.disabled = True
  107. if current_page >= len(self.pages) - 1:
  108. self.handle_next_button.disabled = True
  109. self.send_args = send_args
  110. text = TextDisplay('')
  111. row = ActionRow()
  112. @row.button(label='< Prev')
  113. async def handle_prev_button(self, interaction: Interaction, button: Button) -> None:
  114. new_page = max(0, self.current_page - 1)
  115. await update_paged_content(interaction, self.original_interaction, new_page, self.pages, **self.send_args)
  116. @row.button(label='Next >')
  117. async def handle_next_button(self, interaction: Interaction, button: Button) -> None:
  118. new_page = min(len(self.pages) - 1, self.current_page + 1)
  119. await update_paged_content(interaction, self.original_interaction, new_page, self.pages, **self.send_args)