1
0
Fork 0
mirror of https://github.com/uhIgnacio/EmoteManager.git synced 2024-08-15 02:23:13 +00:00
EmoteManager/cogs/emote.py

601 lines
21 KiB
Python
Raw Normal View History

2020-05-12 23:55:08 +00:00
# © 20182020 io mintz <io@mintz.cc>
#
# Emote Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Emote Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Emote Manager. If not, see <https://www.gnu.org/licenses/>.
2018-07-30 04:04:20 +00:00
import asyncio
2019-06-04 03:08:33 +00:00
import cgi
import collections
import contextlib
2019-08-04 10:15:15 +00:00
import io
import json
import logging
import operator
2019-08-04 10:15:15 +00:00
import posixpath
import re
import traceback
2018-08-01 01:46:21 +00:00
import urllib.parse
2019-10-15 21:59:34 +00:00
import zipfile
import warnings
import weakref
import aiohttp
import discord
import humanize
2018-07-30 04:04:20 +00:00
from discord.ext import commands
import utils
2018-07-30 05:26:15 +00:00
import utils.image
from utils import errors
2018-07-30 05:43:30 +00:00
from utils.paginator import ListPaginator
from utils.emote_client import EmoteClient
from utils.converter import emote_type_filter_default
2018-07-31 10:13:54 +00:00
logger = logging.getLogger(__name__)
2019-10-15 21:59:34 +00:00
# guilds can have duplicate emotes, so let us create zips to match
warnings.filterwarnings('ignore', module='zipfile', category=UserWarning, message=r"^Duplicate name: .*$")
class UserCancelledError(commands.UserInputError):
pass
2019-03-14 00:19:14 +00:00
class Emotes(commands.Cog):
2020-06-11 23:57:35 +00:00
IMAGE_MIMETYPES = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
2019-08-04 10:15:15 +00:00
# TAR_MIMETYPES = {'application/x-tar', 'application/x-xz', 'application/gzip', 'application/x-bzip2'}
TAR_MIMETYPES = {'application/x-tar'}
ZIP_MIMETYPES = {'application/zip', 'application/octet-stream', 'application/x-zip-compressed', 'multipart/x-zip'}
ARCHIVE_MIMETYPES = TAR_MIMETYPES | ZIP_MIMETYPES
2020-06-23 22:51:54 +00:00
ZIP_OVERHEAD_BYTES = 30
2019-08-04 10:15:15 +00:00
2018-07-30 04:04:20 +00:00
def __init__(self, bot):
self.bot = bot
connector = None
socks5_url = self.bot.config.get('socks5_proxy_url')
if socks5_url:
from aiohttp_socks import SocksConnector
connector = SocksConnector.from_url(socks5_url, rdns=True)
self.http = aiohttp.ClientSession(
loop=self.bot.loop,
read_timeout=self.bot.config.get('http_read_timeout', 60),
connector=connector if self.bot.config.get('use_socks5_for_all_connections') else None,
headers={
'User-Agent':
self.bot.config['user_agent'] + ' '
+ self.bot.http.user_agent
})
self.emote_client = EmoteClient(self.bot)
with open('data/ec-emotes-final.json') as f:
self.ec_emotes = json.load(f)
2018-07-30 05:43:30 +00:00
# keep track of paginators so we can end them when the cog is unloaded
self.paginators = weakref.WeakSet()
2019-03-14 00:19:14 +00:00
def cog_unload(self):
async def close():
await self.http.close()
await self.emote_client.close()
2018-07-30 05:43:30 +00:00
for paginator in self.paginators:
await paginator.stop()
self.bot.loop.create_task(close())
2020-09-29 00:13:09 +00:00
public_commands = set()
def public(command, public_commands=public_commands): # resolve some kinda scope issue that i don't understand
public_commands.add(command.qualified_name)
return command
2019-03-14 00:19:14 +00:00
async def cog_check(self, context):
if not context.guild:
raise commands.NoPrivateMessage
2018-08-12 02:08:23 +00:00
2020-09-29 09:12:48 +00:00
# we can't just do `context.command in self.public_commands` here
2020-09-29 00:13:09 +00:00
# because apparently Command.__eq__ is not defined
if context.command.qualified_name in self.public_commands:
2018-08-12 02:08:23 +00:00
return True
if (
not context.author.guild_permissions.manage_emojis
or not context.guild.me.guild_permissions.manage_emojis
):
raise errors.MissingManageEmojisPermission
return True
2020-05-13 01:37:26 +00:00
@commands.Cog.listener()
async def on_command_error(self, context, error):
if isinstance(error, errors.EmoteManagerError):
2020-05-13 08:26:32 +00:00
await context.send(error)
2020-05-13 01:37:26 +00:00
if isinstance(error, commands.NoPrivateMessage):
await context.send(
f'{utils.SUCCESS_EMOJIS[False]} Sorry, this command may only be used in a server.')
2018-10-09 06:07:52 +00:00
@commands.command(usage='[name] <image URL or custom emote>')
async def add(self, context, *args):
2018-07-30 05:57:47 +00:00
"""Add a new emote to this server.
You can use it like this:
`add :thonkang:` (if you already have that emote)
`add rollsafe https://image.noelshack.com/fichiers/2017/06/1486495269-rollsafe.png`
`add speedtest <https://cdn.discordapp.com/emojis/379127000398430219.png>`
With a file attachment:
`add name` will upload a new emote using the first attachment as the image and call it `name`
`add` will upload a new emote using the first attachment as the image,
and its filename as the name
"""
name, url = self.parse_add_command_args(context, args)
async with context.typing():
message = await self.add_safe(context, name, url, context.message.author.id)
await context.send(message)
@commands.command(name='add-these')
async def add_these(self, context, *emotes):
"""Add a bunch of custom emotes."""
ran = False
# we could use *emotes: discord.PartialEmoji here but that would require spaces between each emote.
# and would fail if any arguments were not valid emotes
for match in re.finditer(utils.emote.RE_CUSTOM_EMOTE, ''.join(emotes)):
ran = True
animated, name, id = match.groups()
image_url = utils.emote.url(id, animated=animated)
async with context.typing():
message = await self.add_safe(context, name, image_url, context.author.id)
await context.send(message)
if not ran:
return await context.send('Error: no custom emotes were provided.')
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
@classmethod
def parse_add_command_args(cls, context, args):
if context.message.attachments:
return cls.parse_add_command_attachment(context, args)
elif len(args) == 1:
match = utils.emote.RE_CUSTOM_EMOTE.match(args[0])
if match is None:
raise commands.BadArgument(
'Error: I expected a custom emote as the first argument, '
'but I got something else. '
"If you're trying to add an emote using an image URL, "
'you need to provide a name as the first argument, like this:\n'
'`{}add NAME_HERE URL_HERE`'.format(context.prefix))
else:
animated, name, id = match.groups()
url = utils.emote.url(id, animated=animated)
return name, url
elif len(args) >= 2:
name = args[0]
match = utils.emote.RE_CUSTOM_EMOTE.match(args[1])
if match is None:
url = utils.strip_angle_brackets(args[1])
else:
url = utils.emote.url(match['id'], animated=match['animated'])
return name, url
elif not args:
raise commands.BadArgument('Your message had no emotes and no name!')
2019-08-04 10:15:15 +00:00
@classmethod
def parse_add_command_attachment(cls, context, args):
attachment = context.message.attachments[0]
2019-08-04 10:15:15 +00:00
name = cls.format_emote_filename(''.join(args) if args else attachment.filename)
url = attachment.url
return name, url
2019-08-04 10:15:15 +00:00
@staticmethod
def format_emote_filename(filename):
"""format a filename to an emote name as discord does when you upload an emote image"""
2020-07-06 04:52:34 +00:00
left, sep, right = posixpath.splitext(filename)[0].rpartition('-')
return (left or right).replace(' ', '')
2019-08-04 10:15:15 +00:00
2018-08-01 01:46:21 +00:00
@commands.command(name='add-from-ec', aliases=['addfromec'])
async def add_from_ec(self, context, name, *names):
"""Copies one or more emotes from Emote Collector to your server."""
if names:
for name in (name,) + names:
await context.invoke(self.add_from_ec, name)
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
return
2018-08-17 05:34:31 +00:00
try:
emote = self.ec_emotes[name.strip(':').lower()]
except KeyError:
2018-09-09 04:16:42 +00:00
return await context.send("Emote not found in Emote Collector's database.")
2018-08-01 01:46:21 +00:00
reason = (
2018-09-09 04:16:42 +00:00
f'Added from Emote Collector by {utils.format_user(self.bot, context.author.id)}. '
f'Original emote author: {utils.format_user(self.bot, emote["author"])}')
2018-08-01 01:46:21 +00:00
image_url = utils.emote.url(emote['id'], animated=emote['animated'])
2018-08-01 01:46:21 +00:00
async with context.typing():
message = await self.add_safe(context, name, image_url, context.author.id, reason=reason)
2018-08-01 01:46:21 +00:00
await context.send(message)
2020-09-29 00:13:09 +00:00
@public
2020-06-01 05:47:10 +00:00
@emote_type_filter_default
@commands.command()
@commands.bot_has_permissions(attach_files=True)
2020-06-01 05:47:10 +00:00
async def export(self, context, image_type='all'):
2019-10-15 21:59:34 +00:00
"""Export all emotes from this server to a zip file, suitable for use with the import command.
If animated is provided, only include animated emotes.
If static is provided, only include static emotes.
Otherwise, or if all is provided, export all emotes.
This command requires the attach files permission.
2019-10-15 21:59:34 +00:00
"""
emotes = list(filter(image_type, context.guild.emojis))
if not emotes:
raise commands.BadArgument('No emotes of that type were found in this server.')
async with context.typing():
2020-06-23 22:14:10 +00:00
async for zip_file in self.archive_emotes(context, emotes):
await context.send(file=zip_file)
async def archive_emotes(self, context, emotes):
2020-06-23 22:56:05 +00:00
filesize_limit = context.guild.filesize_limit
2020-06-23 22:14:10 +00:00
discrims = collections.defaultdict(int)
2020-06-23 22:51:54 +00:00
downloaded = collections.deque()
async def download(emote):
# don't put two files in the zip with the same name
discrims[emote.name] += 1
discrim = discrims[emote.name]
if discrim == 1:
name = emote.name
else:
name = f'{emote.name}-{discrim}'
name = f'{name}.{"gif" if emote.animated else "png"}'
# place some level of trust on discord's CDN to actually give us images
data = await self.fetch_safe(str(emote.url), validate_headers=False)
if type(data) is str: # error case
await context.send(f'{emote}: {data}')
return
est_zip_overhead = len(name) + self.ZIP_OVERHEAD_BYTES
est_size_in_zip = est_zip_overhead + len(data)
if est_size_in_zip >= filesize_limit:
self.bot.loop.create_task(
context.send(f'{emote} could not be added because it alone would exceed the file size limit.')
)
return
downloaded.append((name, emote.created_at, est_size_in_zip, data))
await utils.gather_or_cancel(*map(download, emotes))
2020-06-23 22:14:10 +00:00
count = 1
while True:
out = io.BytesIO()
2019-10-15 21:59:34 +00:00
with zipfile.ZipFile(out, 'w', compression=zipfile.ZIP_STORED) as zip:
2020-06-23 22:14:10 +00:00
while True:
try:
2020-06-23 22:51:54 +00:00
item = downloaded.popleft()
2020-06-23 22:14:10 +00:00
except IndexError:
break
2020-06-23 22:51:54 +00:00
name, created_at, est_size, image_data = item
if out.tell() + est_size >= filesize_limit:
2020-06-23 22:14:10 +00:00
# adding this emote would bring us over the file size limit
2020-06-23 22:51:54 +00:00
downloaded.appendleft(item)
2020-06-23 22:14:10 +00:00
break
2020-06-23 22:51:54 +00:00
zinfo = zipfile.ZipInfo(name, date_time=created_at.timetuple()[:6])
zip.writestr(zinfo, image_data)
2019-10-15 21:59:34 +00:00
2020-06-23 22:14:10 +00:00
if out.tell() == 0:
# no emotes were written
break
out.seek(0)
yield discord.File(out, f'emotes-{context.guild.id}-{count}.zip')
count += 1
2019-10-15 21:59:34 +00:00
@commands.command(name='import', aliases=['add-zip', 'add-tar', 'add-from-zip', 'add-from-tar'])
async def import_(self, context, url=None):
2019-08-04 10:15:15 +00:00
"""Add several emotes from a .zip or .tar archive.
You may either pass a URL to an archive or upload one as an attachment.
All valid GIF, PNG, and JPEG files in the archive will be uploaded as emotes.
The rest will be ignored.
2019-08-04 10:15:15 +00:00
"""
if url and context.message.attachments:
raise commands.BadArgument('Either a URL or an attachment must be given, not both.')
if not url and not context.message.attachments:
raise commands.BadArgument('A URL or attachment must be given.')
self.emote_client.check_create(context.guild.id)
2019-08-04 10:15:15 +00:00
url = url or context.message.attachments[0].url
async with context.typing():
archive = await self.fetch_safe(url, valid_mimetypes=self.ARCHIVE_MIMETYPES)
2019-08-04 10:15:15 +00:00
if type(archive) is str: # error case
await context.send(archive)
return
await self.add_from_archive(context, archive)
with contextlib.suppress(discord.HTTPException):
# so they know when we're done
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
2019-08-04 10:15:15 +00:00
async def add_from_archive(self, context, archive):
limit = 50_000_000 # prevent someone from trying to make a giant compressed file
async for name, img, error in utils.archive.extract_async(io.BytesIO(archive), size_limit=limit):
try:
utils.image.mime_type_for_image(img)
except errors.InvalidImageError:
continue
2019-08-04 10:15:15 +00:00
if error is None:
name = self.format_emote_filename(posixpath.basename(name))
2019-08-04 10:15:15 +00:00
async with context.typing():
message = await self.add_safe_bytes(context, name, context.author.id, img)
await context.send(message)
2019-08-04 10:15:15 +00:00
continue
if isinstance(error, errors.FileTooBigError):
await context.send(
f'{name}: file too big. '
f'The limit is {humanize.naturalsize(error.limit)} '
f'but this file is {humanize.naturalsize(error.size)}.')
2019-08-04 10:15:15 +00:00
continue
await context.send(f'{name}: {error}')
async def add_safe(self, context, name, url, author_id, *, reason=None):
"""Try to add an emote. Returns a string that should be sent to the user."""
self.emote_client.check_create(context.guild.id)
try:
2019-08-04 10:15:15 +00:00
image_data = await self.fetch_safe(url)
except errors.InvalidFileError:
raise errors.InvalidImageError
if type(image_data) is str: # error case (shitty i know)
2019-08-04 10:15:15 +00:00
return image_data
return await self.add_safe_bytes(context, name, author_id, image_data, reason=reason)
2019-08-04 10:15:15 +00:00
2019-10-15 21:59:34 +00:00
async def fetch_safe(self, url, valid_mimetypes=None, *, validate_headers=False):
2019-08-04 10:15:15 +00:00
"""Try to fetch a URL. On error return a string that should be sent to the user."""
try:
2019-10-15 21:59:34 +00:00
return await self.fetch(url, valid_mimetypes=valid_mimetypes, validate_headers=validate_headers)
except asyncio.TimeoutError:
return 'Error: retrieving the image took too long.'
except ValueError:
return 'Error: Invalid URL.'
except aiohttp.ClientResponseError as exc:
raise errors.HTTPException(exc.status)
async def add_safe_bytes(self, context, name, author_id, image_data: bytes, *, reason=None):
"""Try to add an emote from bytes. On error, return a string that should be sent to the user.
If the image is static and there are not enough free static slots, convert the image to a gif instead.
"""
counts = collections.Counter(map(operator.attrgetter('animated'), context.guild.emojis))
# >= rather than == because there are sneaky ways to exceed the limit
if counts[False] >= context.guild.emoji_limit and counts[True] >= context.guild.emoji_limit:
# we raise instead of returning a string in order to abort commands that run this function in a loop
raise commands.UserInputError('This server is out of emote slots.')
static = utils.image.mime_type_for_image(image_data) != 'image/gif'
converted = False
if static and counts[False] >= context.guild.emoji_limit:
image_data = await utils.image.convert_to_gif_in_subprocess(image_data)
converted = True
2019-08-04 10:15:15 +00:00
try:
emote = await self.create_emote_from_bytes(context.guild, name, author_id, image_data, reason=reason)
except discord.InvalidArgument:
2020-06-11 23:59:13 +00:00
return discord.utils.escape_mentions(f'{name}: The file supplied was not a valid GIF, PNG, JPEG, or WEBP file.')
2019-08-04 10:15:15 +00:00
except discord.HTTPException as ex:
return discord.utils.escape_mentions(
f'{name}: An error occurred while creating the the emote:\n'
2019-08-04 10:15:15 +00:00
+ utils.format_http_exception(ex))
s = f'Emote {emote} successfully created'
return s + ' as a GIF.' if converted else s + '.'
2020-06-11 23:59:13 +00:00
async def fetch(self, url, valid_mimetypes=IMAGE_MIMETYPES, *, validate_headers=True):
2019-08-04 10:15:15 +00:00
valid_mimetypes = valid_mimetypes or self.IMAGE_MIMETYPES
2019-06-04 03:08:33 +00:00
def validate_headers(response):
response.raise_for_status()
2019-06-04 03:08:33 +00:00
# some dumb servers also send '; charset=UTF-8' which we should ignore
mimetype, options = cgi.parse_header(response.headers.get('Content-Type', ''))
2019-08-04 10:15:15 +00:00
if mimetype not in valid_mimetypes:
raise errors.InvalidFileError
2019-08-04 10:15:15 +00:00
async def validate(request):
try:
async with request as response:
validate_headers(response)
return await response.read()
except aiohttp.ClientResponseError:
raise
2019-08-04 10:15:15 +00:00
except aiohttp.ClientError as exc:
2020-01-18 00:20:40 +00:00
raise errors.EmoteManagerError(f'An error occurred while retrieving the file: {exc}')
2019-06-04 03:08:33 +00:00
2019-10-15 21:59:34 +00:00
if validate_headers: await validate(self.http.head(url, timeout=self.bot.config.get('http_head_timeout', 10)))
2019-08-04 10:15:15 +00:00
return await validate(self.http.get(url))
2019-06-04 03:08:33 +00:00
async def create_emote_from_bytes(self, guild, name, author_id, image_data: bytes, *, reason=None):
image_data = await utils.image.resize_in_subprocess(image_data)
2018-08-01 01:46:21 +00:00
if reason is None:
reason = 'Created by ' + utils.format_user(self.bot, author_id)
return await self.emote_client.create(guild=guild, name=name, image=image_data, reason=reason)
2018-08-17 07:13:38 +00:00
@commands.command(aliases=('delete', 'delet', 'rm'))
async def remove(self, context, emote, *emotes):
"""Remove an emote from this server.
emotes: the name of an emote or of one or more emotes you'd like to remove.
"""
if not emotes:
emote = await self.parse_emote(context, emote)
await self.emote_client.delete(
guild_id=context.guild.id,
emote_id=emote.id,
reason='Removed by ' + utils.format_user(self.bot, context.author.id),
)
await context.send(fr'Emote \:{emote.name}: successfully removed.')
else:
for emote in (emote,) + emotes:
await context.invoke(self.remove, emote)
with contextlib.suppress(discord.HTTPException):
2018-08-22 15:27:35 +00:00
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
2018-07-30 05:15:09 +00:00
2018-08-17 07:13:38 +00:00
@commands.command(aliases=('mv',))
async def rename(self, context, old, new_name):
2018-07-30 05:57:47 +00:00
"""Rename an emote on this server.
old: the name of the emote to rename, or the emote itself
2018-07-30 05:57:47 +00:00
new_name: what you'd like to rename it to
"""
emote = await self.parse_emote(context, old)
2018-07-30 05:33:13 +00:00
try:
await emote.edit(
name=new_name,
reason=f'Renamed by {utils.format_user(self.bot, context.author.id)}')
except discord.HTTPException as ex:
return await context.send(
'An error occurred while renaming the emote:\n'
+ utils.format_http_exception(ex))
await context.send(fr'Emote successfully renamed to \:{new_name}:')
2018-07-30 05:33:13 +00:00
2020-09-29 00:13:09 +00:00
@public
2020-06-01 05:47:10 +00:00
@emote_type_filter_default
@commands.command(aliases=('ls', 'dir'))
2020-06-01 05:47:10 +00:00
async def list(self, context, image_type='all'):
2018-07-31 10:13:54 +00:00
"""A list of all emotes on this server.
The list shows each emote and its raw form.
2018-11-09 18:31:32 +00:00
If "animated" is provided, only show animated emotes.
If "static" is provided, only show static emotes.
2020-06-01 05:47:10 +00:00
If all is provided, show all emotes.
2018-07-31 10:13:54 +00:00
"""
2018-07-30 05:43:30 +00:00
emotes = sorted(
2020-06-01 05:47:10 +00:00
filter(image_type, context.guild.emojis),
2018-07-30 05:43:30 +00:00
key=lambda e: e.name.lower())
processed = []
for emote in emotes:
raw = str(emote).replace(':', r'\:')
2018-07-31 10:13:54 +00:00
processed.append(f'{emote} {raw}')
2018-07-30 05:43:30 +00:00
paginator = ListPaginator(context, processed)
self.paginators.add(paginator)
await paginator.begin()
2020-09-29 00:13:09 +00:00
@public
2020-04-28 02:57:49 +00:00
@commands.command(aliases=['status'])
async def stats(self, context):
"""The current number of animated and static emotes relative to the limits."""
emote_limit = context.guild.emoji_limit
2020-04-26 02:12:45 +00:00
static_emotes = animated_emotes = total_emotes = 0
for emote in context.guild.emojis:
if emote.animated:
animated_emotes += 1
else:
static_emotes += 1
total_emotes += 1
percent_static = round((static_emotes / emote_limit) * 100, 2)
percent_animated = round((animated_emotes / emote_limit) * 100, 2)
2020-04-26 02:12:45 +00:00
static_left = emote_limit - static_emotes
animated_left = emote_limit - animated_emotes
2020-04-26 02:12:45 +00:00
await context.send(
f'Static emotes: **{static_emotes} / {emote_limit}** ({static_left} left, {percent_static}% full)\n'
f'Animated emotes: **{animated_emotes} / {emote_limit}** ({animated_left} left, {percent_animated}% full)\n'
f'Total: **{total_emotes} / {emote_limit * 2}**')
2020-04-26 02:12:45 +00:00
2021-02-16 06:31:24 +00:00
@commands.command(aliases=["embiggen"])
async def big(self, context, emote):
"""Shows the original image for the given emote.
emote: the emote to embiggen.
"""
emote = await self.parse_emote(context, emote, local=False)
2021-02-16 06:31:24 +00:00
await context.send(f'{emote.name}: {emote.url}')
async def parse_emote(self, context, name_or_emote, *, local=True):
# this function is mostly synchronous,
# so we yield in order to let the emoji cache update between repeated calls
await asyncio.sleep(0)
match = utils.emote.RE_CUSTOM_EMOTE.match(name_or_emote)
if match:
id = int(match['id'])
if local:
emote = discord.utils.get(context.guild.emojis, id=id)
if emote:
return emote
else:
return discord.PartialEmoji(
animated=bool(match['animated']),
name=match['name'],
id=int(match['id']),
)
name = name_or_emote
return await self.disambiguate(context, name)
2018-07-30 05:15:09 +00:00
async def disambiguate(self, context, name):
name = name.strip(':') # in case the user tries :foo: and foo is animated
2018-07-30 05:15:09 +00:00
candidates = [e for e in context.guild.emojis if e.name.lower() == name.lower() and e.require_colons]
if not candidates:
raise errors.EmoteNotFoundError(name)
if len(candidates) == 1:
return candidates[0]
message = ['Multiple emotes were found with that name. Which one do you mean?']
for i, emote in enumerate(candidates, 1):
message.append(fr'{i}. {emote} (\:{emote.name}:)')
2018-07-30 05:15:09 +00:00
await context.send('\n'.join(message))
def check(message):
try:
int(message.content)
except ValueError:
return False
else:
return message.author == context.author
try:
message = await self.bot.wait_for('message', check=check, timeout=30)
except asyncio.TimeoutError:
raise commands.UserInputError('Sorry, you took too long. Try again.')
return candidates[int(message.content)-1]
2018-07-30 04:04:20 +00:00
def setup(bot):
2018-07-30 04:42:27 +00:00
bot.add_cog(Emotes(bot))