mirror of
https://github.com/uhIgnacio/EmoteManager.git
synced 2024-08-15 02:23:13 +00:00
format
This commit is contained in:
parent
1ff3e93938
commit
3f73309974
10 changed files with 584 additions and 523 deletions
|
@ -14,6 +14,7 @@ from . import errors
|
||||||
|
|
||||||
ArchiveInfo = collections.namedtuple('ArchiveInfo', 'filename content error')
|
ArchiveInfo = collections.namedtuple('ArchiveInfo', 'filename content error')
|
||||||
|
|
||||||
|
|
||||||
def extract(archive: typing.io.BinaryIO, *, size_limit=None) \
|
def extract(archive: typing.io.BinaryIO, *, size_limit=None) \
|
||||||
-> Iterable[Tuple[str, Optional[bytes], Optional[BaseException]]]:
|
-> Iterable[Tuple[str, Optional[bytes], Optional[BaseException]]]:
|
||||||
"""
|
"""
|
||||||
|
@ -39,6 +40,7 @@ def extract(archive: typing.io.BinaryIO, *, size_limit=None) \
|
||||||
finally:
|
finally:
|
||||||
archive.seek(0)
|
archive.seek(0)
|
||||||
|
|
||||||
|
|
||||||
def extract_zip(archive, *, size_limit=None):
|
def extract_zip(archive, *, size_limit=None):
|
||||||
with zipfile.ZipFile(archive) as zip:
|
with zipfile.ZipFile(archive) as zip:
|
||||||
members = [m for m in zip.infolist() if not m.is_dir()]
|
members = [m for m in zip.infolist() if not m.is_dir()]
|
||||||
|
@ -57,6 +59,7 @@ def extract_zip(archive, *, size_limit=None):
|
||||||
else: # this else is required to avoid UnboundLocalError for some reason
|
else: # this else is required to avoid UnboundLocalError for some reason
|
||||||
yield ArchiveInfo(filename=member.filename, content=content, error=None)
|
yield ArchiveInfo(filename=member.filename, content=content, error=None)
|
||||||
|
|
||||||
|
|
||||||
def extract_tar(archive, *, size_limit=None):
|
def extract_tar(archive, *, size_limit=None):
|
||||||
with tarfile.open(fileobj=archive) as tar:
|
with tarfile.open(fileobj=archive) as tar:
|
||||||
members = [f for f in tar.getmembers() if f.isfile()]
|
members = [f for f in tar.getmembers() if f.isfile()]
|
||||||
|
@ -70,10 +73,12 @@ def extract_tar(archive, *, size_limit=None):
|
||||||
|
|
||||||
yield ArchiveInfo(member.name, content=tar.extractfile(member).read(), error=None)
|
yield ArchiveInfo(member.name, content=tar.extractfile(member).read(), error=None)
|
||||||
|
|
||||||
|
|
||||||
async def extract_async(archive: typing.io.BinaryIO, size_limit=None):
|
async def extract_async(archive: typing.io.BinaryIO, size_limit=None):
|
||||||
for x in extract(archive, size_limit=size_limit):
|
for x in extract(archive, size_limit=size_limit):
|
||||||
yield await asyncio.sleep(0, x)
|
yield await asyncio.sleep(0, x)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
|
@ -88,5 +93,6 @@ def main():
|
||||||
|
|
||||||
print(f'{name}: {humanize.naturalsize(len(data)):>10}')
|
print(f'{name}: {humanize.naturalsize(len(data)):>10}')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from discord.ext.commands import BadArgument
|
from nextcord.ext.commands import BadArgument
|
||||||
|
|
||||||
_emote_type_predicates = {
|
_emote_type_predicates = {
|
||||||
'all': lambda _: True,
|
'all': lambda _: True,
|
||||||
|
@ -10,6 +10,8 @@ _emote_type_predicates = {
|
||||||
'animated': lambda e: e.animated}
|
'animated': lambda e: e.animated}
|
||||||
|
|
||||||
# this is kind of a hack to ensure that the last argument is always converted, even if the default is used.
|
# this is kind of a hack to ensure that the last argument is always converted, even if the default is used.
|
||||||
|
|
||||||
|
|
||||||
def emote_type_filter_default(command):
|
def emote_type_filter_default(command):
|
||||||
old_callback = command.callback
|
old_callback = command.callback
|
||||||
|
|
||||||
|
@ -19,7 +21,8 @@ def emote_type_filter_default(command):
|
||||||
try:
|
try:
|
||||||
image_type = _emote_type_predicates[image_type]
|
image_type = _emote_type_predicates[image_type]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise BadArgument(f'Invalid emote type. Specify one of "all", "static", or "animated".')
|
raise BadArgument(
|
||||||
|
f'Invalid emote type. Specify one of "all", "static", or "animated".')
|
||||||
return await old_callback(self, ctx, *args[:-1], image_type)
|
return await old_callback(self, ctx, *args[:-1], image_type)
|
||||||
|
|
||||||
command.callback = callback
|
command.callback = callback
|
||||||
|
|
|
@ -11,7 +11,9 @@ various utilities related to custom emotes
|
||||||
RE_EMOTE = re.compile(r'(:|;)(?P<name>\w{2,32})\1|(?P<newline>\n)', re.ASCII)
|
RE_EMOTE = re.compile(r'(:|;)(?P<name>\w{2,32})\1|(?P<newline>\n)', re.ASCII)
|
||||||
|
|
||||||
"""Matches only custom server emotes."""
|
"""Matches only custom server emotes."""
|
||||||
RE_CUSTOM_EMOTE = re.compile(r'<(?P<animated>a?):(?P<name>\w{2,32}):(?P<id>\d{17,})>', re.ASCII)
|
RE_CUSTOM_EMOTE = re.compile(
|
||||||
|
r'<(?P<animated>a?):(?P<name>\w{2,32}):(?P<id>\d{17,})>', re.ASCII)
|
||||||
|
|
||||||
|
|
||||||
def url(id, *, animated: bool = False):
|
def url(id, *, animated: bool = False):
|
||||||
"""Convert an emote ID to the image URL for that emote."""
|
"""Convert an emote ID to the image URL for that emote."""
|
||||||
|
|
|
@ -17,6 +17,7 @@ from nextcord import HTTPException, Forbidden, NotFound, DiscordServerError
|
||||||
|
|
||||||
GuildId = int
|
GuildId = int
|
||||||
|
|
||||||
|
|
||||||
async def json_or_text(resp):
|
async def json_or_text(resp):
|
||||||
text = await resp.text(encoding='utf-8')
|
text = await resp.text(encoding='utf-8')
|
||||||
try:
|
try:
|
||||||
|
@ -28,6 +29,7 @@ async def json_or_text(resp):
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
class EmoteClient:
|
class EmoteClient:
|
||||||
BASE_URL = 'https://discord.com/api/v9'
|
BASE_URL = 'https://discord.com/api/v9'
|
||||||
HTTP_ERROR_CLASSES = {
|
HTTP_ERROR_CLASSES = {
|
||||||
|
@ -51,7 +53,8 @@ class EmoteClient:
|
||||||
# Emote Manager shouldn't use walrus op until Debian adopts 3.8 :(
|
# Emote Manager shouldn't use walrus op until Debian adopts 3.8 :(
|
||||||
reason = kwargs.pop('reason', None)
|
reason = kwargs.pop('reason', None)
|
||||||
if reason:
|
if reason:
|
||||||
headers['X-Audit-Log-Reason'] = urllib.parse.quote(reason, safe='/ ')
|
headers['X-Audit-Log-Reason'] = urllib.parse.quote(
|
||||||
|
reason, safe='/ ')
|
||||||
kwargs['headers'] = headers
|
kwargs['headers'] = headers
|
||||||
|
|
||||||
# TODO handle OSError and 500/502, like dpy does
|
# TODO handle OSError and 500/502, like dpy does
|
||||||
|
@ -83,7 +86,8 @@ class EmoteClient:
|
||||||
|
|
||||||
async def _handle_rl(self, resp, method, path, guild_id, **kwargs):
|
async def _handle_rl(self, resp, method, path, guild_id, **kwargs):
|
||||||
retry_after = (await resp.json())['retry_after'] / 1000.0
|
retry_after = (await resp.json())['retry_after'] / 1000.0
|
||||||
retry_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(seconds=retry_after)
|
retry_at = datetime.datetime.now(
|
||||||
|
tz=datetime.timezone.utc) + datetime.timedelta(seconds=retry_after)
|
||||||
|
|
||||||
# cache unconditionally in case request() is called again while we're sleeping
|
# cache unconditionally in case request() is called again while we're sleeping
|
||||||
self.guild_rls[guild_id] = retry_at.timestamp()
|
self.guild_rls[guild_id] = retry_at.timestamp()
|
||||||
|
@ -100,7 +104,8 @@ class EmoteClient:
|
||||||
data = await self.request(
|
data = await self.request(
|
||||||
'POST', f'/guilds/{guild.id}/emojis',
|
'POST', f'/guilds/{guild.id}/emojis',
|
||||||
guild.id,
|
guild.id,
|
||||||
json=dict(name=name, image=image_utils.image_to_base64_url(image), roles=role_ids),
|
json=dict(name=name, image=image_utils.image_to_base64_url(
|
||||||
|
image), roles=role_ids),
|
||||||
reason=reason,
|
reason=reason,
|
||||||
)
|
)
|
||||||
return PartialEmoji(animated=data.get('animated', False), name=data.get('name'), id=data.get('id'))
|
return PartialEmoji(animated=data.get('animated', False), name=data.get('name'), id=data.get('id'))
|
||||||
|
|
|
@ -5,7 +5,8 @@ import utils
|
||||||
import asyncio
|
import asyncio
|
||||||
import humanize
|
import humanize
|
||||||
import datetime
|
import datetime
|
||||||
from discord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
class MissingManageEmojisPermission(commands.MissingPermissions):
|
class MissingManageEmojisPermission(commands.MissingPermissions):
|
||||||
"""The invoker or the bot doesn't have permissions to manage server emojis."""
|
"""The invoker or the bot doesn't have permissions to manage server emojis."""
|
||||||
|
@ -16,62 +17,86 @@ class MissingManageEmojisPermission(commands.MissingPermissions):
|
||||||
"Sorry, you don't have enough permissions to run this command. "
|
"Sorry, you don't have enough permissions to run this command. "
|
||||||
'You and I both need the Manage Emojis permission.')
|
'You and I both need the Manage Emojis permission.')
|
||||||
|
|
||||||
|
|
||||||
class EmoteManagerError(commands.CommandError):
|
class EmoteManagerError(commands.CommandError):
|
||||||
"""Generic error with the bot. This can be used to catch all bot errors."""
|
"""Generic error with the bot. This can be used to catch all bot errors."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ImageProcessingTimeoutError(EmoteManagerError, asyncio.TimeoutError):
|
class ImageProcessingTimeoutError(EmoteManagerError, asyncio.TimeoutError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ImageResizeTimeoutError(ImageProcessingTimeoutError):
|
class ImageResizeTimeoutError(ImageProcessingTimeoutError):
|
||||||
"""Resizing the image took too long."""
|
"""Resizing the image took too long."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('Error: resizing the image took too long.')
|
super().__init__('Error: resizing the image took too long.')
|
||||||
|
|
||||||
|
|
||||||
class ImageConversionTimeoutError(ImageProcessingTimeoutError):
|
class ImageConversionTimeoutError(ImageProcessingTimeoutError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('Error: converting the image to a GIF took too long.')
|
super().__init__('Error: converting the image to a GIF took too long.')
|
||||||
|
|
||||||
|
|
||||||
class HTTPException(EmoteManagerError):
|
class HTTPException(EmoteManagerError):
|
||||||
"""The server did not respond with an OK status code. This is only for non-Discord HTTP requests."""
|
"""The server did not respond with an OK status code. This is only for non-Discord HTTP requests."""
|
||||||
|
|
||||||
def __init__(self, status):
|
def __init__(self, status):
|
||||||
super().__init__(f'URL error: server returned error code {status}')
|
super().__init__(f'URL error: server returned error code {status}')
|
||||||
|
|
||||||
|
|
||||||
class RateLimitedError(EmoteManagerError):
|
class RateLimitedError(EmoteManagerError):
|
||||||
def __init__(self, retry_at):
|
def __init__(self, retry_at):
|
||||||
if isinstance(retry_at, float):
|
if isinstance(retry_at, float):
|
||||||
# it took me about an HOUR to realize i had to pass tz because utcfromtimestamp returns a NAÏVE time obj!
|
# it took me about an HOUR to realize i had to pass tz because utcfromtimestamp returns a NAÏVE time obj!
|
||||||
retry_at = datetime.datetime.fromtimestamp(retry_at, tz=datetime.timezone.utc)
|
retry_at = datetime.datetime.fromtimestamp(
|
||||||
|
retry_at, tz=datetime.timezone.utc)
|
||||||
# humanize.naturaltime is annoying to work with due to timezones so we use this
|
# humanize.naturaltime is annoying to work with due to timezones so we use this
|
||||||
delta = humanize.naturaldelta(retry_at, when=datetime.datetime.now(tz=datetime.timezone.utc))
|
delta = humanize.naturaldelta(
|
||||||
super().__init__(f'Error: Discord told me to slow down! Please retry this command in {delta}.')
|
retry_at, when=datetime.datetime.now(tz=datetime.timezone.utc))
|
||||||
|
super().__init__(
|
||||||
|
f'Error: Discord told me to slow down! Please retry this command in {delta}.')
|
||||||
|
|
||||||
|
|
||||||
class EmoteNotFoundError(EmoteManagerError):
|
class EmoteNotFoundError(EmoteManagerError):
|
||||||
"""An emote with that name was not found"""
|
"""An emote with that name was not found"""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__(f'An emote called `{name}` does not exist in this server.')
|
super().__init__(
|
||||||
|
f'An emote called `{name}` does not exist in this server.')
|
||||||
|
|
||||||
|
|
||||||
class FileTooBigError(EmoteManagerError):
|
class FileTooBigError(EmoteManagerError):
|
||||||
def __init__(self, size, limit):
|
def __init__(self, size, limit):
|
||||||
self.size = size
|
self.size = size
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
|
|
||||||
|
|
||||||
class InvalidFileError(EmoteManagerError):
|
class InvalidFileError(EmoteManagerError):
|
||||||
"""The file is not a zip, tar, GIF, PNG, JPG, or WEBP file."""
|
"""The file is not a zip, tar, GIF, PNG, JPG, or WEBP file."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('Invalid file given.')
|
super().__init__('Invalid file given.')
|
||||||
|
|
||||||
|
|
||||||
class InvalidImageError(InvalidFileError):
|
class InvalidImageError(InvalidFileError):
|
||||||
"""The image is not a GIF, PNG, or JPG"""
|
"""The image is not a GIF, PNG, or JPG"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Exception, self).__init__('The image supplied was not a GIF, PNG, JPG, or WEBP file.')
|
super(Exception, self).__init__(
|
||||||
|
'The image supplied was not a GIF, PNG, JPG, or WEBP file.')
|
||||||
|
|
||||||
|
|
||||||
class PermissionDeniedError(EmoteManagerError):
|
class PermissionDeniedError(EmoteManagerError):
|
||||||
"""Raised when a user tries to modify an emote without the Manage Emojis permission"""
|
"""Raised when a user tries to modify an emote without the Manage Emojis permission"""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__(f"You're not authorized to modify `{name}`.")
|
super().__init__(f"You're not authorized to modify `{name}`.")
|
||||||
|
|
||||||
|
|
||||||
class DiscordError(Exception):
|
class DiscordError(Exception):
|
||||||
"""Usually raised when the client cache is being baka"""
|
"""Usually raised when the client cache is being baka"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('Discord seems to be having issues right now, please try again later.')
|
super().__init__('Discord seems to be having issues right now, please try again later.')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# © lambda#0987 <lambda@lambda.dance>
|
# © lambda#0987 <lambda@lambda.dance>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
from utils import errors
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
|
@ -16,11 +17,11 @@ logger = logging.getLogger(__name__)
|
||||||
try:
|
try:
|
||||||
import wand.image
|
import wand.image
|
||||||
except (ImportError, OSError):
|
except (ImportError, OSError):
|
||||||
logger.warn('Failed to import wand.image. Image manipulation functions will be unavailable.')
|
logger.warn(
|
||||||
|
'Failed to import wand.image. Image manipulation functions will be unavailable.')
|
||||||
else:
|
else:
|
||||||
import wand.exceptions
|
import wand.exceptions
|
||||||
|
|
||||||
from utils import errors
|
|
||||||
|
|
||||||
def resize_until_small(image_data: io.BytesIO) -> None:
|
def resize_until_small(image_data: io.BytesIO) -> None:
|
||||||
"""If the image_data is bigger than 256KB, resize it until it's not."""
|
"""If the image_data is bigger than 256KB, resize it until it's not."""
|
||||||
|
@ -37,10 +38,12 @@ def resize_until_small(image_data: io.BytesIO) -> None:
|
||||||
with wand.image.Image(blob=image_data) as original_image:
|
with wand.image.Image(blob=image_data) as original_image:
|
||||||
while True:
|
while True:
|
||||||
logger.debug('image size too big (%s bytes)', image_size)
|
logger.debug('image size too big (%s bytes)', image_size)
|
||||||
logger.debug('attempting resize to at most%s*%s pixels', max_resolution, max_resolution)
|
logger.debug('attempting resize to at most%s*%s pixels',
|
||||||
|
max_resolution, max_resolution)
|
||||||
|
|
||||||
with original_image.clone() as resized:
|
with original_image.clone() as resized:
|
||||||
resized.transform(resize=f'{max_resolution}x{max_resolution}')
|
resized.transform(
|
||||||
|
resize=f'{max_resolution}x{max_resolution}')
|
||||||
image_size = len(resized.make_blob())
|
image_size = len(resized.make_blob())
|
||||||
if image_size <= 256 * 2**10 or max_resolution < 32: # don't resize past 256KiB or 32×32
|
if image_size <= 256 * 2**10 or max_resolution < 32: # don't resize past 256KiB or 32×32
|
||||||
image_data.truncate(0)
|
image_data.truncate(0)
|
||||||
|
@ -53,6 +56,7 @@ def resize_until_small(image_data: io.BytesIO) -> None:
|
||||||
except wand.exceptions.CoderError:
|
except wand.exceptions.CoderError:
|
||||||
raise errors.InvalidImageError
|
raise errors.InvalidImageError
|
||||||
|
|
||||||
|
|
||||||
def convert_to_gif(image_data: io.BytesIO) -> None:
|
def convert_to_gif(image_data: io.BytesIO) -> None:
|
||||||
try:
|
try:
|
||||||
with wand.image.Image(blob=image_data) as orig, orig.convert('gif') as converted:
|
with wand.image.Image(blob=image_data) as orig, orig.convert('gif') as converted:
|
||||||
|
@ -68,6 +72,7 @@ def convert_to_gif(image_data: io.BytesIO) -> None:
|
||||||
except wand.exceptions.CoderError:
|
except wand.exceptions.CoderError:
|
||||||
raise errors.InvalidImageError
|
raise errors.InvalidImageError
|
||||||
|
|
||||||
|
|
||||||
def mime_type_for_image(data):
|
def mime_type_for_image(data):
|
||||||
if data.startswith(b'\x89PNG\r\n\x1a\n'):
|
if data.startswith(b'\x89PNG\r\n\x1a\n'):
|
||||||
return 'image/png'
|
return 'image/png'
|
||||||
|
@ -79,12 +84,14 @@ def mime_type_for_image(data):
|
||||||
return 'image/webp'
|
return 'image/webp'
|
||||||
raise errors.InvalidImageError
|
raise errors.InvalidImageError
|
||||||
|
|
||||||
|
|
||||||
def image_to_base64_url(data):
|
def image_to_base64_url(data):
|
||||||
fmt = 'data:{mime};base64,{data}'
|
fmt = 'data:{mime};base64,{data}'
|
||||||
mime = mime_type_for_image(data)
|
mime = mime_type_for_image(data)
|
||||||
b64 = base64.b64encode(data).decode('ascii')
|
b64 = base64.b64encode(data).decode('ascii')
|
||||||
return fmt.format(mime=mime, data=b64)
|
return fmt.format(mime=mime, data=b64)
|
||||||
|
|
||||||
|
|
||||||
def main() -> typing.NoReturn:
|
def main() -> typing.NoReturn:
|
||||||
"""resize or convert an image from stdin and write the resized or converted version to stdout."""
|
"""resize or convert an image from stdin and write the resized or converted version to stdout."""
|
||||||
import sys
|
import sys
|
||||||
|
@ -114,6 +121,7 @@ def main() -> typing.NoReturn:
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
async def process_image_in_subprocess(command_name, image_data: bytes):
|
async def process_image_in_subprocess(command_name, image_data: bytes):
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
sys.executable, '-m', __name__, command_name,
|
sys.executable, '-m', __name__, command_name,
|
||||||
|
@ -131,12 +139,15 @@ async def process_image_in_subprocess(command_name, image_data: bytes):
|
||||||
if proc.returncode == 2:
|
if proc.returncode == 2:
|
||||||
raise errors.InvalidImageError
|
raise errors.InvalidImageError
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(err.decode('utf-8') + f'Return code: {proc.returncode}')
|
raise RuntimeError(err.decode('utf-8') +
|
||||||
|
f'Return code: {proc.returncode}')
|
||||||
|
|
||||||
return image_data
|
return image_data
|
||||||
|
|
||||||
resize_in_subprocess = functools.partial(process_image_in_subprocess, 'resize')
|
resize_in_subprocess = functools.partial(process_image_in_subprocess, 'resize')
|
||||||
convert_to_gif_in_subprocess = functools.partial(process_image_in_subprocess, 'convert')
|
convert_to_gif_in_subprocess = functools.partial(
|
||||||
|
process_image_in_subprocess, 'convert')
|
||||||
|
|
||||||
|
|
||||||
def size(fp):
|
def size(fp):
|
||||||
"""return the size, in bytes, of the data a file-like object represents"""
|
"""return the size, in bytes, of the data a file-like object represents"""
|
||||||
|
@ -144,6 +155,7 @@ def size(fp):
|
||||||
fp.seek(0, io.SEEK_END)
|
fp.seek(0, io.SEEK_END)
|
||||||
return fp.tell()
|
return fp.tell()
|
||||||
|
|
||||||
|
|
||||||
class preserve_position(contextlib.AbstractContextManager):
|
class preserve_position(contextlib.AbstractContextManager):
|
||||||
def __init__(self, fp):
|
def __init__(self, fp):
|
||||||
self.fp = fp
|
self.fp = fp
|
||||||
|
@ -152,5 +164,6 @@ class preserve_position(contextlib.AbstractContextManager):
|
||||||
def __exit__(self, *excinfo):
|
def __exit__(self, *excinfo):
|
||||||
self.fp.seek(self.old_pos)
|
self.fp.seek(self.old_pos)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import nextcord
|
||||||
|
|
||||||
|
|
||||||
def format_user(user, *, mention=False):
|
def format_user(user, *, mention=False):
|
||||||
"""Format a user object for audit log purposes."""
|
"""Format a user object for audit log purposes."""
|
||||||
|
@ -18,8 +19,9 @@ def format_user(user, *, mention=False):
|
||||||
else:
|
else:
|
||||||
return f'@{user} ({user.id})'
|
return f'@{user} ({user.id})'
|
||||||
|
|
||||||
def format_http_exception(exception: discord.HTTPException):
|
|
||||||
"""Formats a discord.HTTPException for relaying to the user.
|
def format_http_exception(exception: nextcord.HTTPException):
|
||||||
|
"""Formats a nextcord.HTTPException for relaying to the user.
|
||||||
Sample return value:
|
Sample return value:
|
||||||
|
|
||||||
BAD REQUEST (status code: 400):
|
BAD REQUEST (status code: 400):
|
||||||
|
@ -30,6 +32,7 @@ def format_http_exception(exception: discord.HTTPException):
|
||||||
f'{exception.response.reason} (status code: {exception.response.status}):'
|
f'{exception.response.reason} (status code: {exception.response.status}):'
|
||||||
f'\n{exception.text}')
|
f'\n{exception.text}')
|
||||||
|
|
||||||
|
|
||||||
def strip_angle_brackets(string):
|
def strip_angle_brackets(string):
|
||||||
"""Strip leading < and trailing > from a string.
|
"""Strip leading < and trailing > from a string.
|
||||||
Useful if a user sends you a url like <this> to avoid embeds, or to convert emotes to reactions."""
|
Useful if a user sends you a url like <this> to avoid embeds, or to convert emotes to reactions."""
|
||||||
|
@ -37,6 +40,7 @@ def strip_angle_brackets(string):
|
||||||
return string[1:-1]
|
return string[1:-1]
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
async def gather_or_cancel(*awaitables, loop=None):
|
async def gather_or_cancel(*awaitables, loop=None):
|
||||||
"""run the awaitables in the sequence concurrently. If any of them raise an exception,
|
"""run the awaitables in the sequence concurrently. If any of them raise an exception,
|
||||||
propagate the first exception raised and cancel all other awaitables.
|
propagate the first exception raised and cancel all other awaitables.
|
||||||
|
|
|
@ -5,12 +5,13 @@ import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
import discord
|
import nextcord
|
||||||
from discord.ext.commands import Context
|
from nextcord.ext.commands import Context
|
||||||
|
|
||||||
# Copyright © 2016-2017 Pandentia and contributors
|
# Copyright © 2016-2017 Pandentia and contributors
|
||||||
# https://github.com/Thessia/Liara/blob/75fa11948b8b2ea27842d8815a32e51ef280a999/cogs/utils/paginator.py
|
# https://github.com/Thessia/Liara/blob/75fa11948b8b2ea27842d8815a32e51ef280a999/cogs/utils/paginator.py
|
||||||
|
|
||||||
|
|
||||||
class Paginator:
|
class Paginator:
|
||||||
def __init__(self, ctx: Context, pages: typing.Iterable, *, timeout=300, delete_message=False,
|
def __init__(self, ctx: Context, pages: typing.Iterable, *, timeout=300, delete_message=False,
|
||||||
delete_message_on_timeout=False, text_message=None):
|
delete_message_on_timeout=False, text_message=None):
|
||||||
|
@ -39,7 +40,7 @@ class Paginator:
|
||||||
|
|
||||||
self._page = None
|
self._page = None
|
||||||
|
|
||||||
def react_check(self, reaction: discord.RawReactionActionEvent):
|
def react_check(self, reaction: nextcord.RawReactionActionEvent):
|
||||||
if reaction.user_id != self.author.id:
|
if reaction.user_id != self.author.id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -47,12 +48,12 @@ class Paginator:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target_emoji = str(reaction.emoji)
|
target_emoji = str(reaction.emoji)
|
||||||
return bool(discord.utils.find(lambda emoji: target_emoji == emoji, self.navigation))
|
return bool(nextcord.utils.find(lambda emoji: target_emoji == emoji, self.navigation))
|
||||||
|
|
||||||
async def begin(self):
|
async def begin(self):
|
||||||
"""Starts pagination"""
|
"""Starts pagination"""
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self._embed = discord.Embed()
|
self._embed = nextcord.Embed()
|
||||||
await self.first_page()
|
await self.first_page()
|
||||||
for button in self.navigation:
|
for button in self.navigation:
|
||||||
await self._message.add_reaction(button)
|
await self._message.add_reaction(button)
|
||||||
|
@ -69,8 +70,8 @@ class Paginator:
|
||||||
await self.navigation[str(reaction.emoji)]()
|
await self.navigation[str(reaction.emoji)]()
|
||||||
|
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
with contextlib.suppress(discord.HTTPException):
|
with contextlib.suppress(nextcord.HTTPException):
|
||||||
await self._message.remove_reaction(reaction.emoji, discord.Object(reaction.user_id))
|
await self._message.remove_reaction(reaction.emoji, nextcord.Object(reaction.user_id))
|
||||||
|
|
||||||
async def stop(self, *, delete=None):
|
async def stop(self, *, delete=None):
|
||||||
"""Aborts pagination."""
|
"""Aborts pagination."""
|
||||||
|
@ -78,7 +79,7 @@ class Paginator:
|
||||||
delete = self.delete_msg
|
delete = self.delete_msg
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
with contextlib.suppress(discord.HTTPException):
|
with contextlib.suppress(nextcord.HTTPException):
|
||||||
await self._message.delete()
|
await self._message.delete()
|
||||||
else:
|
else:
|
||||||
await self._clear_reactions()
|
await self._clear_reactions()
|
||||||
|
@ -87,16 +88,17 @@ class Paginator:
|
||||||
async def _clear_reactions(self):
|
async def _clear_reactions(self):
|
||||||
try:
|
try:
|
||||||
await self._message.clear_reactions()
|
await self._message.clear_reactions()
|
||||||
except discord.Forbidden:
|
except nextcord.Forbidden:
|
||||||
for button in self.navigation:
|
for button in self.navigation:
|
||||||
with contextlib.suppress(discord.HTTPException):
|
with contextlib.suppress(nextcord.HTTPException):
|
||||||
await self._message.remove_reaction(button, self._message.author)
|
await self._message.remove_reaction(button, self._message.author)
|
||||||
except discord.HTTPException:
|
except nextcord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def format_page(self):
|
async def format_page(self):
|
||||||
self._embed.description = self.pages[self._page]
|
self._embed.description = self.pages[self._page]
|
||||||
self._embed.set_footer(text=self.footer.format(self._page + 1, len(self.pages)))
|
self._embed.set_footer(text=self.footer.format(
|
||||||
|
self._page + 1, len(self.pages)))
|
||||||
|
|
||||||
kwargs = {'embed': self._embed}
|
kwargs = {'embed': self._embed}
|
||||||
if self.text_message:
|
if self.text_message:
|
||||||
|
@ -127,6 +129,7 @@ class Paginator:
|
||||||
self._page = len(self.pages) - 1
|
self._page = len(self.pages) - 1
|
||||||
await self.format_page()
|
await self.format_page()
|
||||||
|
|
||||||
|
|
||||||
class ListPaginator(Paginator):
|
class ListPaginator(Paginator):
|
||||||
def __init__(self, ctx, _list: list, per_page=10, **kwargs):
|
def __init__(self, ctx, _list: list, per_page=10, **kwargs):
|
||||||
pages = []
|
pages = []
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue