aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/clickup.py8
-rw-r--r--bot/cogs/cogs.py6
-rw-r--r--bot/cogs/tags.py6
-rw-r--r--bot/pagination.py237
-rw-r--r--bot/utils.py128
5 files changed, 248 insertions, 137 deletions
diff --git a/bot/cogs/clickup.py b/bot/cogs/clickup.py
index cde0f6a80..aa6fa4a62 100644
--- a/bot/cogs/clickup.py
+++ b/bot/cogs/clickup.py
@@ -10,7 +10,9 @@ from bot.constants import (
ADMIN_ROLE, CLICKUP_KEY, CLICKUP_SPACE, CLICKUP_TEAM, DEVOPS_ROLE, MODERATOR_ROLE, OWNER_ROLE
)
from bot.decorators import with_role
-from bot.utils import CaseInsensitiveDict, paginate
+from bot.pagination import LinePaginator
+from bot.utils import CaseInsensitiveDict
+
CREATE_TASK_URL = "https://api.clickup.com/api/v1/list/{list_id}/task"
EDIT_TASK_URL = "https://api.clickup.com/api/v1/task/{task_id}"
@@ -110,7 +112,7 @@ class ClickUp:
status = f"{task['status']['status'].title()}"
lines.append(f"{id_fragment} ({status})\n\u00BB {task['name']}")
- return await paginate(lines, ctx, embed, max_size=750)
+ return await LinePaginator.paginate(lines, ctx, embed, max_size=750)
return await ctx.send(embed=embed)
@command(name="clickup.task()", aliases=["clickup.task", "task", "get_task"])
@@ -172,7 +174,7 @@ class ClickUp:
f"**Assignees**\n{assignees}"
)
- return await paginate(lines, ctx, embed, max_size=750)
+ return await LinePaginator.paginate(lines, ctx, embed, max_size=750)
return await ctx.send(embed=embed)
@command(name="clickup.team()", aliases=["clickup.team", "team", "list_team"])
diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py
index 7ad27656e..774f5a68d 100644
--- a/bot/cogs/cogs.py
+++ b/bot/cogs/cogs.py
@@ -10,7 +10,7 @@ from bot.constants import (
WHITE_CHEVRON
)
from bot.decorators import with_role
-from bot.utils import paginate
+from bot.pagination import LinePaginator
class Cogs:
@@ -202,7 +202,7 @@ class Cogs:
for cog, error in failed_loads:
lines.append(f"`{cog}` {WHITE_CHEVRON} `{error}`")
- return await paginate(lines, ctx, embed, empty=False)
+ return await LinePaginator.paginate(lines, ctx, embed, empty=False)
elif full_cog in self.bot.extensions:
try:
@@ -262,7 +262,7 @@ class Cogs:
lines.append(f"{chevron} {cog}")
- await paginate(lines, ctx, embed, max_size=300, empty=False)
+ await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False)
def setup(bot):
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index ab46be009..2bb939ff0 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -8,7 +8,7 @@ from discord.ext.commands import AutoShardedBot, Context, command
from bot.constants import ADMIN_ROLE, MODERATOR_ROLE, OWNER_ROLE
from bot.constants import SITE_API_KEY, SITE_API_TAGS_URL, TAG_COOLDOWN
from bot.decorators import with_role
-from bot.utils import paginate
+from bot.pagination import LinePaginator
class Tags:
@@ -176,12 +176,12 @@ class Tags:
# Paginate if this is a list of all tags
if tags:
- return await paginate(
+ return await LinePaginator.paginate(
(lines for lines in tags),
ctx, embed,
footer_text="To show a tag, type bot.tags.get <tagname>.",
empty=False,
- max_size=200
+ max_lines=15
)
return await ctx.send(embed=embed)
diff --git a/bot/pagination.py b/bot/pagination.py
new file mode 100644
index 000000000..51ddad212
--- /dev/null
+++ b/bot/pagination.py
@@ -0,0 +1,237 @@
+# coding=utf-8
+import asyncio
+from typing import Iterable, Optional
+
+from discord import Embed, Member, Reaction
+from discord.abc import User
+from discord.ext.commands import Context, Paginator
+
+LEFT_EMOJI = "\u2B05"
+RIGHT_EMOJI = "\u27A1"
+DELETE_EMOJI = "\u274c"
+FIRST_EMOJI = "\u23EE"
+LAST_EMOJI = "\u23ED"
+
+PAGINATION_EMOJI = [FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMOJI]
+
+
+class LinePaginator(Paginator):
+ """
+ A class that aids in paginating code blocks for Discord messages.
+
+ Attributes
+ -----------
+ prefix: :class:`str`
+ The prefix inserted to every page. e.g. three backticks.
+ suffix: :class:`str`
+ The suffix appended at the end of every page. e.g. three backticks.
+ max_size: :class:`int`
+ The maximum amount of codepoints allowed in a page.
+ max_lines: :class:`int`
+ The maximum amount of lines allowed in a page.
+ """
+
+ def __init__(self, prefix='```', suffix='```',
+ max_size=2000, max_lines=None):
+ """
+ This function overrides the Paginator.__init__
+ from inside discord.ext.commands.
+ It overrides in order to allow us to configure
+ the maximum number of lines per page.
+ """
+ self.prefix = prefix
+ self.suffix = suffix
+ self.max_size = max_size - len(suffix)
+ self.max_lines = max_lines
+ self._current_page = [prefix]
+ self._linecount = 0
+ self._count = len(prefix) + 1 # prefix + newline
+ self._pages = []
+
+ def add_line(self, line='', *, empty=False):
+ """Adds a line to the current page.
+
+ If the line exceeds the :attr:`max_size` then an exception
+ is raised.
+
+ This function overrides the Paginator.add_line
+ from inside discord.ext.commands.
+ It overrides in order to allow us to configure
+ the maximum number of lines per page.
+
+ Parameters
+ -----------
+ line: str
+ The line to add.
+ empty: bool
+ Indicates if another empty line should be added.
+
+ Raises
+ ------
+ RuntimeError
+ The line was too big for the current :attr:`max_size`.
+ """
+ if len(line) > self.max_size - len(self.prefix) - 2:
+ raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
+
+ if self.max_lines is not None:
+ if self._linecount >= self.max_lines:
+ self._linecount = 0
+ self.close_page()
+
+ self._linecount += 1
+ if self._count + len(line) + 1 > self.max_size:
+ self.close_page()
+
+ self._count += len(line) + 1
+ self._current_page.append(line)
+
+ if empty:
+ self._current_page.append('')
+ self._count += 1
+
+ @classmethod
+ async def paginate(cls, lines: Iterable[str], ctx: Context, embed: Embed,
+ prefix: str = "", suffix: str = "", max_lines: Optional[int] = None, max_size: int = 500,
+ empty: bool = True, restrict_to_user: User = None, timeout: int=300,
+ footer_text: str = None):
+ """
+ Use a paginator and set of reactions to provide pagination over a set of lines. The reactions are used to
+ switch page, or to finish with pagination.
+ When used, this will send a message using `ctx.send()` and apply a set of reactions to it. These reactions may
+ be used to change page, or to remove pagination from the message. Pagination will also be removed automatically
+ if no reaction is added for five minutes (300 seconds).
+ >>> embed = Embed()
+ >>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
+ >>> await LinePaginator.paginate(
+ ... (line for line in lines),
+ ... ctx, embed
+ ... )
+ :param lines: The lines to be paginated
+ :param ctx: Current context object
+ :param embed: A pre-configured embed to be used as a template for each page
+ :param prefix: Text to place before each page
+ :param suffix: Text to place after each page
+ :param max_lines: The maximum number of lines on each page
+ :param max_size: The maximum number of characters on each page
+ :param empty: Whether to place an empty line between each given line
+ :param restrict_to_user: A user to lock pagination operations to for this message, if supplied
+ :param timeout: The amount of time in seconds to disable pagination of no reaction is added
+ :param footer_text: Text to prefix the page number in the footer with
+ """
+
+ def event_check(reaction_: Reaction, user_: Member):
+ """
+ Make sure that this reaction is what we want to operate on
+ """
+
+ no_restrictions = (
+ # Pagination is not restricted
+ not restrict_to_user or
+ # The reaction was by a whitelisted user
+ user_.id == restrict_to_user.id
+ )
+
+ return (
+ # Conditions for a successful pagination:
+ all((
+ # Reaction is on this message
+ reaction_.message.id == message.id,
+ # Reaction is one of the pagination emotes
+ reaction_.emoji in PAGINATION_EMOJI,
+ # Reaction was not made by the Bot
+ user_.id != ctx.bot.user.id,
+ # There were no restrictions
+ no_restrictions
+ ))
+ )
+
+ paginator = cls(prefix=prefix, suffix=suffix, max_size=max_size, max_lines=max_lines)
+ current_page = 0
+
+ for line in lines:
+ paginator.add_line(line, empty=empty)
+
+ embed.description = paginator.pages[current_page]
+
+ if len(paginator.pages) <= 1:
+ if footer_text:
+ embed.set_footer(text=footer_text)
+
+ return await ctx.send(embed=embed)
+ else:
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+
+ message = await ctx.send(embed=embed)
+
+ for emoji in PAGINATION_EMOJI:
+ # Add all the applicable emoji to the message
+ await message.add_reaction(emoji)
+
+ while True:
+ try:
+ reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=event_check)
+ except asyncio.TimeoutError:
+ break # We're done, no reactions for the last 5 minutes
+
+ if reaction.emoji == DELETE_EMOJI:
+ break
+
+ if reaction.emoji == FIRST_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+ current_page = 0
+ embed.description = paginator.pages[current_page]
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ await message.edit(embed=embed)
+
+ if reaction.emoji == LAST_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+ current_page = len(paginator.pages) - 1
+ embed.description = paginator.pages[current_page]
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ await message.edit(embed=embed)
+
+ if reaction.emoji == LEFT_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+
+ if current_page <= 0:
+ continue
+
+ current_page -= 1
+
+ embed.description = paginator.pages[current_page]
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+
+ await message.edit(embed=embed)
+
+ if reaction.emoji == RIGHT_EMOJI:
+ await message.remove_reaction(reaction.emoji, user)
+
+ if current_page >= len(paginator.pages) - 1:
+ continue
+
+ current_page += 1
+
+ embed.description = paginator.pages[current_page]
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
+ else:
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+
+ await message.edit(embed=embed)
+
+ await message.clear_reactions()
diff --git a/bot/utils.py b/bot/utils.py
index c3ee680ed..eac37a4b4 100644
--- a/bot/utils.py
+++ b/bot/utils.py
@@ -1,16 +1,4 @@
# coding=utf-8
-import asyncio
-from typing import Iterable
-
-from discord import Embed, Member, Reaction
-from discord.abc import User
-from discord.ext.commands import Context, Paginator
-
-LEFT_EMOJI = "\u2B05"
-RIGHT_EMOJI = "\u27A1"
-DELETE_EMOJI = "\U0001F5D1"
-
-PAGINATION_EMOJI = [LEFT_EMOJI, RIGHT_EMOJI, DELETE_EMOJI]
class CaseInsensitiveDict(dict):
@@ -57,119 +45,3 @@ class CaseInsensitiveDict(dict):
for k in list(self.keys()):
v = super(CaseInsensitiveDict, self).pop(k)
self.__setitem__(k, v)
-
-
-async def paginate(lines: Iterable[str], ctx: Context, embed: Embed,
- prefix: str = "", suffix: str = "", max_size: int = 500, empty: bool = True,
- restrict_to_user: User = None, timeout: int=300,
- footer_text: str = None):
- """
- Use a paginator and set of reactions to provide pagination over a set of lines. The reactions are used to
- switch page, or to finish with pagination.
-
- When used, this will send a message using `ctx.send()` and apply a set of reactions to it. These reactions may
- be used to change page, or to remove pagination from the message. Pagination will also be removed automatically
- if no reaction is added for five minutes (300 seconds).
-
- >>> embed = Embed()
- >>> embed.set_author(name="Some Operation", url=url, icon_url=icon)
- >>> await paginate(
- ... (line for line in lines),
- ... ctx, embed
- ... )
-
- :param lines: The lines to be paginated
- :param ctx: Current context object
- :param embed: A pre-configured embed to be used as a template for each page
- :param prefix: Text to place before each page
- :param suffix: Text to place after each page
- :param max_size: The maximum number of characters on each page
- :param empty: Whether to place an empty line between each given line
- :param restrict_to_user: A user to lock pagination operations to for this message, if supplied
- :param timeout: The amount of time in seconds to disable pagination of no reaction is added
- :param footer_text: Text to prefix the page number in the footer with
- """
-
- def event_check(reaction_: Reaction, user_: Member):
- """
- Make sure that this reaction is what we want to operate on
- """
-
- return (
- reaction_.message.id == message.id and # Reaction on this specific message
- reaction_.emoji in PAGINATION_EMOJI and # One of the reactions we handle
- user_.id != ctx.bot.user.id and ( # Not applied by the bot itself
- not restrict_to_user or # Unrestricted if there's no user to restrict to, or...
- user_.id == restrict_to_user.id # Only by the restricted user
- )
- )
-
- paginator = Paginator(prefix=prefix, suffix=suffix, max_size=max_size)
- current_page = 0
-
- for line in lines:
- paginator.add_line(line, empty=empty)
-
- embed.description = paginator.pages[current_page]
-
- if len(paginator.pages) <= 1:
- if footer_text:
- embed.set_footer(text=footer_text)
-
- return await ctx.send(embed=embed)
- else:
- if footer_text:
- embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
- else:
- embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
-
- message = await ctx.send(embed=embed)
-
- for emoji in PAGINATION_EMOJI:
- # Add all the applicable emoji to the message
- await message.add_reaction(emoji)
-
- while True:
- try:
- reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=event_check)
- except asyncio.TimeoutError:
- break # We're done, no reactions for the last 5 minutes
-
- if reaction.emoji == DELETE_EMOJI:
- break
-
- if reaction.emoji == LEFT_EMOJI:
- await message.remove_reaction(reaction.emoji, user)
-
- if current_page <= 0:
- continue
-
- current_page -= 1
-
- embed.description = paginator.pages[current_page]
-
- if footer_text:
- embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
- else:
- embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
-
- await message.edit(embed=embed)
-
- if reaction.emoji == RIGHT_EMOJI:
- await message.remove_reaction(reaction.emoji, user)
-
- if current_page >= len(paginator.pages) - 1:
- continue
-
- current_page += 1
-
- embed.description = paginator.pages[current_page]
-
- if footer_text:
- embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
- else:
- embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
-
- await message.edit(embed=embed)
-
- await message.clear_reactions()