diff --git a/src/cogs/booru.py b/src/cogs/booru.py index 8b129cc..f6b3192 100644 --- a/src/cogs/booru.py +++ b/src/cogs/booru.py @@ -1,1605 +1,1672 @@ -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 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() - - await temp[0].send(embed=temp[1]) - - await asyncio.sleep(self.RATE_LIMIT) - - print('STOPPED : hearting') - - async def queue_for_hearts(self, *, message, send): - def on_reaction(reaction, user): - if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id: - raise exc.Save(user) - return False - - try: - await message.add_reaction('\N{HEAVY BLACK HEART}') - await asyncio.sleep(1) - - while self.hearting: - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=60 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60 * 60)]) - - except exc.Save as e: - await self.heartqueue.put((e.user, send)) - - except asyncio.TimeoutError: - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - # @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'{u.get_aspectratio(post["width"], post["height"])} \N{ZERO WIDTH SPACE} {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'{u.get_aspectratio(post["width"], post["height"])} \N{ZERO WIDTH SPACE} {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'{u.get_aspectratio(post["width"], post["height"])} \N{ZERO WIDTH SPACE} {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'{u.get_aspectratio(post["width"], post["height"])} \N{ZERO WIDTH SPACE} {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']['score'])) - - await message.channel.send('**Probable match from** {}'.format(message.author.display_name), embed=embed) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - 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}') - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - with suppress(err.NotFound): - await message.delete() - - print('STOPPED : reversifying') - - async def queue_for_reversification(self, channel): - def check(msg): - if msg.content.lower() == 'stop' 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` 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.group(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: + raise exc.Save(user) + return False + def on_message(msg): + if 'stop h' in msg.content.lower(): + raise exc.Abort + return msg.channel.id == channel.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', msg.content) or msg.attachments) + + 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: + async for message in channel.history(limit=300): + if re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content) or message.attachments: + self.bot.loop.create_task(self._wait_for_reaction(message)) + + while self.hearting: + message = await self.bot.wait_for('message', check=on_message) + self.bot.loop.create_task(self._wait_for_reaction(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) + + async def _wait_for_reaction(self, message): + def on_reaction(reaction, user): + if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id: + raise exc.Save(user) + return False + + while self.hearting: + try: + await self.bot.wait_for('reaction_add', check=on_reaction) + + except exc.Save as e: + await self.heartqueue.put((e.user, message)) + + @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: + await ctx.send('**Already auto-hearting in {}.** Type `stop h(earting)` to stop.'.format(ctx.channel.mention), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + # @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.group(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/management.py b/src/cogs/management.py index 7b38be9..3bd4130 100644 --- a/src/cogs/management.py +++ b/src/cogs/management.py @@ -116,6 +116,10 @@ class Administration: await ctx.send('**Deletion timed out**', delete_after=7) await ctx.message.add_reaction('\N{CROSS MARK}') + @cmds.group(aliases=['task', 'tsk']) + async def tasks(self): + pass + async def delete(self): while self.deleting: message = await self.queue.get() @@ -128,7 +132,7 @@ class Administration: async def queue_for_deletion(self, channel): def check(msg): - if msg.content.lower() == 'stop' and msg.channel is channel and msg.author.guild_permissions.administrator: + if 'stop d' in msg.content.lower() and msg.channel is channel and msg.author.guild_permissions.administrator: raise exc.Abort elif msg.channel is channel and not msg.pinned: return True @@ -136,7 +140,7 @@ class Administration: try: async for message in channel.history(limit=None): - if message.content.lower() == 'stop' and message.author.guild_permissions.administrator: + if 'stop d' in message.content.lower() and message.author.guild_permissions.administrator: raise exc.Abort if not message.pinned: await self.queue.put(message) @@ -170,7 +174,7 @@ class Administration: raise exc.Exists except exc.Exists: - await ctx.send('**Already auto-deleting in {}.** Type `stop` to stop.'.format(ctx.channel.mention), delete_after=7) + await ctx.send('**Already auto-deleting in {}.** Type `stop d(eleting)` to stop.'.format(ctx.channel.mention), delete_after=7) await ctx.message.add_reaction('\N{CROSS MARK}') @cmds.group(aliases=['setting', 'set', 's']) @@ -179,7 +183,7 @@ class Administration: pass @settings.command(name='deletecommands', aliases=['delcmds', 'delcmd']) - async def _settings_delete_commands(self, ctx): + async def _settings_deletecommands(self, ctx): if ctx.guild.id not in u.settings['del_ctx']: u.settings['del_ctx'].append(ctx.guild.id) else: @@ -197,3 +201,13 @@ class Administration: del u.settings['prefixes'][ctx.guild.id] await ctx.send(f'**Prefix set to:** `{"` or `".join(prefixes if ctx.guild.id in u.settings["prefixes"] else u.config["prefix"])}`') + + @settings.command(name='deleteresponses', aliases=['delresps', 'delresp']) + async def _settings_deleteresponses(self, ctx): + if ctx.guild.id not in u.settings['del_resp']: + u.settings['del_resp'].append(ctx.guild.id) + else: + u.settings['del_resp'].remove(ctx.guild.id) + u.dump(u.settings, 'settings.pkl') + + await ctx.send(f'**Delete command responses:** `{ctx.guild.id in u.settings["del_resp"]}`') diff --git a/src/cogs/music.py b/src/cogs/music.py new file mode 100644 index 0000000..6f44964 --- /dev/null +++ b/src/cogs/music.py @@ -0,0 +1,57 @@ +import asyncio +import json +from datetime import datetime as dt +from urllib import parse +import re +from pprint import pprint + +import discord as d +from discord import errors as err +from discord.ext import commands as cmds +from discord.ext.commands import errors as errext +import gmusicapi as gpm +import googleapiclient as gapic +import apiclient as apic + +from misc import exceptions as exc +from misc import checks +from utils import utils as u + + +class Music: + + def __init__(self, bot): + self.bot = bot + + self.yt_service = apic.discovery.build('youtube', 'v3', developerKey=u.secrets['client_secrets']['client_secret']) + + @cmds.group(aliases=['pl'], brief='(G) Play music', description='Play music from YouTube, Soundcloud, or Google Play Music') + async def play(self, ctx): + print(ctx.invoked_subcommand) + + @play.command(name='youtube', aliases=['you', 'tube', 'yt', 'y']) + async def _play_youtube(self, ctx, *videos): + try: + if not videos: + raise exc.MissingArgument + + vids = [] + + for video in videos: + if 'http' in video and 'youtube' in video: + vids.append(parse.parse_qs(parse.urlparse(video).query)['v'][0]) + else: + vids.append(video) + + print(vids) + + response = self.yt_service.videos().list(part='snippet', id=','.join(vids)).execute() + pprint(response) + + except exc.MissingArgument: + await ctx.send('**Invalid youtube url or ID**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @play.command(name='googleplaymusic', aliases=['googleplay', 'googlemusic', 'playmusic', 'play', 'gpm']) + async def _play_googleplaymusic(self, ctx, query): + pass diff --git a/src/cogs/periodic.py b/src/cogs/periodic.py index 49a85c5..1f36b88 100644 --- a/src/cogs/periodic.py +++ b/src/cogs/periodic.py @@ -23,3 +23,11 @@ class Post: async def _check_posts(self, user, channel): pass + + @cmds.group(aliases=['update', 'up', 'u']) + async def updates(self, ctx): + pass + + @updates.command(name='googleplaymusic', aliases=['googlemusic', 'playmusic', 'music', 'gpm']) + async def _updates_googleplaymusic(self, ctx): + pass diff --git a/src/misc/exceptions.py b/src/misc/exceptions.py index ae9af19..965ebe9 100644 --- a/src/misc/exceptions.py +++ b/src/misc/exceptions.py @@ -39,9 +39,9 @@ class Right(Exception): class Save(Exception): - def __init__(self, user=None): + def __init__(self, user=None, message=None): self.user = user - + self.message = message class GoTo(Exception): pass diff --git a/src/run.py b/src/run.py index 7070ba8..0c401dc 100644 --- a/src/run.py +++ b/src/run.py @@ -1,255 +1,260 @@ -import asyncio -from datetime import datetime as dt -import json -import logging as log -import subprocess -import sys -import traceback as tb -from contextlib import suppress -from pprint import pprint -from hurry.filesize import size, alternative -from urllib.parse import urlparse - -import discord as d -from discord import errors as err -from discord import utils -from discord.ext import commands as cmds -from discord.ext.commands import errors as errext - -from misc import exceptions as exc -from misc import checks -from utils import utils as u - -log.basicConfig(level=log.WARNING) - - -# class HelpFormatter(cmds.HelpFormatter): -# -# async def format(self): -# self._paginator = cmds.Paginator() -# -# # we need a padding of ~80 or so -# -# description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) -# -# if description: -# # portion -# self._paginator.add_line(description, empty=True) -# -# if isinstance(self.command, cmds.Command): -# # -# signature = self.get_command_signature() -# self._paginator.add_line(signature, empty=True) -# -# # section -# if self.command.help: -# self._paginator.add_line(self.command.help, empty=True) -# -# # end it here if it's just a regular command -# if not self.has_subcommands(): -# self._paginator.close_page() -# return self._paginator.pages -# -# max_width = self.max_name_size - - -def get_prefix(bot, message): - with suppress(AttributeError): - return u.settings['prefixes'].get(message.guild.id, u.config['prefix']) - return u.config['prefix'] - -bot = cmds.Bot(command_prefix=get_prefix, self_bot=u.config['selfbot'], formatter=cmds.HelpFormatter(show_check_failure=True), description='Modufur - A booru bot with a side of management and automated tasking\nMade by @Myned#3985\n\nNSFW for Not Safe For Wumpus commands\n(G) for group commands\n@permission@ for required permissions\n!notice! for important information\np for prefix\n\n\{\} for mandatory argument\n[] for optional argument\n... for one or more arguments', help_attrs={'aliases': ['h']}, pm_help=None) - -@bot.command(help='help', brief='brief', description='description', usage='usage', hidden=True) -async def test(ctx): - pass - -# Send and print ready message to #testing and console after logon - - -@bot.event -async def on_ready(): - if not checks.ready: - from cogs import booru, info, management, owner, tools - - for cog in (tools.Utils(bot), owner.Bot(bot), owner.Tools(bot), management.Administration(bot), info.Info(bot), booru.MsG(bot)): - bot.add_cog(cog) - print(f'COG : {type(cog).__name__}') - - # bot.loop.create_task(u.clear(booru.temp_urls, 30*60)) - - if u.config['playing'] is not '': - await bot.change_presence(game=d.Game(name=u.config['playing'])) - - print('\n> > > > > > > > >\nC O N N E C T E D : {}\n> > > > > > > > >\n'.format(bot.user.name)) - await bot.get_channel(u.config['info_channel']).send(f'**Started** \N{BLACK SUN WITH RAYS} `{"` or `".join(u.config["prefix"])}`') - # u.notify('C O N N E C T E D') - - if u.temp['startup']: - with suppress(err.NotFound): - if u.temp['startup'][0] == 'guild': - dest = bot.get_channel(u.temp['startup'][1]) - else: - dest = bot.get_user(u.temp['startup'][1]) - message = await dest.get_message(u.temp['startup'][2]) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - u.temp['startup'] = () - u.dump(u.temp, 'temp/temp.pkl') - - checks.ready = True - else: - print('\n- - - -\nI N F O : reconnected, skipping initialization\n- - - -') - - -@bot.event -async def on_message(message): - if not u.config['selfbot']: - if message.author is not bot.user and not message.author.bot: - await bot.process_commands(message) - else: - if not message.author.bot: - await bot.process_commands(message) - - -@bot.event -async def on_error(error, *args, **kwargs): - print('\n! ! ! ! !\nE R R O R : {}\n! ! ! ! !\n'.format(error), file=sys.stderr) - tb.print_exc() - await bot.get_user(u.config['owner_id']).send('**ERROR** \N{WARNING SIGN}\n```\n{}```'.format(error)) - await bot.get_channel(u.config['info_channel']).send('**ERROR** \N{WARNING SIGN}\n```\n{}```'.format(error)) - - if u.temp['startup']: - with suppress(err.NotFound): - if u.temp['startup'][0] == 'guild': - dest = bot.get_channel(u.temp['startup'][1]) - else: - dest = bot.get_user(u.temp['startup'][1]) - message = await dest.get_message(u.temp['startup'][2]) - - await message.add_reaction('\N{WARNING SIGN}') - - u.temp.clear() - u.dump(u.temp, 'temp/temp.pkl') - # u.notify('E R R O R') - await bot.logout() - u.close(bot.loop) - - -@bot.event -async def on_command_error(ctx, error): - if isinstance(error, err.NotFound): - print('NOT FOUND') - elif isinstance(error, errext.CheckFailure): - await ctx.send('**Insufficient permissions**', delete_after=10) - await ctx.message.add_reaction('\N{NO ENTRY}') - elif isinstance(error, errext.CommandNotFound): - print('INVALID COMMAND : {}'.format(error), file=sys.stderr) - await ctx.message.add_reaction('\N{BLACK QUESTION MARK ORNAMENT}') - else: - print('\n! ! ! ! ! ! ! ! ! ! ! !\nC O M M A N D E R R O R : {}\n! ! ! ! ! ! ! ! ! ! ! !\n'.format( - error), file=sys.stderr) - tb.print_exception(type(error), error, error.__traceback__, file=sys.stderr) - await bot.get_user(u.config['owner_id']).send('**COMMAND ERROR** \N{WARNING SIGN} `{}` from {} in {}\n```\n{}```'.format(ctx.message.content, ctx.author.mention, ctx.channel.mention, ''.join(tb.format_exception(type(error), error, error.__traceback__ if len(str(error.__traceback__)) < 1500 else str(error.__traceback__)[:1500])))) - await bot.get_channel(u.config['info_channel']).send('**COMMAND ERROR** \N{WARNING SIGN} `{}` from {} in {}\n```\n{}```'.format(ctx.message.content, ctx.author.mention, ctx.channel.mention, error)) - await exc.send_error(ctx, error) - await ctx.message.add_reaction('\N{WARNING SIGN}') - # u.notify('C O M M A N D E R R O R') - -@bot.event -async def on_command_completion(ctx): - with suppress(err.NotFound): - with suppress(AttributeError): - if ctx.guild.id in u.settings['del_ctx'] and ctx.me.permissions_in(ctx.channel).manage_messages and isinstance(ctx.message.channel, d.TextChannel): - await ctx.message.delete() - - await ctx.message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - for command in ('lastcommand', ',restart', ',die'): - if ctx.command.name == command: - return - - u.last_commands[ctx.author.id] = ctx - -@bot.event -async def on_guild_remove(guild): - print(f'LEFT : {guild.name}') - - for task, idents in u.tasks.items(): - for channel in guild.channels: - if channel.id in idents: - idents.remove(channel.id) - print(f'STOPPED : {task} in #{channel.id}') - u.dump(u.tasks, 'cogs/tasks.pkl') - -# d.opus.load_opus('opus') - - -async def wait(voice): - asyncio.sleep(5) - await voice.disconnect() - - -def after(voice, error): - coro = voice.disconnect() - future = asyncio.run_coroutine_threadsafe(coro, voice.loop) - future.result() - -# suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'None', 'tags': {}, 'total': 0}) -@bot.command(name=',test', hidden=True) -@cmds.is_owner() -async def test(ctx): - post = await u.fetch('https://e621.net/post/show.json?id=1145042', json=True) - - tags = [] - if post['tags']: - temptags = post['tags'].split(' ') - cis = [] - for tag in suggested: - pass - for tag in temptags: - tags.append(f'[{tag}](https://e621.net/post?tags={tag})') - # tags = ' '.join(tags) - else: - tags = 'None' - - if post['description']: - post_description = post['description'] if len(post['description']) < 200 else f'{post["description"][:200]}...' - else: - post_description = 'None' - - title = ', '.join(post['artist']) - description = f'posted by: *[{post["author"]}](https://e621.net/post?tags=user:{post["author"]})*' - url = f'https://e621.net/post?tags={",".join(post["artist"])}' - # timestamp = dt.utcnow() - color = ctx.me.color - footer = {'text': post['score'], 'icon_url': 'https://images-ext-1.discordapp.net/external/W2k0ZzhU7ngvN_-CdqAa3H3FmkfCNYQTxPG_DsvacB4/https/emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png'} - # image = 'https://e621.net/post/show/54360' - thumbnail = post['file_url'] - author = {'name': post['id'], 'url': f'https://e621.net/post/show/{post["id"]}', 'icon_url': ctx.author.avatar_url} - - fields = [] - names = ('File', 'Sources', 'Description', 'tags', 'tags (ext.)') - values = (f'[{post["md5"]}]({post["file_url"]}) | [{post["file_ext"]}](https://e621.net/post?tags=type:{post["file_ext"]})\n\n**Size** [{size(post["file_size"], system=alternative)}](https://e621.net/post?tags=filesize:{post["file_size"]})\n**Resolution** [{post["width"]} x {post["height"]}](https://e621.net/post?tags=width:{post["width"]},height:{post["height"]}) | [{u.get_aspectratio(post["width"], post["height"])}](https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f})', '\n'.join([f'[{urlparse(source).netloc}]({source})' for source in post['sources']]), post_description, ' '.join(tags[:20]), ' '.join(tags[20:])) - inlines = (False, False, False, True, True) - for name, value, inline in zip(names, values, inlines): - fields.append({'name': name, 'value': value, 'inline': inline}) - - embed = u.generate_embed(ctx, title=title, description=description, url=url, colour=color, footer=footer, thumbnail=thumbnail, author=author, fields=fields) - - await ctx.send(embed=embed) - # print(ctx.args) - # print(ctx.kwargs) - # if '<:N_:368917475531816962>' in message: - # await ctx.send('<:N_:368917475531816962>') - # logs = [] - # async for entry in ctx.guild.audit_logs(limit=None, action=d.AuditLogAction.message_delete): - # logs.append( - # f'@{entry.user.name} deleted {entry.extra.count} messages from @{entry.target.name} in #{entry.extra.channel.name}') - # pprint(logs) - # channel = bot.get_channel(int(cid)) - # voice = await channel.connect() - # voice.play(d.AudioSource, after=lambda: after(voice)) - -bot.run(u.config['token'], bot=not u.config['selfbot']) +import asyncio +from datetime import datetime as dt +import json +import logging as log +import subprocess +import sys +import traceback as tb +from contextlib import suppress +from pprint import pprint +from hurry.filesize import size, alternative +from urllib.parse import urlparse + +import discord as d +from discord import errors as err +from discord import utils +from discord.ext import commands as cmds +from discord.ext.commands import errors as errext + +from misc import exceptions as exc +from misc import checks +from utils import utils as u + +log.basicConfig(level=log.WARNING) + + +# class HelpFormatter(cmds.HelpFormatter): +# +# async def format(self): +# self._paginator = cmds.Paginator() +# +# # we need a padding of ~80 or so +# +# description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) +# +# if description: +# # portion +# self._paginator.add_line(description, empty=True) +# +# if isinstance(self.command, cmds.Command): +# # +# signature = self.get_command_signature() +# self._paginator.add_line(signature, empty=True) +# +# # section +# if self.command.help: +# self._paginator.add_line(self.command.help, empty=True) +# +# # end it here if it's just a regular command +# if not self.has_subcommands(): +# self._paginator.close_page() +# return self._paginator.pages +# +# max_width = self.max_name_size + + +def get_prefix(bot, message): + with suppress(AttributeError): + return u.settings['prefixes'].get(message.guild.id, u.config['prefix']) + return u.config['prefix'] + +bot = cmds.Bot(command_prefix=get_prefix, self_bot=u.config['selfbot'], formatter=cmds.HelpFormatter(show_check_failure=True), description='Modufur - A booru bot with a side of management and automated tasking\nMade by @Myned#3985\n\nNSFW for Not Safe For Wumpus commands\n(G) for group commands\n@permission@ for required permissions\n!notice! for important information\np for prefix\n\n\{\} for mandatory argument\n[] for optional argument\n... for one or more arguments', help_attrs={'aliases': ['h']}, pm_help=None) + +@bot.command(help='help', brief='brief', description='description', usage='usage', hidden=True) +async def test(ctx): + pass + +# Send and print ready message to #testing and console after logon + + +@bot.event +async def on_ready(): + if not checks.ready: + # d.opus.load_opus('opuslib') + + from cogs import booru, info, management, music, owner, tools + + for cog in (tools.Utils(bot), owner.Bot(bot), owner.Tools(bot), management.Administration(bot), music.Music(bot), info.Info(bot), booru.MsG(bot)): + bot.add_cog(cog) + print(f'COG : {type(cog).__name__}') + + # bot.loop.create_task(u.clear(booru.temp_urls, 30*60)) + + if u.config['playing'] is not '': + await bot.change_presence(game=d.Game(name=u.config['playing'])) + + print('\n> > > > > > > > >\nC O N N E C T E D : {}\n> > > > > > > > >\n'.format(bot.user.name)) + await bot.get_channel(u.config['info_channel']).send(f'**Started** \N{BLACK SUN WITH RAYS} `{"` or `".join(u.config["prefix"])}`') + # u.notify('C O N N E C T E D') + + if u.temp['startup']: + with suppress(err.NotFound): + if u.temp['startup'][0] == 'guild': + dest = bot.get_channel(u.temp['startup'][1]) + else: + dest = bot.get_user(u.temp['startup'][1]) + message = await dest.get_message(u.temp['startup'][2]) + + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + u.temp['startup'] = () + u.dump(u.temp, 'temp/temp.pkl') + + checks.ready = True + else: + print('\n- - - -\nI N F O : reconnected, skipping initialization\n- - - -') + + +@bot.event +async def on_message(message): + if not u.config['selfbot']: + if message.author is not bot.user and not message.author.bot: + await bot.process_commands(message) + else: + if not message.author.bot: + await bot.process_commands(message) + + +@bot.event +async def on_error(error, *args, **kwargs): + print('\n! ! ! ! !\nE R R O R : {}\n! ! ! ! !\n'.format(error), file=sys.stderr) + tb.print_exc() + await bot.get_user(u.config['owner_id']).send('**ERROR** \N{WARNING SIGN}\n```\n{}```'.format(error)) + await bot.get_channel(u.config['info_channel']).send('**ERROR** \N{WARNING SIGN}\n```\n{}```'.format(error)) + + if u.temp['startup']: + with suppress(err.NotFound): + if u.temp['startup'][0] == 'guild': + dest = bot.get_channel(u.temp['startup'][1]) + else: + dest = bot.get_user(u.temp['startup'][1]) + message = await dest.get_message(u.temp['startup'][2]) + + await message.add_reaction('\N{WARNING SIGN}') + + u.temp.clear() + u.dump(u.temp, 'temp/temp.pkl') + # u.notify('E R R O R') + await bot.logout() + u.close(bot.loop) + + +@bot.event +async def on_command_error(ctx, error): + if isinstance(error, err.NotFound): + print('NOT FOUND') + elif isinstance(error, errext.CheckFailure): + await ctx.send('**Insufficient permissions**', delete_after=10) + await ctx.message.add_reaction('\N{NO ENTRY}') + elif isinstance(error, errext.CommandNotFound): + print('INVALID COMMAND : {}'.format(error), file=sys.stderr) + await ctx.message.add_reaction('\N{BLACK QUESTION MARK ORNAMENT}') + else: + print('\n! ! ! ! ! ! ! ! ! ! ! !\nC O M M A N D E R R O R : {}\n! ! ! ! ! ! ! ! ! ! ! !\n'.format( + error), file=sys.stderr) + tb.print_exception(type(error), error, error.__traceback__, file=sys.stderr) + await bot.get_user(u.config['owner_id']).send('**COMMAND ERROR** \N{WARNING SIGN} `{}` from {} in {}\n```\n{}```'.format(ctx.message.content, ctx.author.mention, ctx.channel.mention, ''.join(tb.format_exception(type(error), error, error.__traceback__ if len(str(error.__traceback__)) < 1500 else str(error.__traceback__)[:1500])))) + await bot.get_channel(u.config['info_channel']).send('**COMMAND ERROR** \N{WARNING SIGN} `{}` from {} in {}\n```\n{}```'.format(ctx.message.content, ctx.author.mention, ctx.channel.mention, error)) + await exc.send_error(ctx, error) + await ctx.message.add_reaction('\N{WARNING SIGN}') + # u.notify('C O M M A N D E R R O R') + +# @bot.event +# async def on_command(ctx): +# if ctx.guild.id in u.settings['del_resp']: +# pass + +@bot.event +async def on_command_completion(ctx): + with suppress(err.NotFound): + with suppress(AttributeError): + if ctx.guild.id in u.settings['del_ctx'] and ctx.me.permissions_in(ctx.channel).manage_messages and isinstance(ctx.message.channel, d.TextChannel): + await ctx.message.delete() + + await ctx.message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + for command in ('lastcommand', ',restart', ',die'): + if ctx.command.name == command: + return + + u.last_commands[ctx.author.id] = ctx + +@bot.event +async def on_guild_remove(guild): + print(f'LEFT : {guild.name}') + + for task, idents in u.tasks.items(): + for channel in guild.channels: + if channel.id in idents: + idents.remove(channel.id) + print(f'STOPPED : {task} in #{channel.id}') + u.dump(u.tasks, 'cogs/tasks.pkl') + + +async def wait(voice): + asyncio.sleep(5) + await voice.disconnect() + + +def after(voice, error): + coro = voice.disconnect() + future = asyncio.run_coroutine_threadsafe(coro, voice.loop) + future.result() + +# suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'None', 'tags': {}, 'total': 0}) +@bot.command(name=',test', hidden=True) +@cmds.is_owner() +async def test(ctx): + post = await u.fetch('https://e621.net/post/show.json?id=1145042', json=True) + + tags = [] + if post['tags']: + temptags = post['tags'].split(' ') + cis = [] + for tag in suggested: + pass + for tag in temptags: + tags.append(f'[{tag}](https://e621.net/post?tags={tag})') + # tags = ' '.join(tags) + else: + tags = 'None' + + if post['description']: + post_description = post['description'] if len(post['description']) < 200 else f'{post["description"][:200]}...' + else: + post_description = 'None' + + title = ', '.join(post['artist']) + description = f'posted by: *[{post["author"]}](https://e621.net/post?tags=user:{post["author"]})*' + url = f'https://e621.net/post?tags={",".join(post["artist"])}' + # timestamp = dt.utcnow() + color = ctx.me.color + footer = {'text': post['score'], 'icon_url': 'https://images-ext-1.discordapp.net/external/W2k0ZzhU7ngvN_-CdqAa3H3FmkfCNYQTxPG_DsvacB4/https/emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png'} + # image = 'https://e621.net/post/show/54360' + thumbnail = post['file_url'] + author = {'name': post['id'], 'url': f'https://e621.net/post/show/{post["id"]}', 'icon_url': ctx.author.avatar_url} + + fields = [] + names = ('File', 'Sources', 'Description', 'tags', 'tags (ext.)') + values = (f'[{post["md5"]}]({post["file_url"]}) | [{post["file_ext"]}](https://e621.net/post?tags=type:{post["file_ext"]})\n\n**Size** [{size(post["file_size"], system=alternative)}](https://e621.net/post?tags=filesize:{post["file_size"]})\n**Resolution** [{post["width"]} x {post["height"]}](https://e621.net/post?tags=width:{post["width"]},height:{post["height"]}) | [{u.get_aspectratio(post["width"], post["height"])}](https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f})', '\n'.join([f'[{urlparse(source).netloc}]({source})' for source in post['sources']]), post_description, ' '.join(tags[:20]), ' '.join(tags[20:])) + inlines = (False, False, False, True, True) + for name, value, inline in zip(names, values, inlines): + fields.append({'name': name, 'value': value, 'inline': inline}) + + embed = u.generate_embed(ctx, title=title, description=description, url=url, colour=color, footer=footer, thumbnail=thumbnail, author=author, fields=fields) + + await ctx.send(embed=embed) + # print(ctx.args) + # print(ctx.kwargs) + # if '<:N_:368917475531816962>' in message: + # await ctx.send('<:N_:368917475531816962>') + # logs = [] + # async for entry in ctx.guild.audit_logs(limit=None, action=d.AuditLogAction.message_delete): + # logs.append( + # f'@{entry.user.name} deleted {entry.extra.count} messages from @{entry.target.name} in #{entry.extra.channel.name}') + # pprint(logs) + # channel = bot.get_channel(int(cid)) + # voice = await channel.connect() + # voice.play(d.AudioSource, after=lambda: after(voice)) + +bot.run(u.config['token'], bot=not u.config['selfbot']) diff --git a/src/temp/__init__.py b/src/temp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/scraper.py b/src/utils/scraper.py index ccb60fe..d03990a 100644 --- a/src/utils/scraper.py +++ b/src/utils/scraper.py @@ -15,6 +15,7 @@ async def get_post(url): filesize = int(image.headers['Content-Length']) if filesize > 8192 * 1024: raise exc.SizeError(size(filesize, system=alternative)) + except ValueError: raise exc.MissingArgument diff --git a/src/utils/utils.py b/src/utils/utils.py index 4c865b0..ebb30d1 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -82,9 +82,10 @@ def dump(obj, filename, *, json=False): jsn.dump(obj, outfile, indent=4, sort_keys=True) -settings = setdefault('misc/settings.pkl', {'del_ctx': [], 'prefixes': {}}) -tasks = setdefault('cogs/tasks.pkl', {'auto_del': [], 'auto_rev': [], 'periodic_gpm': []}) +settings = setdefault('misc/settings.pkl', {'del_ctx': [], 'del_resp': [], 'prefixes': {}}) +tasks = setdefault('cogs/tasks.pkl', {'auto_del': [], 'auto_hrt': [], 'auto_rev': [], 'periodic_gpm': []}) temp = setdefault('temp/temp.pkl', {'startup': ()}) +secrets = setdefault('secrets.json', {'client_secrets': {'client_id': '', 'client_secret': ''}}, json=True) RATE_LIMIT = 2.2 color = d.Color(0x1A1A1A)