1
0
Fork 0
mirror of https://github.com/uhIgnacio/EmoteManager.git synced 2024-08-15 02:23:13 +00:00
This commit is contained in:
bmintz 2018-11-09 18:33:16 +00:00
commit 4d336d419f
9 changed files with 213 additions and 45 deletions

34
README.md Normal file
View file

@ -0,0 +1,34 @@
# Emote Manager
[![Discord Bots](https://discordbots.org/api/widget/status/473370418007244852.svg?noavatar=true)](https://discordbots.org/bot/473370418007244852)
Need to edit your server's custom emotes from your phone? Just add this simple bot, and use its commands to do it for you!
**Note:** both you and the bot will need the "Manage Emojis" permission to edit custom server emotes.
To add the bot to your server, visit https://discordapp.com/oauth2/authorize?client_id=473370418007244852&scope=bot&permissions=1074023488.
## Commands
<p>
To add an emote:
<ul>
<li><code>@Emote Manager add <img class="emote" src="https://cdn.discordapp.com/emojis/407347328606011413.png?v=1&size=32" alt=":thonkang:" title=":thonkang:"></code> (if you already have that emote)
<li><code>@Emote Manager add rollsafe &lt;https://image.noelshack.com/fichiers/2017/06/1486495269-rollsafe.png&gt;</code>
<li><code>@Emote Manager add speedtest https://cdn.discordapp.com/emojis/379127000398430219.png</code>
</ul>
If you invoke <code>@Emote Manager add</code> with an image upload, the image will be used as the emote image, and the filename will be used as the emote name. To choose a different name, simply run it like<br>
<code>@Emote Manager add :some_emote:</code> instead.
</p>
<p>
<code>@Emote Manager list</code> gives you a list of all emotes on this server.
</p>
<p>
<code>@Emote Manager remove emote</code> will remove :emote:.
</p>
<p>
<code>@Emote Manager rename old_name new_name</code> will rename :old_name: to :new_name:.
</p>

18
bot.py
View file

@ -13,14 +13,28 @@ logger.setLevel(logging.INFO)
class Bot(commands.AutoShardedBot): class Bot(commands.AutoShardedBot):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(command_prefix=commands.when_mentioned, **kwargs)
with open('config.py') as f: with open('config.py') as f:
self.config = eval(f.read(), {}) self.config = eval(f.read(), {})
super().__init__(
command_prefix=commands.when_mentioned,
description=self.config.get('description', ''),
**kwargs)
self._setup_success_emojis()
for cog in self.config['cogs']: for cog in self.config['cogs']:
self.load_extension(cog) self.load_extension(cog)
def _setup_success_emojis(self):
"""Load the emojis from the config to be used when a command fails or succeeds
We do it this way so that they can be used anywhere instead of requiring a bot instance.
"""
import utils.misc
default = ('', '')
utils.SUCCESS_EMOJIS = utils.misc.SUCCESS_EMOJIS = (
self.config.get('response_emojis', {}).get('success', default))
def run(self): def run(self):
super().run(self.config['tokens'].pop('discord')) super().run(self.config['tokens'].pop('discord'))

View file

@ -8,7 +8,9 @@ import logging
import weakref import weakref
import traceback import traceback
import contextlib import contextlib
import urllib.parse
import aioec
import aiohttp import aiohttp
import discord import discord
from discord.ext import commands from discord.ext import commands
@ -28,11 +30,13 @@ class Emotes:
self.bot.config['user_agent'] + ' ' self.bot.config['user_agent'] + ' '
+ self.bot.http.user_agent + self.bot.http.user_agent
}) })
self.aioec = aioec.Client(loop=self.bot.loop)
# keep track of paginators so we can end them when the cog is unloaded # keep track of paginators so we can end them when the cog is unloaded
self.paginators = weakref.WeakSet() self.paginators = weakref.WeakSet()
def __unload(self): def __unload(self):
self.bot.loop.create_task(self.http.close()) self.bot.loop.create_task(self.http.close())
self.bot.loop.create_task(self.aioec.close())
async def stop_all_paginators(): async def stop_all_paginators():
for paginator in self.paginators: for paginator in self.paginators:
@ -41,9 +45,11 @@ class Emotes:
self.bot.loop.create_task(stop_all_paginators()) self.bot.loop.create_task(stop_all_paginators())
async def __local_check(self, context): async def __local_check(self, context):
if not context.guild: if not context.guild or not isinstance(context.author, discord.Member):
raise commands.NoPrivateMessage raise commands.NoPrivateMessage
return False
if context.command is self.list:
return True
if context.command is self.list: if context.command is self.list:
return True return True
@ -62,9 +68,9 @@ class Emotes:
if isinstance(error, commands.NoPrivateMessage): if isinstance(error, commands.NoPrivateMessage):
await context.send( await context.send(
f'{utils.SUCCESS_EMOTES[False]} Sorry, this command may only be used in a server.') f'{utils.SUCCESS_EMOJIS[False]} Sorry, this command may only be used in a server.')
@commands.command() @commands.command(usage='[name] <image URL or custom emote>')
async def add(self, context, *args): async def add(self, context, *args):
"""Add a new emote to this server. """Add a new emote to this server.
@ -78,11 +84,7 @@ class Emotes:
`add` will upload a new emote using the first attachment as the image, `add` will upload a new emote using the first attachment as the image,
and its filename as the name and its filename as the name
""" """
try:
name, url = self.parse_add_command_args(context, args) name, url = self.parse_add_command_args(context, args)
except commands.BadArgument as exception:
return await context.send(exception)
async with context.typing(): async with context.typing():
message = await self.add_safe(context.guild, name, url, context.message.author.id) message = await self.add_safe(context.guild, name, url, context.message.author.id)
await context.send(message) await context.send(message)
@ -113,7 +115,7 @@ class Emotes:
if match is None: if match is None:
url = utils.strip_angle_brackets(args[1]) url = utils.strip_angle_brackets(args[1])
else: else:
url = utils.emote.url(match.group('id')) url = utils.emote.url(match['id'], animated=match['animated'])
return name, url return name, url
@ -129,10 +131,42 @@ class Emotes:
return name, url return name, url
async def add_safe(self, guild, name, url, author_id): @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.
The list of possible emotes you can copy is here:
https://emote-collector.python-for.life/list
"""
if names:
for name in (name,) + names:
await context.invoke(self.add_from_ec, name)
return
name = name.strip(':')
try:
emote = await self.aioec.emote(name)
except aioec.NotFound:
return await context.send("Emote not found in Emote Collector's database.")
except aioec.HttpException as exception:
return await context.send(
f'Error: the Emote Collector API returned status code {exception.status}')
reason = (
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)}')
async with context.typing():
message = await self.add_safe(context.guild, name, utils.emote.url(
emote.id, animated=emote.animated
), context.author.id, reason=reason)
await context.send(message)
async def add_safe(self, guild, name, url, author_id, *, reason=None):
"""Try to add an emote. Returns a string that should be sent to the user.""" """Try to add an emote. Returns a string that should be sent to the user."""
try: try:
emote = await self.add_from_url(guild, name, url, author_id) emote = await self.add_from_url(guild, name, url, author_id, reason=reason)
except discord.HTTPException as ex: except discord.HTTPException as ex:
return ( return (
'An error occurred while creating the emote:\n' 'An error occurred while creating the emote:\n'
@ -144,9 +178,9 @@ class Emotes:
else: else:
return f'Emote {emote} successfully created.' return f'Emote {emote} successfully created.'
async def add_from_url(self, guild, name, url, author_id): async def add_from_url(self, guild, name, url, author_id, *, reason=None):
image_data = await self.fetch_emote(url) image_data = await self.fetch_emote(url)
emote = await self.create_emote_from_bytes(guild, name, author_id, image_data) emote = await self.create_emote_from_bytes(guild, name, author_id, image_data, reason=reason)
return emote return emote
@ -164,38 +198,42 @@ class Emotes:
raise errors.HTTPException(response.status) raise errors.HTTPException(response.status)
return io.BytesIO(await response.read()) return io.BytesIO(await response.read())
async def create_emote_from_bytes(self, guild, name, author_id, image_data: io.BytesIO): async def create_emote_from_bytes(self, guild, name, author_id, image_data: io.BytesIO, *, reason=None):
# resize_until_small is normally blocking, because wand is. # resize_until_small is normally blocking, because wand is.
# run_in_executor is magic that makes it non blocking somehow. # run_in_executor is magic that makes it non blocking somehow.
# also, None as the executor arg means "use the loop's default executor" # also, None as the executor arg means "use the loop's default executor"
image_data = await self.bot.loop.run_in_executor(None, utils.image.resize_until_small, image_data) image_data = await self.bot.loop.run_in_executor(None, utils.image.resize_until_small, image_data)
if reason is None:
reason = f'Created by {utils.format_user(self.bot, author_id)}'
return await guild.create_custom_emoji( return await guild.create_custom_emoji(
name=name, name=name,
image=image_data.read(), image=image_data.read(),
reason=f'Created by {utils.format_user(self.bot, author_id)}') reason=reason)
@commands.command() @commands.command(aliases=('delete', 'delet', 'rm'))
async def remove(self, context, *names): async def remove(self, context, emote, *emotes):
"""Remove an emote from this server. """Remove an emote from this server.
names: the names of one or more emotes you'd like to remove. emotes: the name of an emote or of one or more emotes you'd like to remove.
""" """
if len(names) == 1: if not emotes:
emote = await self.disambiguate(context, names[0]) emote = await self.parse_emote(context, emote)
await emote.delete(reason=f'Removed by {utils.format_user(self.bot, context.author.id)}') await emote.delete(reason=f'Removed by {utils.format_user(self.bot, context.author.id)}')
await context.send(f'Emote \:{emote.name}: successfully removed.') await context.send(f'Emote \:{emote.name}: successfully removed.')
else: else:
for name in names: for emote in (emote,) + emotes:
await context.invoke(self.remove, name) await context.invoke(self.remove, emote)
with contextlib.suppress(discord.HTTPException):
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
@commands.command() @commands.command(aliases=('mv',))
async def rename(self, context, old_name, new_name): async def rename(self, context, old, new_name):
"""Rename an emote on this server. """Rename an emote on this server.
old_name: the name of the emote to rename old: the name of the emote to rename, or the emote itself
new_name: what you'd like to rename it to new_name: what you'd like to rename it to
""" """
emote = await self.disambiguate(context, old_name) emote = await self.parse_emote(context, old)
try: try:
await emote.edit( await emote.edit(
name=new_name, name=new_name,
@ -205,9 +243,9 @@ class Emotes:
'An error occurred while renaming the emote:\n' 'An error occurred while renaming the emote:\n'
+ utils.format_http_exception(ex)) + utils.format_http_exception(ex))
await context.send(f'Emote \:{old_name}: successfully renamed to \:{new_name}:') await context.send(f'Emote successfully renamed to \:{new_name}:')
@commands.command() @commands.command(aliases=('ls', 'dir'))
async def list(self, context, animated=''): async def list(self, context, animated=''):
"""A list of all emotes on this server. """A list of all emotes on this server.
@ -239,7 +277,18 @@ class Emotes:
self.paginators.add(paginator) self.paginators.add(paginator)
await paginator.begin() await paginator.begin()
async def parse_emote(self, context, name_or_emote):
match = utils.emote.RE_CUSTOM_EMOTE.match(name_or_emote)
if match:
id = int(match.group('id'))
emote = discord.utils.get(context.guild.emojis, id=id)
if emote:
return emote
name = name_or_emote
return await self.disambiguate(context, name)
async def disambiguate(self, context, name): async def disambiguate(self, context, name):
name = name.strip(':') # in case the user tries :foo: and foo is animated
candidates = [e for e in context.guild.emojis if e.name.lower() == name.lower() and e.require_colons] candidates = [e for e in context.guild.emojis if e.name.lower() == name.lower() and e.require_colons]
if not candidates: if not candidates:
raise errors.EmoteNotFoundError(name) raise errors.EmoteNotFoundError(name)

45
cogs/meta.py Normal file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python3
# encoding: utf-8
import contextlib
import discord
from discord.ext import commands
class Meta:
def __init__(self, bot):
self.bot = bot
@commands.command(aliases=['inv'])
async def invite(self, context):
"""Gives you a link to add me to your server."""
permissions = discord.Permissions()
permissions.update(**dict.fromkeys((
'read_messages',
'send_messages',
'add_reactions',
'external_emojis',
'manage_emojis',
'embed_links',
), True))
await context.send('<%s>' % discord.utils.oauth_url(self.bot.user.id, permissions))
@commands.command()
async def support(self, context):
"""Directs you to the support server."""
try:
await context.author.send(self.bot.config['support_server_invite'])
with contextlib.suppress(discord.HTTPException):
await context.message.add_reaction('📬') # TODO make this emoji configurable too
except discord.Forbidden:
with contextlib.suppress(discord.HTTPException):
await context.message.add_reaction(utils.SUCCESS_EMOJIS[True])
await context.send('Unable to send invite in DMs. Please allow DMs from server members.')
def setup(bot):
bot.add_cog(Meta(bot))
if not bot.config.get('support_server_invite'):
bot.remove_command('support')

View file

@ -1,6 +1,14 @@
{ {
'description':
'Emote Manager lets you manage custom server emotes from your phone.\n\n'
'NOTE: Most commands will be unavailable until both you and the bot have the '
'"Manage Emojis" permission.',
'support_server_invite': 'https://discord.gg/some-invite',
'cogs': ( 'cogs': (
'cogs.emote', 'cogs.emote',
'cogs.meta',
'ben_cogs.debug', 'ben_cogs.debug',
'ben_cogs.misc', 'ben_cogs.misc',
'ben_cogs.debug' 'ben_cogs.debug'
@ -16,5 +24,18 @@
}, },
}, },
'user_agent': 'EmojiManagerBot (https://github.com/bmintz/emoji-manager-bot)', 'user_agent': 'EmoteManagerBot (https://github.com/bmintz/emote-manager-bot)',
# emotes that the bot may use to respond to you
# If not provided, the bot will use ('❌', '✅') instead.
#
# You can obtain these ones from the discordbots.org server under the name "tickNo" and "tickYes"
# but I uploaded them to my test server
# so that both the staging and the stable versions of the bot can use them
'response_emotes': {
'success': { # emotes used to indicate success or failure
False: '<:error:478164511879069707>',
True: '<:success:478164452261363712>'
},
},
} }

View file

@ -1,3 +1,4 @@
aioec
git+https://github.com/Rapptz/discord.py@rewrite git+https://github.com/Rapptz/discord.py@rewrite
jishaku jishaku
ben_cogs ben_cogs

View file

@ -11,7 +11,7 @@ class MissingManageEmojisPermission(commands.MissingPermissions):
def __init__(self): def __init__(self):
super(Exception, self).__init__( super(Exception, self).__init__(
f'{utils.SUCCESS_EMOTES[False]} ' f'{utils.SUCCESS_EMOJIS[False]} '
"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.')

View file

@ -5,11 +5,6 @@ import discord
"""various utilities for use within the bot""" """various utilities for use within the bot"""
"""Emotes used to indicate success/failure. You can obtain these from the discordbots.org guild,
but I uploaded them to my test server
so that both the staging and the stable versions of the bot can use them"""
SUCCESS_EMOTES = ('<:error:416845770239508512>', '<:success:416845760810844160>')
def format_user(bot, id, *, mention=False): def format_user(bot, id, *, mention=False):
"""Format a user ID for human readable display.""" """Format a user ID for human readable display."""
user = bot.get_user(id) user = bot.get_user(id)

View file

@ -11,15 +11,12 @@ from discord.ext.commands import Context
class Paginator: class Paginator:
def __init__(self, ctx: Context, pages: typing.Iterable, *, timeout=300, delete_message=False, predicate=None, 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):
if predicate is None:
def predicate(_, user):
return user == ctx.message.author
self.pages = list(pages) self.pages = list(pages)
self.predicate = predicate
self.timeout = timeout self.timeout = timeout
self.author = ctx.author
self.target = ctx.channel self.target = ctx.channel
self.delete_msg = delete_message self.delete_msg = delete_message
self.delete_msg_timeout = delete_message_on_timeout self.delete_msg_timeout = delete_message_on_timeout
@ -41,6 +38,15 @@ class Paginator:
self._page = None self._page = None
def react_check(self, reaction, user):
if user is None or user != self.author:
return False
if reaction.message.id != self._message.id:
return False
return bool(discord.utils.find(lambda emoji: reaction.emoji == emoji, self.navigation))
async def begin(self): async def begin(self):
"""Starts pagination""" """Starts pagination"""
self._stopped = False self._stopped = False
@ -50,7 +56,10 @@ class Paginator:
await self._message.add_reaction(button) await self._message.add_reaction(button)
while not self._stopped: while not self._stopped:
try: try:
reaction, user = await self._client.wait_for('reaction_add', check=self.predicate, timeout=self.timeout) reaction, user = await self._client.wait_for(
'reaction_add',
check=self.react_check,
timeout=self.timeout)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await self.stop(delete=self.delete_msg_timeout) await self.stop(delete=self.delete_msg_timeout)
continue continue