diff --git a/src/cogs/booru.py b/src/cogs/booru.py index a22e9c2..5461a7b 100644 --- a/src/cogs/booru.py +++ b/src/cogs/booru.py @@ -1,1661 +1,1661 @@ -import asyncio -import json -import re -import sys -import traceback as tb -from contextlib import suppress -from datetime import datetime as dt -from datetime import timedelta as td -from fractions import gcd -import copy - -import discord as d -from discord import errors as err -from discord import reaction -from discord.ext import commands as cmds -from discord.ext.commands import errors as errext - -from cogs import tools -from misc import exceptions as exc -from misc import checks -from utils import utils as u -from utils import formatter, scraper - - -class MsG: - - def __init__(self, bot): - self.bot = bot - self.LIMIT = 100 - self.HISTORY_LIMIT = 150 - self.RATE_LIMIT = u.RATE_LIMIT - self.reversiqueue = asyncio.Queue() - self.heartqueue = asyncio.Queue() - self.reversifying = False - self.updating = False - self.hearting = False - - time = (dt.utcnow() - td(days=29)).strftime('%d/%m/%Y/%H:%M:%S') - self.suggested = u.setdefault('cogs/suggested.pkl', 7) - # self.suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'test', 'tags': {}, 'total': 1}) - print(self.suggested) - self.favorites = u.setdefault('cogs/favorites.pkl', {}) - self.blacklists = u.setdefault( - 'cogs/blacklists.pkl', {'global_blacklist': set(), 'guild_blacklist': {}, 'user_blacklist': {}}) - self.aliases = u.setdefault('cogs/aliases.pkl', {}) - - if not self.hearting: - self.hearting = True - self.bot.loop.create_task(self._send_hearts()) - print('STARTED : hearting') - if u.tasks['auto_rev']: - for channel in u.tasks['auto_rev']: - temp = self.bot.get_channel(channel) - self.bot.loop.create_task(self.queue_for_reversification(temp)) - print('STARTED : auto-reversifying in #{}'.format(temp.name)) - self.reversifying = True - self.bot.loop.create_task(self._reversify()) - if u.tasks['auto_hrt']: - for channel in u.tasks['auto_hrt']: - temp = self.bot.get_channel(channel) - self.bot.loop.create_task(self.queue_for_hearts(channel=temp)) - print(f'STARTED : auto-hearting in #{temp.name}') - # if not self.updating: - # self.updating = True - # self.bot.loop.create_task(self._update_suggested()) - - async def _update_suggested(self): - while self.updating: - print('Checking for tag updates...') - print(self.suggested) - - time = dt.utcnow() - last_update = dt.strptime(self.suggested['last_update'], '%d/%m/%Y/%H:%M:%S') - delta = time - last_update - print(delta.days) - - if delta.days < 30: - print('Up to date.') - else: - page = 1 - pages = len(list(self.suggested['tags'].keys())) - - print(f'Last updated: {self.suggested["last_update"]}') - print('Updating tags...') - - content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) - while content: - for tag in content: - self.suggested['tags'][tag['name']] = tag['count'] - self.suggested['total'] += tag['count'] - print(f' UPDATED : PAGE {page} / {pages}', end='\r') - - page += 1 - content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) - - u.dump(self.suggested, 'cogs/suggested.pkl') - self.suggested['last_update'] = time.strftime('%d/%m/%Y/%H:%M:%S') - - print('\nFinished updating tags.') - - await asyncio.sleep(24 * 60 * 60) - - def _get_favorites(self, ctx, args): - if '-f' in args or '-favs' in args or '-faves' in args or '-favorites' in args: - if self.favorites.get(ctx.author.id, {}).get('tags', set()): - args = ['~{}'.format(tag) - for tag in self.favorites[ctx.author.id]['tags']] - else: - raise exc.FavoritesNotFound - - return args - - def _get_score(self, score): - if score < 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/pouting-face_1f621.png' - elif score == 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/mozilla/36/pile-of-poo_1f4a9.png' - elif 10 > score > 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/white-medium-star_2b50.png' - elif 50 > score >= 10: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/glowing-star_1f31f.png' - elif 100 > score >= 50: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/dizzy-symbol_1f4ab.png' - elif score >= 100: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png' - return None - - async def _send_hearts(self): - while self.hearting: - temp = await self.heartqueue.get() - - if isinstance(temp[1], d.Embed): - await temp[0].send(embed=temp[1]) - - await asyncio.sleep(self.RATE_LIMIT) - elif isinstance(temp[1], d.Message): - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', temp[1].content): - await temp[0].send(match) - - await asyncio.sleep(self.RATE_LIMIT) - - for attachment in temp[1].attachments: - await temp[0].send(attachment.url) - await asyncio.sleep(self.RATE_LIMIT) - - print('STOPPED : hearting') - - async def queue_for_hearts(self, *, message=None, send=None, channel=None, reaction=True, timeout=60 * 60): - def on_reaction(reaction, user): - if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id and not user.bot: - raise exc.Save(user) - return False - def on_reaction_channel(reaction, user): - if reaction.message.channel.id == channel.id and not user.bot: - if reaction.emoji == '\N{OCTAGONAL SIGN}' and user.permissions_in(reaction.message.channel).administrator: - raise exc.Abort - if reaction.emoji == '\N{HEAVY BLACK HEART}' and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', reaction.message.content) or reaction.message.attachments): - raise exc.Save(user, reaction.message) - return False - - if message: - try: - if reaction: - await message.add_reaction('\N{HEAVY BLACK HEART}') - await asyncio.sleep(1) - - while self.hearting: - try: - await self.bot.wait_for('reaction_add', check=on_reaction, timeout=timeout) - - except exc.Save as e: - await self.heartqueue.put((e.user, send if send else message)) - - except asyncio.TimeoutError: - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - else: - try: - while self.hearting: - try: - await self.bot.wait_for('reaction_add', check=on_reaction_channel) - - except exc.Save as e: - await self.heartqueue.put((e.user, message)) - - except exc.Abort: - u.tasks['auto_hrt'].remove(channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - print('STOPPED : auto-hearting in #{}'.format(channel.name)) - await channel.send('**Stopped queueing messages for hearting in** {}'.format(channel.mention), delete_after=5) - - @cmds.command(name='autoheart', aliases=['autohrt']) - @cmds.has_permissions(administrator=True) - async def auto_heart(self, ctx): - try: - if ctx.channel.id not in u.tasks['auto_hrt']: - u.tasks['auto_hrt'].append(ctx.channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - self.bot.loop.create_task(self.queue_for_hearts(channel=ctx.channel)) - print('STARTED : auto-hearting in #{}'.format(ctx.channel.name)) - await ctx.send('**Auto-hearting all messages in {}**'.format(ctx.channel.mention), delete_after=5) - else: - raise exc.Exists - - except exc.Exists: - message = await ctx.send('**Already auto-hearting in {}.** React with \N{OCTAGONAL SIGN} to stop.'.format(ctx.channel.mention)) - await message.add_reaction('\N{OCTAGONAL SIGN}') - - # @cmds.command() - # async def auto_post(self, ctx): - # try: - # if ctx.channel.id not in u.tasks['auto_post']: - # u.tasks['auto_post'].append(ctx.channel.id) - # u.dump(u.tasks, 'cogs/tasks.pkl') - # self.bot.loop.create_task(self.queue_for_posting(ctx.channel)) - # if not self.posting: - # self.bot.loop.create_task(self._post()) - # self.posting = True - # - # print('STARTED : auto-posting in #{}'.format(ctx.channel.name)) - # await ctx.send('**Auto-posting all images in {}**'.format(ctx.channel.mention), delete_after=5) - # else: - # raise exc.Exists - # - # except exc.Exists: - # await ctx.send('**Already auto-posting in {}.** Type `stop` to stop.'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['tag', 't'], brief='(G) Get info on tags', description='Group command for obtaining info on tags\n\nUsage:\n\{p\}tag \{flag\} \{tag(s)\}') - async def tags(self, ctx): - pass - - # Tag search - @tags.command(name='related', aliases=['relate', 'rel', 'r'], brief='(tags) Search for related tags', description='Return related tags for given tag(s)\n\nExample:\n\{p\}tag related wolf') - async def _tags_related(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - related = [] - c = 0 - - await dest.trigger_typing() - - for tag in tags: - try: - tag_request = await u.fetch('https://e621.net/tag/related.json', params={'tags': tag}, json=True) - for rel in tag_request.get(tag, []): - related.append(rel[0]) - - if related: - await dest.send('`{}` **related tags:**\n```\n{}```'.format(tag, formatter.tostring(related))) - else: - await ctx.send(f'**No related tags found for:** `{tag}`', delete_after=7) - - related.clear() - c += 1 - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - # Tag aliases - @tags.command(name='aliases', aliases=['alias', 'als', 'a'], brief='(tags) Search for tag aliases', description='Return aliases for given tag(s)\n\nExample:\n\{p\}tag alias wolf') - async def _tags_aliases(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - aliases = [] - c = 0 - - await dest.trigger_typing() - - for tag in tags: - try: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - for dic in alias_request: - aliases.append(dic['name']) - - if aliases: - await dest.send('`{}` **aliases:**\n```\n{}```'.format(tag, formatter.tostring(aliases))) - else: - await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) - - aliases.clear() - c += 1 - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['g'], brief='(G) Get e621 elements', description='Group command for obtaining various elements like post info\n\nUsage:\n\{p\}get \{flag\} \{args\}') - async def get(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to get items.**\n*Type* `{}help get` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='info', aliases=['i'], brief='(get) Get info from post', description='Return info for given post URL or ID\n\nExample:\n\{p\}get info 1145042') - async def _get_info(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, posts = kwargs['destination'], kwargs['remaining'] - - if not posts: - raise exc.MissingArgument - - for ident in posts: - try: - await dest.trigger_typing() - - ident = ident if not ident.isdigit() else re.search( - 'show/([0-9]+)', ident).group(1) - post = await u.fetch('https://e621.net/post/show.json', params={'id': ident}, json=True) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_thumbnail(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - # except - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - except exc.MissingArgument: - await ctx.send('**Invalid url**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='image', aliases=['img'], brief='(get) Get direct image from post', description='Return direct image URL for given post\n\nExample:\n\{p\}get image 1145042') - async def _get_image(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, urls = kwargs['destination'], kwargs['remaining'] - c = 0 - - if not urls: - raise exc.MissingArgument - - for url in urls: - try: - await dest.trigger_typing() - - await dest.send(await scraper.get_image(url)) - - c += 1 - - # except - # await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.MissingArgument: - await ctx.send('**Invalid url or file**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='pool', aliases=['p'], brief='(get) Get pool from query', description='Return pool info for given query\n\nExample:\n\{p\}get pool 1145042') - async def _get_pool(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: - raise exc.Abort(match) - return False - - def on_message(msg): - return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, query = kwargs['destination'], kwargs['remaining'] - ident = None - - await dest.trigger_typing() - - pools = [] - pool_request = await u.fetch('https://e621.net/pool/index.json', params={'query': ' '.join(query)}, json=True) - if len(pool_request) > 1: - for pool in pool_request: - pools.append(pool['name']) - match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) - for future in done: - selection = future.result() - - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - await selection.delete() - elif pool_request: - tempool = pool_request[0] - else: - raise exc.NotFound - - await ctx.send(f'**{tempool["name"]}**\nhttps://e621.net/pool/show/{tempool["id"]}') - - except exc.Abort as e: - await e.message.edit(content='\N{NO ENTRY SIGN}', delete_after=7) - - # Reverse image searches a linked image using the public iqdb - @cmds.command(name='reverse', aliases=['rev', 'ris'], brief='Reverse image search from e621', description='NSFW\nReverse-search an image with given URL') - async def reverse(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, urls = kwargs['destination'], kwargs['remaining'] - c = 0 - - if not urls and not ctx.message.attachments: - raise exc.MissingArgument - - for attachment in ctx.message.attachments: - urls.append(attachment.url) - - for url in urls: - try: - await dest.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - await dest.send('**Probable match**', embed=embed) - - c += 1 - - except exc.MatchError as e: - await ctx.send('**No probable match for:** `{}`'.format(e), delete_after=7) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.MissingArgument: - await ctx.send('**Invalid url or file.** Be sure the link directs to an image file', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.SizeError as e: - await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.command(name='reversify', aliases=['revify', 'risify', 'rify']) - async def reversify(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=self.HISTORY_LIMIT / 5) - dest, remove, limit = kwargs['destination'], kwargs['remove'], kwargs['limit'] - links = {} - c = 0 - - if not ctx.author.permissions_in(ctx.channel).manage_messages: - dest = ctx.author - - async for message in ctx.channel.history(limit=self.HISTORY_LIMIT * limit): - if c >= limit: - break - if message.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content) is not None or message.embeds or message.attachments): - links[message] = [] - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): - links[message].append(match.group(0)) - for embed in message.embeds: - if embed.image.url is not d.Embed.Empty: - links[message].append(embed.image.url) - for attachment in message.attachments: - links[message].append(attachment.url) - - await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - c += 1 - - if not links: - raise exc.NotFound - - n = 1 - for message, urls in links.items(): - for url in urls: - try: - await dest.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - await dest.send(f'**Probable match from** {message.author.display_name}', embed=embed) - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - if remove: - with suppress(err.NotFound): - await message.delete() - - except exc.MatchError as e: - await ctx.send('`{} / {}` **No probable match for:** `{}`'.format(n, len(links), e), delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - except exc.SizeError as e: - await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - - finally: - n += 1 - - if c <= 0: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.NotFound: - await ctx.send('**No matches found**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **invalid limit.** Query limited to 30'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - async def _reversify(self): - while self.reversifying: - message = await self.reversiqueue.get() - urls = [] - - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): - urls.append(match.group(0)) - for embed in message.embeds: - if embed.image.url is not d.Embed.Empty: - urls.append(embed.image.url) - for attachment in message.attachments: - urls.append(attachment.url) - - for url in urls: - try: - await message.channel.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=message.channel.guild.me.color if isinstance(message.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=message.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - await message.channel.send('**Probable match from** {}'.format(message.author.display_name), embed=embed) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - await asyncio.sleep(self.RATE_LIMIT) - - with suppress(err.NotFound): - await message.delete() - - except exc.MatchError as e: - await message.channel.send('**No probable match for:** `{}`'.format(e), delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - except exc.SizeError as e: - await message.channel.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - except Exception: - await message.channel.send(f'**An unknown error occurred.**', delete_after=7) - await message.add_reaction('\N{WARNING SIGN}') - - print('STOPPED : reversifying') - - async def queue_for_reversification(self, channel): - def check(msg): - if 'stop r' in msg.content.lower() and msg.channel is channel and msg.author.guild_permissions.administrator: - raise exc.Abort - elif msg.channel is channel and msg.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', msg.content) is not None or msg.attachments or msg.embeds): - return True - return False - - try: - while self.reversifying: - message = await self.bot.wait_for('message', check=check) - await self.reversiqueue.put(message) - await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - except exc.Abort: - u.tasks['auto_rev'].remove(channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - if not u.tasks['auto_rev']: - self.reversifying = False - print('STOPPED : reversifying #{}'.format(channel.name)) - await channel.send('**Stopped queueing messages for reversification in** {}'.format(channel.mention), delete_after=5) - - @cmds.command(name='autoreversify', aliases=['autorev']) - @cmds.has_permissions(manage_channels=True) - async def auto_reversify(self, ctx): - if ctx.channel.id not in u.tasks['auto_rev']: - u.tasks['auto_rev'].append(ctx.channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - self.bot.loop.create_task( - self.queue_for_reversification(ctx.channel)) - if not self.reversifying: - self.bot.loop.create_task(self._reversify()) - self.reversifying = True - - print('STARTED : auto-reversifying in #{}'.format(ctx.channel.name)) - await ctx.send('**Auto-reversifying all images in** {}'.format(ctx.channel.mention), delete_after=5) - else: - await ctx.send('**Already auto-reversifying in {}.** Type `stop r(eversifying)` to stop.'.format(ctx.channel.mention), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - async def _get_pool(self, ctx, *, destination, booru='e621', query=[]): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: - raise exc.Abort(match) - return False - - def on_message(msg): - return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel - - posts = {} - pool = {} - - try: - pools = [] - pool_request = await u.fetch('https://{}.net/pool/index.json'.format(booru), params={'query': ' '.join(query)}, json=True) - if len(pool_request) > 1: - for pool in pool_request: - pools.append(pool['name']) - match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match.\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) - for future in done: - selection = future.result() - - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - await selection.delete() - pool = {'name': tempool['name'], 'id': tempool['id']} - - await destination.trigger_typing() - elif pool_request: - tempool = pool_request[0] - pool = {'name': pool_request[0] - ['name'], 'id': pool_request[0]['id']} - else: - raise exc.NotFound - - page = 1 - while len(posts) < tempool['post_count']: - posts_request = await u.fetch('https://{}.net/pool/show.json'.format(booru), params={'id': tempool['id'], 'page': page}, json=True) - for post in posts_request['posts']: - posts[post['id']] = {'artist': ', '.join( - post['artist']), 'file_url': post['file_url'], 'score': post['score']} - page += 1 - - return pool, posts - - except exc.Abort as e: - await e.message.edit(content='\N{NO ENTRY SIGN}') - raise exc.Continue - - # Messy code that checks image limit and tags in blacklists - async def _get_posts(self, ctx, *, booru='e621', tags=[], limit=1, previous={}): - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - blacklist = set() - # Creates temp blacklist based on context - for bl in (self.blacklists['global_blacklist'], self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set()), self.blacklists['user_blacklist'].get(ctx.author.id, set())): - for tag in bl: - blacklist.update([tag] + list(self.aliases[tag])) - # Checks for, assigns, and removes first order in tags if possible - order = [tag for tag in tags if 'order:' in tag] - if order: - order = order[0] - tags.remove(order) - else: - order = 'order:random' - # Checks if tags are in local blacklists - if tags: - if (len(tags) > 5 and booru == 'e621') or (len(tags) > 4 and booru == 'e926'): - raise exc.TagBoundsError(formatter.tostring(tags[5:])) - for tag in tags: - if tag == 'swf' or tag == 'webm' or tag in blacklist: - raise exc.TagBlacklisted(tag) - - # Checks for blacklisted tags in endpoint blacklists - try/except is for continuing the parent loop - posts = {} - temposts = len(posts) - empty = 0 - c = 0 - while len(posts) < limit: - if c == limit * 5 + (self.LIMIT / 5): - raise exc.Timeout - request = await u.fetch('https://{}.net/post/index.json'.format(booru), params={'tags': ','.join([order] + tags), 'limit': int(self.LIMIT * limit)}, json=True) - if len(request) == 0: - raise exc.NotFound(formatter.tostring(tags)) - if len(request) < limit: - limit = len(request) - - for post in request: - if 'swf' in post['file_ext'] or 'webm' in post['file_ext']: - continue - try: - for tag in blacklist: - if tag in post['tags']: - raise exc.Continue - except exc.Continue: - continue - if post['id'] not in posts.keys() and post['id'] not in previous.keys(): - posts[post['id']] = {'artist': ', '.join( - post['artist']), 'file_url': post['file_url'], 'score': post['score']} - if len(posts) == limit: - break - - if len(posts) == temposts: - empty += 1 - if empty == 5: - break - else: - empty = 0 - temposts = len(posts) - c += 1 - - if posts: - return posts, order - else: - raise exc.NotFound(formatter.tostring(tags)) - - # Creates reaction-based paginator for linked pools - @cmds.command(name='poolpage', aliases=['poolp', 'pp', 'e621pp', 'e6pp', '6pp'], brief='e621 pool paginator', description='e621 | NSFW\nShow pools in a page format') - async def pool_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, query = kwargs['destination'], kwargs['remaining'] - hearted = {} - c = 1 - - await dest.trigger_typing() - - pool, posts = await self._get_pool(ctx, destination=dest, booru='e621', query=query) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=dest.me.color if isinstance(dest.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=pool['name'], - url='https://e621.net/pool/show?id={}'.format(pool['id']), icon_url=ctx.author.avatar_url) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted: - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content='\N{INPUT SYMBOL FOR NUMBERS}') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{WHITE HEAVY CHECK MARK}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound: - await ctx.send('**Pool not found**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Continue: - pass - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - @cmds.command(name='e621page', aliases=['e621p', 'e6p', '6p']) - @checks.is_nsfw() - async def e621_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - limit = self.LIMIT / 5 - hearted = {} - c = 1 - - tags = self._get_favorites(ctx, tags) - - await ctx.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted.keys(): - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content=f'`{c} / {len(posts)}`') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - try: - if c % limit == 0: - await dest.trigger_typing() - temposts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit, previous=posts) - posts.update(temposts) - - keys = list(posts.keys()) - values = list(posts.values()) - - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.NotFound: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - # @e621_paginator.error - # async def e621_paginator_error(self, ctx, error): - # if isinstance(error, exc.NSFW): - # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{NO ENTRY}') - - @cmds.command(name='e926page', aliases=['e926p', 'e9p', '9p']) - async def e926_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - limit = self.LIMIT / 5 - hearted = {} - c = 1 - - tags = self._get_favorites(ctx, tags) - - await ctx.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e926.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e926.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted: - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content=f'`{c} / {len(posts)}`') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - try: - if c % limit == 0: - await dest.trigger_typing() - temposts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit, previous=posts) - posts.update(temposts) - - keys = list(posts.keys()) - values = list(posts.values()) - - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.NotFound: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{WHITE HEAVY CHECK MARK}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - # Searches for and returns images from e621.net given tags when not blacklisted - @cmds.command(aliases=['e6', '6'], brief='e621 | NSFW', description='e621 | NSFW\nTag-based search for e621.net\n\nYou can only search 5 tags and 6 images at once for now.\ne6 [tags...] ([# of images])') - @checks.is_nsfw() - async def e621(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=3) - dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] - - tags = self._get_favorites(ctx, args) - - await dest.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) - - for ident, post in posts.items(): - embed = d.Embed(title=post['artist'], url='https://e621.net/post/show/{}'.format(ident), - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - message = await dest.send(embed=embed) - - self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) - - except exc.TagBlacklisted as e: - await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - # @e621.error - # async def e621_error(self, ctx, error): - # if isinstance(error, exc.NSFW): - # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{NO ENTRY}') - - # Searches for and returns images from e926.net given tags when not blacklisted - @cmds.command(aliases=['e9', '9'], brief='e926 | SFW', description='e926 | SFW\nTag-based search for e926.net\n\nYou can only search 5 tags and 6 images at once for now.\ne9 [tags...] ([# of images])') - async def e926(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=3) - dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] - - tags = self._get_favorites(ctx, args) - - await dest.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) - - for ident, post in posts.items(): - embed = d.Embed(title=post['artist'], url='https://e926.net/post/show/{}'.format(ident), - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - message = await dest.send(embed=embed) - - self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) - - except exc.TagBlacklisted as e: - await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['fave', 'fav', 'f']) - async def favorite(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to manage favorites.**\n*Type* `{}help fav` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @favorite.error - async def favorite_error(self, ctx, error): - pass - - @favorite.group(name='get', aliases=['g']) - async def _get_favorite(self, ctx): - pass - - @_get_favorite.command(name='tags', aliases=['t']) - async def __get_favorite_tags(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{WHITE MEDIUM STAR} {}**\'s favorite tags:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.favorites.get(ctx.author.id, {}).get('tags', set()))), delete_after=7) - - @_get_favorite.command(name='posts', aliases=['p']) - async def __get_favorite_posts(self, ctx): - pass - - @favorite.group(name='add', aliases=['a']) - async def _add_favorite(self, ctx): - pass - - @_add_favorite.command(name='tags', aliases=['t']) - async def __add_favorite_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - if tag in self.blacklists['user_blacklist'].get(ctx.author.id, set()): - raise exc.TagBlacklisted(tag) - with suppress(KeyError): - if len(self.favorites[ctx.author.id]['tags']) + len(tags) > 5: - raise exc.BoundsError - - self.favorites.setdefault(ctx.author.id, {}).setdefault( - 'tags', set()).update(tags) - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{} **added to their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except exc.BoundsError: - await ctx.send('**Favorites list currently limited to:** `5`', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**', delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - - @_add_favorite.command(name='posts', aliases=['p']) - async def __add_favorite_posts(self, ctx, *posts): - pass - - @favorite.group(name='remove', aliases=['r']) - async def _remove_favorite(self, ctx): - pass - - @_remove_favorite.command(name='tags', aliases=['t']) - async def __remove_favorite_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.favorites[ctx.author.id].get( - 'tags', set()).remove(tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{} **removed from their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except KeyError: - await ctx.send('**You do not have any favorites**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagError as e: - await ctx.send('`{}` **not in favorites**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_favorite.command(name='posts', aliases=['p']) - async def __remove_favorite_posts(self, ctx): - pass - - @favorite.group(name='clear', aliases=['c']) - async def _clear_favorite(self, ctx): - pass - - @_clear_favorite.command(name='tags', aliases=['t']) - async def __clear_favorite_tags(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - with suppress(KeyError): - del self.favorites[ctx.author.id] - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{}**\'s favorites cleared**'.format(ctx.author.mention), delete_after=5) - - @_clear_favorite.command(name='posts', aliases=['p']) - async def __clear_favorite_posts(self, ctx): - pass - - # Umbrella command structure to manage global, channel, and user blacklists - @cmds.group(aliases=['bl', 'b'], brief='(G) Manage blacklists', description='Manage channel or personal blacklists\n\nUsage:\n\{p\}bl get \{blacklist\} to show a blacklist\n\{p\}bl clear \{blacklist\} to clear a blacklist\n\{p\}bl add \{blacklist\} \{tags...\} to add tag(s) to a blacklist\n\{p\}bl remove \{blacklist\} \{tags...\} to remove tags from a blacklist') - async def blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to manage blacklists.**\n*Type* `{}help bl` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - # @blacklist.error - # async def blacklist_error(self, ctx, error): - # if isinstance(error, KeyError): - # return await ctx.send('**Blacklist does not exist**', delete_after=7) - - @blacklist.group(name='get', aliases=['g'], brief='(G) Get a blacklist\n\nUsage:\n\{p\}bl get \{blacklist\}') - async def _get_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_get_blacklist.command(name='global', aliases=['gl', 'g'], brief='Get current global blacklist', description='Get current global blacklist\n\nThis applies to all booru commands, in accordance with Discord\'s ToS agreement\n\nExample:\n\{p\}bl get global') - async def __get_global_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} **Global blacklist:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']))) - - @_get_blacklist.command(name='channel', aliases=['ch', 'c'], brief='Get current channel blacklist', description='Get current channel blacklist\n\nThis is based on context - the channel where the command was executed\n\nExample:\{p\}bl get channel') - async def __get_channel_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) - - @_get_blacklist.command(name='me', aliases=['m'], brief='Get your personal blacklist', description='Get your personal blacklist\n\nYour blacklist is not viewable by anyone but you, except if you call this command in a public channel. The blacklist will be deleted soon after for your privacy\n\nExample:\n\{p\}bl get me') - async def __get_user_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} {}**\'s blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.blacklists['user_blacklist'].get(ctx.author.id, set()))), delete_after=7) - - @_get_blacklist.command(name='here', aliases=['h'], brief='Get current global and channel blacklists', description='Get current global and channel blacklists in a single message\n\nExample:\{p\}bl get here') - async def __get_here_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} **__Blacklisted:__**\n\n**Global:**\n```\n{}```\n**{}:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']), ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) - - @_get_blacklist.group(name='all', aliases=['a'], hidden=True) - async def __get_all_blacklists(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - @__get_all_blacklists.command(name='guild', aliases=['g']) - @cmds.has_permissions(manage_channels=True) - async def ___get_all_guild_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} **__{} blacklists:__**\n\n{}'.format(guild.name, formatter.dict_tostring(self.blacklists['guild_blacklist'].get(guild.id, {})))) - - @__get_all_blacklists.command(name='user', aliases=['u', 'member', 'm']) - @cmds.is_owner() - async def ___get_all_user_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} **__User blacklists:__**\n\n{}'.format(formatter.dict_tostring(self.blacklists['user_blacklist']))) - - @blacklist.group(name='add', aliases=['a'], brief='(G) Add tag(s) to a blacklist\n\nUsage:\n\{p\}bl add \{blacklist\} \{tags...\}') - async def _add_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_add_tags.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __add_global_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - await dest.trigger_typing() - - self.blacklists['global_blacklist'].update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('**Added to global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) - - @_add_tags.command(name='channel', aliases=['ch', 'c'], brief='@manage_channel@ Add tag(s) to the current channel blacklist (requires manage_channel)', description='Add tag(s) to the current channel blacklist ') - @cmds.has_permissions(manage_channels=True) - async def __add_channel_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.trigger_typing() - - self.blacklists['guild_blacklist'].setdefault( - guild.id, {}).setdefault(ctx.channel.id, set()).update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('**Added to** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags)), delete_after=5) - - @_add_tags.command(name='me', aliases=['m']) - async def __add_user_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - await dest.trigger_typing() - - self.blacklists['user_blacklist'].setdefault( - ctx.author.id, set()).update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('{} **added to their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - @blacklist.group(name='remove', aliases=['rm', 'r']) - async def _remove_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __remove_global_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.blacklists['global_blacklist'].remove(tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Removed from global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='channel', aliases=['ch', 'c']) - @cmds.has_permissions(manage_channels=True) - async def __remove_channel_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - for tag in tags: - try: - self.blacklists['guild_blacklist'][guild.id][ctx.channel.id].remove( - tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Removed from** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags), delete_after=5)) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='me', aliases=['m']) - async def __remove_user_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.blacklists['user_blacklist'][ctx.author.id].remove( - tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{} **removed from their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @blacklist.group(name='clear', aliases=['cl', 'c']) - async def _clear_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_clear_blacklist.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __clear_global_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - self.blacklists['global_blacklist'].clear() - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Global blacklist cleared**', delete_after=5) - - @_clear_blacklist.command(name='channel', aliases=['ch', 'c']) - @cmds.has_permissions(manage_channels=True) - async def __clear_channel_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - with suppress(KeyError): - del self.blacklists['guild_blacklist'][guild.id][ctx.channel.id] - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{} **blacklist cleared**'.format(ctx.channel.mention), delete_after=5) - - @_clear_blacklist.command(name='me', aliases=['m']) - async def __clear_user_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - with suppress(KeyError): - del self.blacklists['user_blacklist'][ctx.author.id] - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{}**\'s blacklist cleared**'.format(ctx.author.mention), delete_after=5) +import asyncio +import json +import re +import sys +import traceback as tb +from contextlib import suppress +from datetime import datetime as dt +from datetime import timedelta as td +from fractions import gcd +import copy + +import discord as d +from discord import errors as err +from discord import reaction +from discord.ext import commands as cmds +from discord.ext.commands import errors as errext + +from cogs import tools +from misc import exceptions as exc +from misc import checks +from utils import utils as u +from utils import formatter, scraper + + +class MsG: + + def __init__(self, bot): + self.bot = bot + self.LIMIT = 100 + self.HISTORY_LIMIT = 150 + self.RATE_LIMIT = u.RATE_LIMIT + self.reversiqueue = asyncio.Queue() + self.heartqueue = asyncio.Queue() + self.reversifying = False + self.updating = False + self.hearting = False + + time = (dt.utcnow() - td(days=29)).strftime('%d/%m/%Y/%H:%M:%S') + self.suggested = u.setdefault('cogs/suggested.pkl', 7) + # self.suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'test', 'tags': {}, 'total': 1}) + print(self.suggested) + self.favorites = u.setdefault('cogs/favorites.pkl', {}) + self.blacklists = u.setdefault( + 'cogs/blacklists.pkl', {'global_blacklist': set(), 'guild_blacklist': {}, 'user_blacklist': {}}) + self.aliases = u.setdefault('cogs/aliases.pkl', {}) + + if not self.hearting: + self.hearting = True + self.bot.loop.create_task(self._send_hearts()) + print('STARTED : hearting') + if u.tasks['auto_rev']: + for channel in u.tasks['auto_rev']: + temp = self.bot.get_channel(channel) + self.bot.loop.create_task(self.queue_for_reversification(temp)) + print('STARTED : auto-reversifying in #{}'.format(temp.name)) + self.reversifying = True + self.bot.loop.create_task(self._reversify()) + if u.tasks['auto_hrt']: + for channel in u.tasks['auto_hrt']: + temp = self.bot.get_channel(channel) + self.bot.loop.create_task(self.queue_for_hearts(channel=temp)) + print(f'STARTED : auto-hearting in #{temp.name}') + # if not self.updating: + # self.updating = True + # self.bot.loop.create_task(self._update_suggested()) + + async def _update_suggested(self): + while self.updating: + print('Checking for tag updates...') + print(self.suggested) + + time = dt.utcnow() + last_update = dt.strptime(self.suggested['last_update'], '%d/%m/%Y/%H:%M:%S') + delta = time - last_update + print(delta.days) + + if delta.days < 30: + print('Up to date.') + else: + page = 1 + pages = len(list(self.suggested['tags'].keys())) + + print(f'Last updated: {self.suggested["last_update"]}') + print('Updating tags...') + + content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) + while content: + for tag in content: + self.suggested['tags'][tag['name']] = tag['count'] + self.suggested['total'] += tag['count'] + print(f' UPDATED : PAGE {page} / {pages}', end='\r') + + page += 1 + content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) + + u.dump(self.suggested, 'cogs/suggested.pkl') + self.suggested['last_update'] = time.strftime('%d/%m/%Y/%H:%M:%S') + + print('\nFinished updating tags.') + + await asyncio.sleep(24 * 60 * 60) + + def _get_favorites(self, ctx, args): + if '-f' in args or '-favs' in args or '-faves' in args or '-favorites' in args: + if self.favorites.get(ctx.author.id, {}).get('tags', set()): + args = ['~{}'.format(tag) + for tag in self.favorites[ctx.author.id]['tags']] + else: + raise exc.FavoritesNotFound + + return args + + def _get_score(self, score): + if score < 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/pouting-face_1f621.png' + elif score == 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/mozilla/36/pile-of-poo_1f4a9.png' + elif 10 > score > 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/white-medium-star_2b50.png' + elif 50 > score >= 10: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/glowing-star_1f31f.png' + elif 100 > score >= 50: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/dizzy-symbol_1f4ab.png' + elif score >= 100: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png' + return None + + async def _send_hearts(self): + while self.hearting: + temp = await self.heartqueue.get() + + if isinstance(temp[1], d.Embed): + await temp[0].send(embed=temp[1]) + + await asyncio.sleep(self.RATE_LIMIT) + elif isinstance(temp[1], d.Message): + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', temp[1].content): + await temp[0].send(match) + + await asyncio.sleep(self.RATE_LIMIT) + + for attachment in temp[1].attachments: + await temp[0].send(attachment.url) + await asyncio.sleep(self.RATE_LIMIT) + + print('STOPPED : hearting') + + async def queue_for_hearts(self, *, message=None, send=None, channel=None, reaction=True, timeout=60 * 60): + def on_reaction(reaction, user): + if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id and not user.bot: + raise exc.Save(user) + return False + def on_reaction_channel(reaction, user): + if reaction.message.channel.id == channel.id and not user.bot: + if reaction.emoji == '\N{OCTAGONAL SIGN}' and user.permissions_in(reaction.message.channel).administrator: + raise exc.Abort + if reaction.emoji == '\N{HEAVY BLACK HEART}' and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', reaction.message.content) or reaction.message.attachments): + raise exc.Save(user, reaction.message) + return False + + if message: + try: + if reaction: + await message.add_reaction('\N{HEAVY BLACK HEART}') + await asyncio.sleep(1) + + while self.hearting: + try: + await self.bot.wait_for('reaction_add', check=on_reaction, timeout=timeout) + + except exc.Save as e: + await self.heartqueue.put((e.user, send if send else message)) + + except asyncio.TimeoutError: + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + else: + try: + while self.hearting: + try: + await self.bot.wait_for('reaction_add', check=on_reaction_channel) + + except exc.Save as e: + await self.heartqueue.put((e.user, message)) + + except exc.Abort: + u.tasks['auto_hrt'].remove(channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + print('STOPPED : auto-hearting in #{}'.format(channel.name)) + await channel.send('**Stopped queueing messages for hearting in** {}'.format(channel.mention), delete_after=5) + + @cmds.command(name='autoheart', aliases=['autohrt']) + @cmds.has_permissions(administrator=True) + async def auto_heart(self, ctx): + try: + if ctx.channel.id not in u.tasks['auto_hrt']: + u.tasks['auto_hrt'].append(ctx.channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + self.bot.loop.create_task(self.queue_for_hearts(channel=ctx.channel)) + print('STARTED : auto-hearting in #{}'.format(ctx.channel.name)) + await ctx.send('**Auto-hearting all messages in {}**'.format(ctx.channel.mention), delete_after=5) + else: + raise exc.Exists + + except exc.Exists: + message = await ctx.send('**Already auto-hearting in {}.** React with \N{OCTAGONAL SIGN} to stop.'.format(ctx.channel.mention)) + await message.add_reaction('\N{OCTAGONAL SIGN}') + + # @cmds.command() + # async def auto_post(self, ctx): + # try: + # if ctx.channel.id not in u.tasks['auto_post']: + # u.tasks['auto_post'].append(ctx.channel.id) + # u.dump(u.tasks, 'cogs/tasks.pkl') + # self.bot.loop.create_task(self.queue_for_posting(ctx.channel)) + # if not self.posting: + # self.bot.loop.create_task(self._post()) + # self.posting = True + # + # print('STARTED : auto-posting in #{}'.format(ctx.channel.name)) + # await ctx.send('**Auto-posting all images in {}**'.format(ctx.channel.mention), delete_after=5) + # else: + # raise exc.Exists + # + # except exc.Exists: + # await ctx.send('**Already auto-posting in {}.** Type `stop` to stop.'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['tag', 't'], brief='(G) Get info on tags', description='Group command for obtaining info on tags\n\nUsage:\n\{p\}tag \{flag\} \{tag(s)\}') + async def tags(self, ctx): + pass + + # Tag search + @tags.command(name='related', aliases=['relate', 'rel', 'r'], brief='(tags) Search for related tags', description='Return related tags for given tag(s)\n\nExample:\n\{p\}tag related wolf') + async def _tags_related(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + related = [] + c = 0 + + await dest.trigger_typing() + + for tag in tags: + try: + tag_request = await u.fetch('https://e621.net/tag/related.json', params={'tags': tag}, json=True) + for rel in tag_request.get(tag, []): + related.append(rel[0]) + + if related: + await dest.send('`{}` **related tags:**\n```\n{}```'.format(tag, formatter.tostring(related))) + else: + await ctx.send(f'**No related tags found for:** `{tag}`', delete_after=7) + + related.clear() + c += 1 + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + # Tag aliases + @tags.command(name='aliases', aliases=['alias', 'als', 'a'], brief='(tags) Search for tag aliases', description='Return aliases for given tag(s)\n\nExample:\n\{p\}tag alias wolf') + async def _tags_aliases(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + aliases = [] + c = 0 + + await dest.trigger_typing() + + for tag in tags: + try: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + for dic in alias_request: + aliases.append(dic['name']) + + if aliases: + await dest.send('`{}` **aliases:**\n```\n{}```'.format(tag, formatter.tostring(aliases))) + else: + await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) + + aliases.clear() + c += 1 + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['g'], brief='(G) Get e621 elements', description='Group command for obtaining various elements like post info\n\nUsage:\n\{p\}get \{flag\} \{args\}') + async def get(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to get items.**\n*Type* `{}help get` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='info', aliases=['i'], brief='(get) Get info from post', description='Return info for given post URL or ID\n\nExample:\n\{p\}get info 1145042') + async def _get_info(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, posts = kwargs['destination'], kwargs['remaining'] + + if not posts: + raise exc.MissingArgument + + for ident in posts: + try: + await dest.trigger_typing() + + ident = ident if not ident.isdigit() else re.search( + 'show/([0-9]+)', ident).group(1) + post = await u.fetch('https://e621.net/post/show.json', params={'id': ident}, json=True) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_thumbnail(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + # except + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + except exc.MissingArgument: + await ctx.send('**Invalid url**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='image', aliases=['img'], brief='(get) Get direct image from post', description='Return direct image URL for given post\n\nExample:\n\{p\}get image 1145042') + async def _get_image(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, urls = kwargs['destination'], kwargs['remaining'] + c = 0 + + if not urls: + raise exc.MissingArgument + + for url in urls: + try: + await dest.trigger_typing() + + await dest.send(await scraper.get_image(url)) + + c += 1 + + # except + # await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.MissingArgument: + await ctx.send('**Invalid url or file**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='pool', aliases=['p'], brief='(get) Get pool from query', description='Return pool info for given query\n\nExample:\n\{p\}get pool 1145042') + async def _get_pool(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: + raise exc.Abort(match) + return False + + def on_message(msg): + return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, query = kwargs['destination'], kwargs['remaining'] + ident = None + + await dest.trigger_typing() + + pools = [] + pool_request = await u.fetch('https://e621.net/pool/index.json', params={'query': ' '.join(query)}, json=True) + if len(pool_request) > 1: + for pool in pool_request: + pools.append(pool['name']) + match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) + for future in done: + selection = future.result() + + await match.delete() + tempool = [pool for pool in pool_request if pool['name'] + == pools[int(selection.content) - 1]][0] + await selection.delete() + elif pool_request: + tempool = pool_request[0] + else: + raise exc.NotFound + + await ctx.send(f'**{tempool["name"]}**\nhttps://e621.net/pool/show/{tempool["id"]}') + + except exc.Abort as e: + await e.message.edit(content='\N{NO ENTRY SIGN}', delete_after=7) + + # Reverse image searches a linked image using the public iqdb + @cmds.command(name='reverse', aliases=['rev', 'ris'], brief='Reverse image search from e621', description='NSFW\nReverse-search an image with given URL') + async def reverse(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, urls = kwargs['destination'], kwargs['remaining'] + c = 0 + + if not urls and not ctx.message.attachments: + raise exc.MissingArgument + + for attachment in ctx.message.attachments: + urls.append(attachment.url) + + for url in urls: + try: + await dest.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + await dest.send('**Probable match**', embed=embed) + + c += 1 + + except exc.MatchError as e: + await ctx.send('**No probable match for:** `{}`'.format(e), delete_after=7) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.MissingArgument: + await ctx.send('**Invalid url or file.** Be sure the link directs to an image file', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.SizeError as e: + await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.command(name='reversify', aliases=['revify', 'risify', 'rify']) + async def reversify(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=self.HISTORY_LIMIT / 5) + dest, remove, limit = kwargs['destination'], kwargs['remove'], kwargs['limit'] + links = {} + c = 0 + + if not ctx.author.permissions_in(ctx.channel).manage_messages: + dest = ctx.author + + async for message in ctx.channel.history(limit=self.HISTORY_LIMIT * limit): + if c >= limit: + break + if message.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content) is not None or message.embeds or message.attachments): + links[message] = [] + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): + links[message].append(match.group(0)) + for embed in message.embeds: + if embed.image.url is not d.Embed.Empty: + links[message].append(embed.image.url) + for attachment in message.attachments: + links[message].append(attachment.url) + + await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + c += 1 + + if not links: + raise exc.NotFound + + n = 1 + for message, urls in links.items(): + for url in urls: + try: + await dest.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + await dest.send(f'**Probable match from** {message.author.display_name}', embed=embed) + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + if remove: + with suppress(err.NotFound): + await message.delete() + + except exc.MatchError as e: + await ctx.send('`{} / {}` **No probable match for:** `{}`'.format(n, len(links), e), delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + c -= 1 + except exc.SizeError as e: + await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + c -= 1 + + finally: + n += 1 + + if c <= 0: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.NotFound: + await ctx.send('**No matches found**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **invalid limit.** Query limited to 30'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + async def _reversify(self): + while self.reversifying: + message = await self.reversiqueue.get() + urls = [] + + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): + urls.append(match.group(0)) + for embed in message.embeds: + if embed.image.url is not d.Embed.Empty: + urls.append(embed.image.url) + for attachment in message.attachments: + urls.append(attachment.url) + + for url in urls: + try: + await message.channel.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=message.channel.guild.me.color if isinstance(message.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=message.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + await message.channel.send('**Probable match from** {}'.format(message.author.display_name), embed=embed) + + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + await asyncio.sleep(self.RATE_LIMIT) + + with suppress(err.NotFound): + await message.delete() + + except exc.MatchError as e: + await message.channel.send('**No probable match for:** `{}`'.format(e), delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + except exc.SizeError as e: + await message.channel.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + except Exception: + await message.channel.send(f'**An unknown error occurred.**', delete_after=7) + await message.add_reaction('\N{WARNING SIGN}') + + print('STOPPED : reversifying') + + async def queue_for_reversification(self, channel): + def check(msg): + if 'stop r' in msg.content.lower() and msg.channel is channel and msg.author.guild_permissions.administrator: + raise exc.Abort + elif msg.channel is channel and msg.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', msg.content) is not None or msg.attachments or msg.embeds): + return True + return False + + try: + while self.reversifying: + message = await self.bot.wait_for('message', check=check) + await self.reversiqueue.put(message) + await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + except exc.Abort: + u.tasks['auto_rev'].remove(channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + if not u.tasks['auto_rev']: + self.reversifying = False + print('STOPPED : reversifying #{}'.format(channel.name)) + await channel.send('**Stopped queueing messages for reversification in** {}'.format(channel.mention), delete_after=5) + + @cmds.command(name='autoreversify', aliases=['autorev']) + @cmds.has_permissions(manage_channels=True) + async def auto_reversify(self, ctx): + if ctx.channel.id not in u.tasks['auto_rev']: + u.tasks['auto_rev'].append(ctx.channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + self.bot.loop.create_task( + self.queue_for_reversification(ctx.channel)) + if not self.reversifying: + self.bot.loop.create_task(self._reversify()) + self.reversifying = True + + print('STARTED : auto-reversifying in #{}'.format(ctx.channel.name)) + await ctx.send('**Auto-reversifying all images in** {}'.format(ctx.channel.mention), delete_after=5) + else: + await ctx.send('**Already auto-reversifying in {}.** Type `stop r(eversifying)` to stop.'.format(ctx.channel.mention), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + async def _get_pool(self, ctx, *, destination, booru='e621', query=[]): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: + raise exc.Abort(match) + return False + + def on_message(msg): + return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel + + posts = {} + pool = {} + + try: + pools = [] + pool_request = await u.fetch('https://{}.net/pool/index.json'.format(booru), params={'query': ' '.join(query)}, json=True) + if len(pool_request) > 1: + for pool in pool_request: + pools.append(pool['name']) + match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match.\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) + for future in done: + selection = future.result() + + await match.delete() + tempool = [pool for pool in pool_request if pool['name'] + == pools[int(selection.content) - 1]][0] + await selection.delete() + pool = {'name': tempool['name'], 'id': tempool['id']} + + await destination.trigger_typing() + elif pool_request: + tempool = pool_request[0] + pool = {'name': pool_request[0] + ['name'], 'id': pool_request[0]['id']} + else: + raise exc.NotFound + + page = 1 + while len(posts) < tempool['post_count']: + posts_request = await u.fetch('https://{}.net/pool/show.json'.format(booru), params={'id': tempool['id'], 'page': page}, json=True) + for post in posts_request['posts']: + posts[post['id']] = {'artist': ', '.join( + post['artist']), 'file_url': post['file_url'], 'score': post['score']} + page += 1 + + return pool, posts + + except exc.Abort as e: + await e.message.edit(content='\N{NO ENTRY SIGN}') + raise exc.Continue + + # Messy code that checks image limit and tags in blacklists + async def _get_posts(self, ctx, *, booru='e621', tags=[], limit=1, previous={}): + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + blacklist = set() + # Creates temp blacklist based on context + for bl in (self.blacklists['global_blacklist'], self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set()), self.blacklists['user_blacklist'].get(ctx.author.id, set())): + for tag in bl: + blacklist.update([tag] + list(self.aliases[tag])) + # Checks for, assigns, and removes first order in tags if possible + order = [tag for tag in tags if 'order:' in tag] + if order: + order = order[0] + tags.remove(order) + else: + order = 'order:random' + # Checks if tags are in local blacklists + if tags: + if (len(tags) > 5 and booru == 'e621') or (len(tags) > 4 and booru == 'e926'): + raise exc.TagBoundsError(formatter.tostring(tags[5:])) + for tag in tags: + if tag == 'swf' or tag == 'webm' or tag in blacklist: + raise exc.TagBlacklisted(tag) + + # Checks for blacklisted tags in endpoint blacklists - try/except is for continuing the parent loop + posts = {} + temposts = len(posts) + empty = 0 + c = 0 + while len(posts) < limit: + if c == limit * 5 + (self.LIMIT / 5): + raise exc.Timeout + request = await u.fetch('https://{}.net/post/index.json'.format(booru), params={'tags': ','.join([order] + tags), 'limit': int(self.LIMIT * limit)}, json=True) + if len(request) == 0: + raise exc.NotFound(formatter.tostring(tags)) + if len(request) < limit: + limit = len(request) + + for post in request: + if 'swf' in post['file_ext'] or 'webm' in post['file_ext']: + continue + try: + for tag in blacklist: + if tag in post['tags']: + raise exc.Continue + except exc.Continue: + continue + if post['id'] not in posts.keys() and post['id'] not in previous.keys(): + posts[post['id']] = {'artist': ', '.join( + post['artist']), 'file_url': post['file_url'], 'score': post['score']} + if len(posts) == limit: + break + + if len(posts) == temposts: + empty += 1 + if empty == 5: + break + else: + empty = 0 + temposts = len(posts) + c += 1 + + if posts: + return posts, order + else: + raise exc.NotFound(formatter.tostring(tags)) + + # Creates reaction-based paginator for linked pools + @cmds.command(name='poolpage', aliases=['poolp', 'pp', 'e621pp', 'e6pp', '6pp'], brief='e621 pool paginator', description='e621 | NSFW\nShow pools in a page format') + async def pool_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, query = kwargs['destination'], kwargs['remaining'] + hearted = {} + c = 1 + + await dest.trigger_typing() + + pool, posts = await self._get_pool(ctx, destination=dest, booru='e621', query=query) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=dest.me.color if isinstance(dest.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=pool['name'], + url='https://e621.net/pool/show?id={}'.format(pool['id']), icon_url=ctx.author.avatar_url) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted: + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content='\N{INPUT SYMBOL FOR NUMBERS}') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{WHITE HEAVY CHECK MARK}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound: + await ctx.send('**Pool not found**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Continue: + pass + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + @cmds.command(name='e621page', aliases=['e621p', 'e6p', '6p']) + @checks.is_nsfw() + async def e621_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + limit = self.LIMIT / 5 + hearted = {} + c = 1 + + tags = self._get_favorites(ctx, tags) + + await ctx.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted.keys(): + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content=f'`{c} / {len(posts)}`') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + try: + if c % limit == 0: + await dest.trigger_typing() + temposts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit, previous=posts) + posts.update(temposts) + + keys = list(posts.keys()) + values = list(posts.values()) + + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.NotFound: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + # @e621_paginator.error + # async def e621_paginator_error(self, ctx, error): + # if isinstance(error, exc.NSFW): + # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{NO ENTRY}') + + @cmds.command(name='e926page', aliases=['e926p', 'e9p', '9p']) + async def e926_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + limit = self.LIMIT / 5 + hearted = {} + c = 1 + + tags = self._get_favorites(ctx, tags) + + await ctx.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e926.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e926.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted: + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content=f'`{c} / {len(posts)}`') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + try: + if c % limit == 0: + await dest.trigger_typing() + temposts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit, previous=posts) + posts.update(temposts) + + keys = list(posts.keys()) + values = list(posts.values()) + + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.NotFound: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{WHITE HEAVY CHECK MARK}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + # Searches for and returns images from e621.net given tags when not blacklisted + @cmds.command(aliases=['e6', '6'], brief='e621 | NSFW', description='e621 | NSFW\nTag-based search for e621.net\n\nYou can only search 5 tags and 6 images at once for now.\ne6 [tags...] ([# of images])') + @checks.is_nsfw() + async def e621(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=3) + dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] + + tags = self._get_favorites(ctx, args) + + await dest.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) + + for ident, post in posts.items(): + embed = d.Embed(title=post['artist'], url='https://e621.net/post/show/{}'.format(ident), + color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + message = await dest.send(embed=embed) + + self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) + + except exc.TagBlacklisted as e: + await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + # @e621.error + # async def e621_error(self, ctx, error): + # if isinstance(error, exc.NSFW): + # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{NO ENTRY}') + + # Searches for and returns images from e926.net given tags when not blacklisted + @cmds.command(aliases=['e9', '9'], brief='e926 | SFW', description='e926 | SFW\nTag-based search for e926.net\n\nYou can only search 5 tags and 6 images at once for now.\ne9 [tags...] ([# of images])') + async def e926(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=3) + dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] + + tags = self._get_favorites(ctx, args) + + await dest.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) + + for ident, post in posts.items(): + embed = d.Embed(title=post['artist'], url='https://e926.net/post/show/{}'.format(ident), + color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + message = await dest.send(embed=embed) + + self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) + + except exc.TagBlacklisted as e: + await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['fave', 'fav', 'f']) + async def favorite(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to manage favorites.**\n*Type* `{}help fav` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @favorite.error + async def favorite_error(self, ctx, error): + pass + + @favorite.group(name='get', aliases=['g']) + async def _get_favorite(self, ctx): + pass + + @_get_favorite.command(name='tags', aliases=['t']) + async def __get_favorite_tags(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{WHITE MEDIUM STAR} {}**\'s favorite tags:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.favorites.get(ctx.author.id, {}).get('tags', set()))), delete_after=7) + + @_get_favorite.command(name='posts', aliases=['p']) + async def __get_favorite_posts(self, ctx): + pass + + @favorite.group(name='add', aliases=['a']) + async def _add_favorite(self, ctx): + pass + + @_add_favorite.command(name='tags', aliases=['t']) + async def __add_favorite_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + if tag in self.blacklists['user_blacklist'].get(ctx.author.id, set()): + raise exc.TagBlacklisted(tag) + with suppress(KeyError): + if len(self.favorites[ctx.author.id]['tags']) + len(tags) > 5: + raise exc.BoundsError + + self.favorites.setdefault(ctx.author.id, {}).setdefault( + 'tags', set()).update(tags) + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{} **added to their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except exc.BoundsError: + await ctx.send('**Favorites list currently limited to:** `5`', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**', delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + + @_add_favorite.command(name='posts', aliases=['p']) + async def __add_favorite_posts(self, ctx, *posts): + pass + + @favorite.group(name='remove', aliases=['r']) + async def _remove_favorite(self, ctx): + pass + + @_remove_favorite.command(name='tags', aliases=['t']) + async def __remove_favorite_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.favorites[ctx.author.id].get( + 'tags', set()).remove(tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{} **removed from their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except KeyError: + await ctx.send('**You do not have any favorites**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagError as e: + await ctx.send('`{}` **not in favorites**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_favorite.command(name='posts', aliases=['p']) + async def __remove_favorite_posts(self, ctx): + pass + + @favorite.group(name='clear', aliases=['c']) + async def _clear_favorite(self, ctx): + pass + + @_clear_favorite.command(name='tags', aliases=['t']) + async def __clear_favorite_tags(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + with suppress(KeyError): + del self.favorites[ctx.author.id] + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{}**\'s favorites cleared**'.format(ctx.author.mention), delete_after=5) + + @_clear_favorite.command(name='posts', aliases=['p']) + async def __clear_favorite_posts(self, ctx): + pass + + # Umbrella command structure to manage global, channel, and user blacklists + @cmds.group(aliases=['bl', 'b'], brief='(G) Manage blacklists', description='Manage channel or personal blacklists\n\nUsage:\n{p}bl get {blacklist} to show a blacklist\n{p}bl clear {blacklist} to clear a blacklist\n{p}bl add {blacklist} {tags...} to add tag(s) to a blacklist\n{p}bl remove {blacklist} {tags...} to remove tags from a blacklist') + async def blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to manage blacklists.**\n*Type* `{}help bl` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + # @blacklist.error + # async def blacklist_error(self, ctx, error): + # if isinstance(error, KeyError): + # return await ctx.send('**Blacklist does not exist**', delete_after=7) + + @blacklist.group(name='get', aliases=['g'], brief='(G) Get a blacklist\n\nUsage:\n\{p\}bl get \{blacklist\}') + async def _get_blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_get_blacklist.command(name='global', aliases=['gl', 'g'], brief='Get current global blacklist', description='Get current global blacklist\n\nThis applies to all booru commands, in accordance with Discord\'s ToS agreement\n\nExample:\n\{p\}bl get global') + async def __get_global_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} **Global blacklist:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']))) + + @_get_blacklist.command(name='channel', aliases=['ch', 'c'], brief='Get current channel blacklist', description='Get current channel blacklist\n\nThis is based on context - the channel where the command was executed\n\nExample:\{p\}bl get channel') + async def __get_channel_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) + + @_get_blacklist.command(name='me', aliases=['m'], brief='Get your personal blacklist', description='Get your personal blacklist\n\nYour blacklist is not viewable by anyone but you, except if you call this command in a public channel. The blacklist will be deleted soon after for your privacy\n\nExample:\n\{p\}bl get me') + async def __get_user_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} {}**\'s blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.blacklists['user_blacklist'].get(ctx.author.id, set()))), delete_after=7) + + @_get_blacklist.command(name='here', aliases=['h'], brief='Get current global and channel blacklists', description='Get current global and channel blacklists in a single message\n\nExample:\{p\}bl get here') + async def __get_here_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} **__Blacklisted:__**\n\n**Global:**\n```\n{}```\n**{}:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']), ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) + + @_get_blacklist.group(name='all', aliases=['a'], hidden=True) + async def __get_all_blacklists(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + @__get_all_blacklists.command(name='guild', aliases=['g']) + @cmds.has_permissions(manage_channels=True) + async def ___get_all_guild_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} **__{} blacklists:__**\n\n{}'.format(guild.name, formatter.dict_tostring(self.blacklists['guild_blacklist'].get(guild.id, {})))) + + @__get_all_blacklists.command(name='user', aliases=['u', 'member', 'm']) + @cmds.is_owner() + async def ___get_all_user_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} **__User blacklists:__**\n\n{}'.format(formatter.dict_tostring(self.blacklists['user_blacklist']))) + + @blacklist.group(name='add', aliases=['a'], brief='(G) Add tag(s) to a blacklist\n\nUsage:\n\{p\}bl add \{blacklist\} \{tags...\}') + async def _add_tags(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_add_tags.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __add_global_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + await dest.trigger_typing() + + self.blacklists['global_blacklist'].update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('**Added to global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) + + @_add_tags.command(name='channel', aliases=['ch', 'c'], brief='@manage_channel@ Add tag(s) to the current channel blacklist (requires manage_channel)', description='Add tag(s) to the current channel blacklist ') + @cmds.has_permissions(manage_channels=True) + async def __add_channel_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.trigger_typing() + + self.blacklists['guild_blacklist'].setdefault( + guild.id, {}).setdefault(ctx.channel.id, set()).update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('**Added to** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags)), delete_after=5) + + @_add_tags.command(name='me', aliases=['m']) + async def __add_user_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + await dest.trigger_typing() + + self.blacklists['user_blacklist'].setdefault( + ctx.author.id, set()).update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('{} **added to their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + @blacklist.group(name='remove', aliases=['rm', 'r']) + async def _remove_tags(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __remove_global_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.blacklists['global_blacklist'].remove(tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Removed from global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='channel', aliases=['ch', 'c']) + @cmds.has_permissions(manage_channels=True) + async def __remove_channel_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + for tag in tags: + try: + self.blacklists['guild_blacklist'][guild.id][ctx.channel.id].remove( + tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Removed from** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags), delete_after=5)) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='me', aliases=['m']) + async def __remove_user_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.blacklists['user_blacklist'][ctx.author.id].remove( + tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{} **removed from their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @blacklist.group(name='clear', aliases=['cl', 'c']) + async def _clear_blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_clear_blacklist.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __clear_global_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + self.blacklists['global_blacklist'].clear() + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Global blacklist cleared**', delete_after=5) + + @_clear_blacklist.command(name='channel', aliases=['ch', 'c']) + @cmds.has_permissions(manage_channels=True) + async def __clear_channel_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + with suppress(KeyError): + del self.blacklists['guild_blacklist'][guild.id][ctx.channel.id] + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{} **blacklist cleared**'.format(ctx.channel.mention), delete_after=5) + + @_clear_blacklist.command(name='me', aliases=['m']) + async def __clear_user_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + with suppress(KeyError): + del self.blacklists['user_blacklist'][ctx.author.id] + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{}**\'s blacklist cleared**'.format(ctx.author.mention), delete_after=5) diff --git a/src/cogs/owner.py b/src/cogs/owner.py index 6c5135f..c4cffe4 100644 --- a/src/cogs/owner.py +++ b/src/cogs/owner.py @@ -1,245 +1,245 @@ -import asyncio -import code -import io -import os -import re -import sys -import traceback as tb -from contextlib import redirect_stdout, suppress - -import discord as d -import pyrasite as pyr -from discord.ext import commands as cmds - -from misc import exceptions as exc -from misc import checks -from utils import utils as u - - -class Bot: - - def __init__(self, bot): - self.bot = bot - - # Close connection to Discord - immediate offline - @cmds.command(name=',die', aliases=[',d'], brief='Kills the bot', description='BOT OWNER ONLY\nCloses the connection to Discord', hidden=True) - @cmds.is_owner() - async def die(self, ctx): - await ctx.message.add_reaction('\N{CRESCENT MOON}') - - await self.bot.get_channel(u.config['info_channel']).send('**Shutting down** \N{CRESCENT MOON} . . .') - - chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' - u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) - u.dump(u.temp, 'temp/temp.pkl') - - # loop = self.bot.loop.all_tasks() - # for task in loop: - # task.cancel() - await self.bot.logout() - u.close(self.bot.loop) - print('\n< < < < < < < < < < < <\nD I S C O N N E C T E D\n< < < < < < < < < < < <\n') - # u.notify('D I S C O N N E C T E D') - - @cmds.command(name=',restart', aliases=[',res', ',r'], hidden=True) - @cmds.is_owner() - async def restart(self, ctx): - await ctx.message.add_reaction('\N{SLEEPING SYMBOL}') - - print('\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\nR E S T A R T I N G\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n') - await self.bot.get_channel(u.config['info_channel']).send('**Restarting** \N{SLEEPING SYMBOL} . . .') - # u.notify('R E S T A R T I N G') - - chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' - u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) - u.dump(u.temp, 'temp/temp.pkl') - - # loop = self.bot.loop.all_tasks() - # for task in loop: - # task.cancel() - await self.bot.logout() - u.close(self.bot.loop) - os.execl(sys.executable, 'python3', 'run.py') - - # Invite bot to bot owner's server - @cmds.command(name=',invite', aliases=[',inv', ',link'], brief='Invite the bot', description='BOT OWNER ONLY\nInvite the bot to a server (Requires admin)', hidden=True) - @cmds.is_owner() - async def invite(self, ctx): - await ctx.message.add_reaction('\N{ENVELOPE}') - - await ctx.send('https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}'.format(u.config['client_id'], u.config['permissions']), delete_after=5) - - @cmds.command(name=',guilds', aliases=[',glds', ',servers', ',servs']) - @cmds.is_owner() - async def guilds(self, ctx): - paginator = cmds.Paginator() - - for guild in self.bot.guilds: - paginator.add_line(guild.name) - - for page in paginator.pages: - await ctx.send(f'**Guilds:**\n{page}') - - @cmds.command(name=',status', aliases=[',presence', ',game'], hidden=True) - @cmds.is_owner() - async def change_status(self, ctx, *, game=None): - if game: - await self.bot.change_presence(game=d.Game(name=game)) - u.config['playing'] = game - u.dump(u.config, 'config.json', json=True) - await ctx.send(f'**Game changed to** `{game}`') - else: - await self.bot.change_presence(game=None) - u.config['playing'] = '' - u.dump(u.config, 'config.json', json=True) - await ctx.send('**Game changed to** ` `') - - @cmds.command(name=',username', aliases=[',user'], hidden=True) - @cmds.is_owner() - async def change_username(self, ctx, *, username=None): - if username: - await self.bot.user.edit(username=username) - await ctx.send(f'**Username changed to** `{username}`') - else: - await ctx.send('**Invalid string**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - -class Tools: - - def __init__(self, bot): - self.bot = bot - - def format(self, i='', o=''): - if len(o) > 1: - return '>>> {}\n{}'.format(i, o) - else: - return '>>> {}'.format(i) - - async def generate(self, d, i='', o=''): - return await d.send('```python\n{}```'.format(self.format(i, o))) - - async def refresh(self, m, i='', o=''): - output = m.content[9:-3] - if len(re.findall('\n', output)) <= 20: - await m.edit(content='```python\n{}\n{}\n>>>```'.format(output, self.format(i, o))) - else: - await m.edit(content='```python\n{}```'.format(self.format(i, o))) - - async def generate_err(self, d, o=''): - return await d.send('```\n{}```'.format(o)) - - async def refresh_err(self, m, o=''): - await m.edit(content='```\n{}```'.format(o)) - - @cmds.command(name=',console', aliases=[',con', ',c'], hidden=True) - @cmds.is_owner() - async def console(self, ctx): - def execute(msg): - if msg.content.lower().startswith('exec ') and msg.author is ctx.author and msg.channel is ctx.channel: - msg.content = msg.content[5:] - return True - return False - - def evaluate(msg): - if msg.content.lower().startswith('eval ') and msg.author is ctx.author and msg.channel is ctx.channel: - msg.content = msg.content[5:] - return True - return False - - def exit(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and user is ctx.author and reaction.message.id == ctx.message.id: - raise exc.Abort - return False - - try: - console = await self.generate(ctx) - exception = await self.generate_err(ctx) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - - while not self.bot.is_closed(): - try: - done, pending = await asyncio.wait([self.bot.wait_for('message', check=execute), self.bot.wait_for('message', check=evaluate), self.bot.wait_for('reaction_add', check=exit)], return_when=asyncio.FIRST_COMPLETED) - - message = done.pop().result() - print(message.content) - - except exc.Execute: - try: - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - exec(message.content) - - except Exception: - await self.refresh_err(exception, tb.format_exc(limit=1)) - - finally: - await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - with suppress(d.NotFound): - await message.delete() - - except exc.Evaluate: - try: - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - eval(message.content) - - except Exception: - await self.refresh_err(exception, tb.format_exc(limit=1)) - - finally: - await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - with suppress(d.NotFound): - await message.delete() - - except exc.Abort: - pass - - finally: - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - print('RESET : sys.std output/error') - - @cmds.command(name=',execute', aliases=[',exec'], hidden=True) - @cmds.is_owner() - async def execute(self, ctx, *, exe): - try: - with io.StringIO() as buff, redirect_stdout(buff): - exec(exe) - await self.generate(ctx, exe, f'\n{buff.getvalue()}') - - except Exception: - await self.generate(ctx, exe, f'\n{tb.format_exc()}') - - @cmds.command(name=',evaluate', aliases=[',eval'], hidden=True) - @cmds.is_owner() - async def evaluate(self, ctx, *, evl): - try: - with io.StringIO() as buff, redirect_stdout(buff): - eval(evl) - await self.generate(ctx, evl, f'\n{buff.getvalue()}') - - except Exception: - await self.generate(ctx, evl, f'\n{tb.format_exc()}') - - @cmds.group(aliases=[',db'], hidden=True) - @cmds.is_owner() - async def debug(self, ctx): - console = await self.generate(ctx) - - @debug.command(name='inject', aliases=['inj']) - async def _inject(self, ctx, *, input_): - pass - - @debug.command(name='inspect', aliases=['ins']) - async def _inspect(self, ctx, *, input_): - pass - - # @cmds.command(name='endpoint', aliases=['end']) - # async def get_endpoint(self, ctx, *args): - # await ctx.send(f'```\n{await u.fetch(f"https://{args[0]}/{args[1]}/{args[2]}", params={args[3]: args[4], "limit": 1}, json=True)}```') +import asyncio +import code +import io +import os +import re +import sys +import traceback as tb +from contextlib import redirect_stdout, suppress + +import discord as d +import pyrasite as pyr +from discord.ext import commands as cmds + +from misc import exceptions as exc +from misc import checks +from utils import utils as u + + +class Bot: + + def __init__(self, bot): + self.bot = bot + + # Close connection to Discord - immediate offline + @cmds.command(name=',die', aliases=[',d'], brief='Kills the bot', description='BOT OWNER ONLY\nCloses the connection to Discord', hidden=True) + @cmds.is_owner() + async def die(self, ctx): + await ctx.message.add_reaction('\N{CRESCENT MOON}') + + await self.bot.get_channel(u.config['info_channel']).send('**Shutting down** \N{CRESCENT MOON} . . .') + + chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' + u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) + u.dump(u.temp, 'temp/temp.pkl') + + # loop = self.bot.loop.all_tasks() + # for task in loop: + # task.cancel() + await self.bot.logout() + u.close(self.bot.loop) + print('\n< < < < < < < < < < < <\nD I S C O N N E C T E D\n< < < < < < < < < < < <\n') + # u.notify('D I S C O N N E C T E D') + + @cmds.command(name=',restart', aliases=[',res', ',r'], hidden=True) + @cmds.is_owner() + async def restart(self, ctx): + await ctx.message.add_reaction('\N{SLEEPING SYMBOL}') + + print('\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\nR E S T A R T I N G\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n') + await self.bot.get_channel(u.config['info_channel']).send('**Restarting** \N{SLEEPING SYMBOL} . . .') + # u.notify('R E S T A R T I N G') + + chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' + u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) + u.dump(u.temp, 'temp/temp.pkl') + + # loop = self.bot.loop.all_tasks() + # for task in loop: + # task.cancel() + await self.bot.logout() + u.close(self.bot.loop) + os.execl(sys.executable, 'python3', 'run.py') + + # Invite bot to bot owner's server + @cmds.command(name=',invite', aliases=[',inv', ',link'], brief='Invite the bot', description='BOT OWNER ONLY\nInvite the bot to a server (Requires admin)', hidden=True) + @cmds.is_owner() + async def invite(self, ctx): + await ctx.message.add_reaction('\N{ENVELOPE}') + + await ctx.send('https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}'.format(u.config['client_id'], u.config['permissions']), delete_after=5) + + @cmds.command(name=',guilds', aliases=[',glds', ',servers', ',svrs']) + @cmds.is_owner() + async def guilds(self, ctx): + paginator = cmds.Paginator() + + for guild in self.bot.guilds: + paginator.add_line(guild.name) + + for page in paginator.pages: + await ctx.send(f'**Guilds:**\n{page}') + + @cmds.command(name=',status', aliases=[',presence', ',game'], hidden=True) + @cmds.is_owner() + async def change_status(self, ctx, *, game=None): + if game: + await self.bot.change_presence(game=d.Game(name=game)) + u.config['playing'] = game + u.dump(u.config, 'config.json', json=True) + await ctx.send(f'**Game changed to** `{game}`') + else: + await self.bot.change_presence(game=None) + u.config['playing'] = '' + u.dump(u.config, 'config.json', json=True) + await ctx.send('**Game changed to** ` `') + + @cmds.command(name=',username', aliases=[',user'], hidden=True) + @cmds.is_owner() + async def change_username(self, ctx, *, username=None): + if username: + await self.bot.user.edit(username=username) + await ctx.send(f'**Username changed to** `{username}`') + else: + await ctx.send('**Invalid string**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + +class Tools: + + def __init__(self, bot): + self.bot = bot + + def format(self, i='', o=''): + if len(o) > 1: + return '>>> {}\n{}'.format(i, o) + else: + return '>>> {}'.format(i) + + async def generate(self, d, i='', o=''): + return await d.send('```python\n{}```'.format(self.format(i, o))) + + async def refresh(self, m, i='', o=''): + output = m.content[9:-3] + if len(re.findall('\n', output)) <= 20: + await m.edit(content='```python\n{}\n{}\n>>>```'.format(output, self.format(i, o))) + else: + await m.edit(content='```python\n{}```'.format(self.format(i, o))) + + async def generate_err(self, d, o=''): + return await d.send('```\n{}```'.format(o)) + + async def refresh_err(self, m, o=''): + await m.edit(content='```\n{}```'.format(o)) + + @cmds.command(name=',console', aliases=[',con', ',c'], hidden=True) + @cmds.is_owner() + async def console(self, ctx): + def execute(msg): + if msg.content.lower().startswith('exec ') and msg.author is ctx.author and msg.channel is ctx.channel: + msg.content = msg.content[5:] + return True + return False + + def evaluate(msg): + if msg.content.lower().startswith('eval ') and msg.author is ctx.author and msg.channel is ctx.channel: + msg.content = msg.content[5:] + return True + return False + + def exit(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and user is ctx.author and reaction.message.id == ctx.message.id: + raise exc.Abort + return False + + try: + console = await self.generate(ctx) + exception = await self.generate_err(ctx) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + + while not self.bot.is_closed(): + try: + done, pending = await asyncio.wait([self.bot.wait_for('message', check=execute), self.bot.wait_for('message', check=evaluate), self.bot.wait_for('reaction_add', check=exit)], return_when=asyncio.FIRST_COMPLETED) + + message = done.pop().result() + print(message.content) + + except exc.Execute: + try: + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + exec(message.content) + + except Exception: + await self.refresh_err(exception, tb.format_exc(limit=1)) + + finally: + await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + with suppress(d.NotFound): + await message.delete() + + except exc.Evaluate: + try: + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + eval(message.content) + + except Exception: + await self.refresh_err(exception, tb.format_exc(limit=1)) + + finally: + await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + with suppress(d.NotFound): + await message.delete() + + except exc.Abort: + pass + + finally: + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + print('RESET : sys.std output/error') + + @cmds.command(name=',execute', aliases=[',exec'], hidden=True) + @cmds.is_owner() + async def execute(self, ctx, *, exe): + try: + with io.StringIO() as buff, redirect_stdout(buff): + exec(exe) + await self.generate(ctx, exe, f'\n{buff.getvalue()}') + + except Exception: + await self.generate(ctx, exe, f'\n{tb.format_exc()}') + + @cmds.command(name=',evaluate', aliases=[',eval'], hidden=True) + @cmds.is_owner() + async def evaluate(self, ctx, *, evl): + try: + with io.StringIO() as buff, redirect_stdout(buff): + eval(evl) + await self.generate(ctx, evl, f'\n{buff.getvalue()}') + + except Exception: + await self.generate(ctx, evl, f'\n{tb.format_exc()}') + + @cmds.group(aliases=[',db'], hidden=True) + @cmds.is_owner() + async def debug(self, ctx): + console = await self.generate(ctx) + + @debug.command(name='inject', aliases=['inj']) + async def _inject(self, ctx, *, input_): + pass + + @debug.command(name='inspect', aliases=['ins']) + async def _inspect(self, ctx, *, input_): + pass + + # @cmds.command(name='endpoint', aliases=['end']) + # async def get_endpoint(self, ctx, *args): + # await ctx.send(f'```\n{await u.fetch(f"https://{args[0]}/{args[1]}/{args[2]}", params={args[3]: args[4], "limit": 1}, json=True)}```')