From 9f487cb57c5c13ec88206b0e92c1512ed0df6dad Mon Sep 17 00:00:00 2001 From: Myned Date: Sat, 31 Mar 2018 16:36:30 -0400 Subject: [PATCH] File map update --- Pipfile | 3 +- Pipfile.lock | 52 +- src/cogs/booru.py | 3322 ++++++++++++++++++++-------------------- src/cogs/management.py | 470 +++--- src/cogs/owner.py | 490 +++--- src/utils/utils.py | 382 ++--- 6 files changed, 2361 insertions(+), 2358 deletions(-) diff --git a/Pipfile b/Pipfile index 436d9ae..ae1d739 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,8 @@ url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" +[requires] +python_version = "3.6" [packages] @@ -25,4 +27,3 @@ pynacl = "*" [dev-packages] - diff --git a/Pipfile.lock b/Pipfile.lock index d1780c4..b492a8a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "19fd84259fea739bf9a47e9ea60f364a06fd636ae4eaf50d40683569f9830f7e" + "sha256": "c8426f63f07b00a7dcc5220e1210acfc6279bb1eccb40d9a884d34e203bf8f85" }, "host-environment-markers": { "implementation_name": "cpython", @@ -17,7 +17,9 @@ "sys_platform": "darwin" }, "pipfile-spec": 6, - "requires": {}, + "requires": { + "python_version": "3.6" + }, "sources": [ { "name": "pypi", @@ -29,30 +31,23 @@ "default": { "aiohttp": { "hashes": [ - "sha256:834f687b806fbf49cb135b5a208b5c27338e19c219d6e09e9049936e01e8bea8", - "sha256:6b8c5a00432b8a5a083792006e8fdfb558b8b10019ce254200855264d3a25895", - "sha256:7b407c22b0ab473ffe0a7d3231f2084a8ae80fdb64a31842b88d57d6b7bdab7c", - "sha256:14821eb8613bfab9118be3c55afc87bf4cef97689896fa0874c6877b117afbeb", - "sha256:8f32a4e157bad9c60ebc38c3bb93fcc907a020b017ddf8f7ab1580390e21940e", - "sha256:82a9068d9cb15eb2d99ecf39f8d56b4ed9f931a77a3622a0de747465fd2a7b96", - "sha256:7ac6378ae364d8e5e5260c7224ea4a1965cb6f4719f15d0552349d0b0cc93953", - "sha256:5a952d4af7de5f78dfb3206dbc352717890b37d447f0bbd4b5969b3c8bb713af", - "sha256:b25c7720c495048ed658086a29925ab485ac7ececf1b346f2b459e5431d85016", - "sha256:528b0b811b6260a79222b055664b82093d01f35fe5c82521d8659cb2b28b8044", - "sha256:46ace48789865a89992419205024ae451d577876f9919fbb0f22f71189822dea", - "sha256:5436ca0ed752bb05a399fc07dc86dc23c756db523a3b7d5da46a457eacf4c4b5", - "sha256:f5e7d41d924a1d5274060c467539ee0c4f3bab318c1671ad65abd91f6b637baf", - "sha256:a8c12f3184c7cad8f66cae6c945d2c97e598b0cb7afd655a5b9471475e67f30e", - "sha256:756fc336a29c551b02252685f01bc87116bc9b04bbd02c1a6b8a96b3c6ad713b", - "sha256:cf790e61c2af0278f39dcedad9a22532bf81fb029c2cd73b1ceba7bea062c908", - "sha256:44c9cf24e63576244c13265ef0786b56d6751f5fb722225ecc021d6ecf91b4d2", - "sha256:ef1a36a16e72b6689ce0a6c7fc6bd88837d8fef4590b16bd72817644ae1f414d", - "sha256:3a4cdb9ca87c099d8ba5eb91cb8f000b60c21f8c1b50c75e04e8777e903bd278", - "sha256:f72bb19cece43483171264584bbaaf8b97717d9c0f244d1ef4a51df1cdb34085", - "sha256:c77e29243a79e376a1b51d71a13df4a61bc54fd4d9597ce3790b8d82ec6eb44d", - "sha256:8adda6583ba438a4c70693374e10b60168663ffa6564c5c75d3c7a9055290964" + "sha256:2e8be4c46083ced9d9bc9ff4d77f31bfcd3e7486613f6138c5aa302d33ea54ed", + "sha256:4634dd3bbb68d0c7e5e4bca7571369d53c497b3300d9d678f939038e1b1231ee", + "sha256:25825c61688fc95e09d6be19e513e925cb4f08aae4d7a7c38a1fa75e0e4c22bd", + "sha256:9e6d6f0bca955923b515f8b5631c4c4f43aa152763852284cbefc89bd544069e", + "sha256:6eef1d7eff9e6fa1029f7a62504f88b2b0afce89ced5c95d3a4cf1c2faef1231", + "sha256:040eecbc37aa5bd007108388fab6c42b2a01b964c4feac26bdffc8fe8af6c110", + "sha256:53988a8cf76c3fb74a759e77b1c2f55ab36880d57c6e7d0d59ad28743a2535fe", + "sha256:d51673140330c660e68c182e14164ddba47810dca873bbd28662f31d7d8c0185", + "sha256:2fe26e836a1803c7414613c376fe29fc4ae0e5145e3813e1db1854cb05c91a3c", + "sha256:15ad4d76bddfd98bf9e48263c70f6603e96d823c5a5c0c842646e9871be72c64", + "sha256:7910089093296b5c8f683965044f553b0c5c9c2dbf310a219db76c6e793fea55", + "sha256:a19b96f77763ddf0249420438ebfc4d9a470daeb26f6614366d913ff520fa29b", + "sha256:b53bc7b44b1115af50bd18d9671972603e5a4934e98dd3f4d671104c070e331d", + "sha256:4b6fa00885ec778154244b010acecb862d277e6652b87fcd85c0f4735d26451c", + "sha256:7aee5c0750584946fde40da70f0b28fe769f85182f1171acef18a35fd8ecd221" ], - "version": "==2.3.10" + "version": "==3.0.1" }, "appdirs": { "hashes": [ @@ -68,6 +63,13 @@ ], "version": "==2.0.0" }, + "attrs": { + "hashes": [ + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" + ], + "version": "==17.4.0" + }, "beautifulsoup4": { "hashes": [ "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", diff --git a/src/cogs/booru.py b/src/cogs/booru.py index 42009b9..a22e9c2 100644 --- a/src/cogs/booru.py +++ b/src/cogs/booru.py @@ -1,1661 +1,1661 @@ -import asyncio -import json -import re -import sys -import traceback as tb -from contextlib import suppress -from datetime import datetime as dt -from datetime import timedelta as td -from fractions import gcd -import copy - -import discord as d -from discord import errors as err -from discord import reaction -from discord.ext import commands as cmds -from discord.ext.commands import errors as errext - -from cogs import tools -from misc import exceptions as exc -from misc import checks -from utils import utils as u -from utils import formatter, scraper - - -class MsG: - - def __init__(self, bot): - self.bot = bot - self.LIMIT = 100 - self.HISTORY_LIMIT = 150 - self.RATE_LIMIT = u.RATE_LIMIT - self.reversiqueue = asyncio.Queue() - self.heartqueue = asyncio.Queue() - self.reversifying = False - self.updating = False - self.hearting = False - - time = (dt.utcnow() - td(days=29)).strftime('%d/%m/%Y/%H:%M:%S') - self.suggested = u.setdefault('cogs/suggested.pkl', 7) - # self.suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'test', 'tags': {}, 'total': 1}) - print(self.suggested) - self.favorites = u.setdefault('cogs/favorites.pkl', {}) - self.blacklists = u.setdefault( - 'cogs/blacklists.pkl', {'global_blacklist': set(), 'guild_blacklist': {}, 'user_blacklist': {}}) - self.aliases = u.setdefault('cogs/aliases.pkl', {}) - - if not self.hearting: - self.hearting = True - self.bot.loop.create_task(self._send_hearts()) - print('STARTED : hearting') - if u.tasks['auto_rev']: - for channel in u.tasks['auto_rev']: - temp = self.bot.get_channel(channel) - self.bot.loop.create_task(self.queue_for_reversification(temp)) - print('STARTED : auto-reversifying in #{}'.format(temp.name)) - self.reversifying = True - self.bot.loop.create_task(self._reversify()) - if u.tasks['auto_hrt']: - for channel in u.tasks['auto_hrt']: - temp = self.bot.get_channel(channel) - self.bot.loop.create_task(self.queue_for_hearts(channel=temp)) - print(f'STARTED : auto-hearting in #{temp.name}') - # if not self.updating: - # self.updating = True - # self.bot.loop.create_task(self._update_suggested()) - - async def _update_suggested(self): - while self.updating: - print('Checking for tag updates...') - print(self.suggested) - - time = dt.utcnow() - last_update = dt.strptime(self.suggested['last_update'], '%d/%m/%Y/%H:%M:%S') - delta = time - last_update - print(delta.days) - - if delta.days < 30: - print('Up to date.') - else: - page = 1 - pages = len(list(self.suggested['tags'].keys())) - - print(f'Last updated: {self.suggested["last_update"]}') - print('Updating tags...') - - content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) - while content: - for tag in content: - self.suggested['tags'][tag['name']] = tag['count'] - self.suggested['total'] += tag['count'] - print(f' UPDATED : PAGE {page} / {pages}', end='\r') - - page += 1 - content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) - - u.dump(self.suggested, 'cogs/suggested.pkl') - self.suggested['last_update'] = time.strftime('%d/%m/%Y/%H:%M:%S') - - print('\nFinished updating tags.') - - await asyncio.sleep(24 * 60 * 60) - - def _get_favorites(self, ctx, args): - if '-f' in args or '-favs' in args or '-faves' in args or '-favorites' in args: - if self.favorites.get(ctx.author.id, {}).get('tags', set()): - args = ['~{}'.format(tag) - for tag in self.favorites[ctx.author.id]['tags']] - else: - raise exc.FavoritesNotFound - - return args - - def _get_score(self, score): - if score < 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/pouting-face_1f621.png' - elif score == 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/mozilla/36/pile-of-poo_1f4a9.png' - elif 10 > score > 0: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/white-medium-star_2b50.png' - elif 50 > score >= 10: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/glowing-star_1f31f.png' - elif 100 > score >= 50: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/dizzy-symbol_1f4ab.png' - elif score >= 100: - return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png' - return None - - async def _send_hearts(self): - while self.hearting: - temp = await self.heartqueue.get() - - if isinstance(temp[1], d.Embed): - await temp[0].send(embed=temp[1]) - - await asyncio.sleep(self.RATE_LIMIT) - elif isinstance(temp[1], d.Message): - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', temp[1].content): - await temp[0].send(match) - - await asyncio.sleep(self.RATE_LIMIT) - - for attachment in temp[1].attachments: - await temp[0].send(attachment.url) - await asyncio.sleep(self.RATE_LIMIT) - - print('STOPPED : hearting') - - async def queue_for_hearts(self, *, message=None, send=None, channel=None, reaction=True, timeout=60 * 60): - def on_reaction(reaction, user): - if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id and not user.bot: - raise exc.Save(user) - return False - def on_reaction_channel(reaction, user): - if reaction.message.channel.id == channel.id and not user.bot: - if reaction.emoji == '\N{OCTAGONAL SIGN}' and user.permissions_in(reaction.message.channel).administrator: - raise exc.Abort - if reaction.emoji == '\N{HEAVY BLACK HEART}' and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', reaction.message.content) or reaction.message.attachments): - raise exc.Save(user, reaction.message) - return False - - if message: - try: - if reaction: - await message.add_reaction('\N{HEAVY BLACK HEART}') - await asyncio.sleep(1) - - while self.hearting: - try: - await self.bot.wait_for('reaction_add', check=on_reaction, timeout=timeout) - - except exc.Save as e: - await self.heartqueue.put((e.user, send if send else message)) - - except asyncio.TimeoutError: - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - else: - try: - while self.hearting: - try: - await self.bot.wait_for('reaction_add', check=on_reaction_channel) - - except exc.Save as e: - await self.heartqueue.put((e.user, message)) - - except exc.Abort: - u.tasks['auto_hrt'].remove(channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - print('STOPPED : auto-hearting in #{}'.format(channel.name)) - await channel.send('**Stopped queueing messages for hearting in** {}'.format(channel.mention), delete_after=5) - - @cmds.command(name='autoheart', aliases=['autohrt']) - @cmds.has_permissions(administrator=True) - async def auto_heart(self, ctx): - try: - if ctx.channel.id not in u.tasks['auto_hrt']: - u.tasks['auto_hrt'].append(ctx.channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - self.bot.loop.create_task(self.queue_for_hearts(channel=ctx.channel)) - print('STARTED : auto-hearting in #{}'.format(ctx.channel.name)) - await ctx.send('**Auto-hearting all messages in {}**'.format(ctx.channel.mention), delete_after=5) - else: - raise exc.Exists - - except exc.Exists: - message = await ctx.send('**Already auto-hearting in {}.** React with \N{OCTAGONAL SIGN} to stop.'.format(ctx.channel.mention)) - await message.add_reaction('\N{OCTAGONAL SIGN}') - - # @cmds.command() - # async def auto_post(self, ctx): - # try: - # if ctx.channel.id not in u.tasks['auto_post']: - # u.tasks['auto_post'].append(ctx.channel.id) - # u.dump(u.tasks, 'cogs/tasks.pkl') - # self.bot.loop.create_task(self.queue_for_posting(ctx.channel)) - # if not self.posting: - # self.bot.loop.create_task(self._post()) - # self.posting = True - # - # print('STARTED : auto-posting in #{}'.format(ctx.channel.name)) - # await ctx.send('**Auto-posting all images in {}**'.format(ctx.channel.mention), delete_after=5) - # else: - # raise exc.Exists - # - # except exc.Exists: - # await ctx.send('**Already auto-posting in {}.** Type `stop` to stop.'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['tag', 't'], brief='(G) Get info on tags', description='Group command for obtaining info on tags\n\nUsage:\n\{p\}tag \{flag\} \{tag(s)\}') - async def tags(self, ctx): - pass - - # Tag search - @tags.command(name='related', aliases=['relate', 'rel', 'r'], brief='(tags) Search for related tags', description='Return related tags for given tag(s)\n\nExample:\n\{p\}tag related wolf') - async def _tags_related(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - related = [] - c = 0 - - await dest.trigger_typing() - - for tag in tags: - try: - tag_request = await u.fetch('https://e621.net/tag/related.json', params={'tags': tag}, json=True) - for rel in tag_request.get(tag, []): - related.append(rel[0]) - - if related: - await dest.send('`{}` **related tags:**\n```\n{}```'.format(tag, formatter.tostring(related))) - else: - await ctx.send(f'**No related tags found for:** `{tag}`', delete_after=7) - - related.clear() - c += 1 - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - # Tag aliases - @tags.command(name='aliases', aliases=['alias', 'als', 'a'], brief='(tags) Search for tag aliases', description='Return aliases for given tag(s)\n\nExample:\n\{p\}tag alias wolf') - async def _tags_aliases(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - aliases = [] - c = 0 - - await dest.trigger_typing() - - for tag in tags: - try: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - for dic in alias_request: - aliases.append(dic['name']) - - if aliases: - await dest.send('`{}` **aliases:**\n```\n{}```'.format(tag, formatter.tostring(aliases))) - else: - await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) - - aliases.clear() - c += 1 - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['g'], brief='(G) Get e621 elements', description='Group command for obtaining various elements like post info\n\nUsage:\n\{p\}get \{flag\} \{args\}') - async def get(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to get items.**\n*Type* `{}help get` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='info', aliases=['i'], brief='(get) Get info from post', description='Return info for given post URL or ID\n\nExample:\n\{p\}get info 1145042') - async def _get_info(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, posts = kwargs['destination'], kwargs['remaining'] - - if not posts: - raise exc.MissingArgument - - for ident in posts: - try: - await dest.trigger_typing() - - ident = ident if not ident.isdigit() else re.search( - 'show/([0-9]+)', ident).group(1) - post = await u.fetch('https://e621.net/post/show.json', params={'id': ident}, json=True) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_thumbnail(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - # except - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - except exc.MissingArgument: - await ctx.send('**Invalid url**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='image', aliases=['img'], brief='(get) Get direct image from post', description='Return direct image URL for given post\n\nExample:\n\{p\}get image 1145042') - async def _get_image(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, urls = kwargs['destination'], kwargs['remaining'] - c = 0 - - if not urls: - raise exc.MissingArgument - - for url in urls: - try: - await dest.trigger_typing() - - await dest.send(await scraper.get_image(url)) - - c += 1 - - # except - # await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) - - finally: - await asyncio.sleep(self.RATE_LIMIT) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.MissingArgument: - await ctx.send('**Invalid url or file**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @get.command(name='pool', aliases=['p'], brief='(get) Get pool from query', description='Return pool info for given query\n\nExample:\n\{p\}get pool 1145042') - async def _get_pool(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: - raise exc.Abort(match) - return False - - def on_message(msg): - return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, query = kwargs['destination'], kwargs['remaining'] - ident = None - - await dest.trigger_typing() - - pools = [] - pool_request = await u.fetch('https://e621.net/pool/index.json', params={'query': ' '.join(query)}, json=True) - if len(pool_request) > 1: - for pool in pool_request: - pools.append(pool['name']) - match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) - for future in done: - selection = future.result() - - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - await selection.delete() - elif pool_request: - tempool = pool_request[0] - else: - raise exc.NotFound - - await ctx.send(f'**{tempool["name"]}**\nhttps://e621.net/pool/show/{tempool["id"]}') - - except exc.Abort as e: - await e.message.edit(content='\N{NO ENTRY SIGN}', delete_after=7) - - # Reverse image searches a linked image using the public iqdb - @cmds.command(name='reverse', aliases=['rev', 'ris'], brief='Reverse image search from e621', description='NSFW\nReverse-search an image with given URL') - async def reverse(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, urls = kwargs['destination'], kwargs['remaining'] - c = 0 - - if not urls and not ctx.message.attachments: - raise exc.MissingArgument - - for attachment in ctx.message.attachments: - urls.append(attachment.url) - - for url in urls: - try: - await dest.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - await dest.send('**Probable match**', embed=embed) - - c += 1 - - except exc.MatchError as e: - await ctx.send('**No probable match for:** `{}`'.format(e), delete_after=7) - - if not c: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.MissingArgument: - await ctx.send('**Invalid url or file.** Be sure the link directs to an image file', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.SizeError as e: - await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.command(name='reversify', aliases=['revify', 'risify', 'rify']) - async def reversify(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=self.HISTORY_LIMIT / 5) - dest, remove, limit = kwargs['destination'], kwargs['remove'], kwargs['limit'] - links = {} - c = 0 - - if not ctx.author.permissions_in(ctx.channel).manage_messages: - dest = ctx.author - - async for message in ctx.channel.history(limit=self.HISTORY_LIMIT * limit): - if c >= limit: - break - if message.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content) is not None or message.embeds or message.attachments): - links[message] = [] - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): - links[message].append(match.group(0)) - for embed in message.embeds: - if embed.image.url is not d.Embed.Empty: - links[message].append(embed.image.url) - for attachment in message.attachments: - links[message].append(attachment.url) - - await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - c += 1 - - if not links: - raise exc.NotFound - - n = 1 - for message, urls in links.items(): - for url in urls: - try: - await dest.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - await dest.send(f'**Probable match from** {message.author.display_name}', embed=embed) - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - if remove: - with suppress(err.NotFound): - await message.delete() - - except exc.MatchError as e: - await ctx.send('`{} / {}` **No probable match for:** `{}`'.format(n, len(links), e), delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - except exc.SizeError as e: - await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - - finally: - n += 1 - - if c <= 0: - await ctx.message.add_reaction('\N{CROSS MARK}') - - except exc.NotFound: - await ctx.send('**No matches found**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **invalid limit.** Query limited to 30'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - async def _reversify(self): - while self.reversifying: - message = await self.reversiqueue.get() - urls = [] - - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): - urls.append(match.group(0)) - for embed in message.embeds: - if embed.image.url is not d.Embed.Empty: - urls.append(embed.image.url) - for attachment in message.attachments: - urls.append(attachment.url) - - for url in urls: - try: - await message.channel.trigger_typing() - - post = await scraper.get_post(url) - - embed = d.Embed( - title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=message.channel.guild.me.color if isinstance(message.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=f'{post["width"]} x {post["height"]}', - url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=message.author.avatar_url) - embed.set_footer(text=post['score'], - icon_url=self._get_score(post['score'])) - - await message.channel.send('**Probable match from** {}'.format(message.author.display_name), embed=embed) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - await asyncio.sleep(self.RATE_LIMIT) - - with suppress(err.NotFound): - await message.delete() - - except exc.MatchError as e: - await message.channel.send('**No probable match for:** `{}`'.format(e), delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - except exc.SizeError as e: - await message.channel.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) - await message.add_reaction('\N{CROSS MARK}') - except Exception: - await message.channel.send(f'**An unknown error occurred.**', delete_after=7) - await message.add_reaction('\N{WARNING SIGN}') - - print('STOPPED : reversifying') - - async def queue_for_reversification(self, channel): - def check(msg): - if 'stop r' in msg.content.lower() and msg.channel is channel and msg.author.guild_permissions.administrator: - raise exc.Abort - elif msg.channel is channel and msg.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', msg.content) is not None or msg.attachments or msg.embeds): - return True - return False - - try: - while self.reversifying: - message = await self.bot.wait_for('message', check=check) - await self.reversiqueue.put(message) - await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - except exc.Abort: - u.tasks['auto_rev'].remove(channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - if not u.tasks['auto_rev']: - self.reversifying = False - print('STOPPED : reversifying #{}'.format(channel.name)) - await channel.send('**Stopped queueing messages for reversification in** {}'.format(channel.mention), delete_after=5) - - @cmds.command(name='autoreversify', aliases=['autorev']) - @cmds.has_permissions(manage_channels=True) - async def auto_reversify(self, ctx): - if ctx.channel.id not in u.tasks['auto_rev']: - u.tasks['auto_rev'].append(ctx.channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - self.bot.loop.create_task( - self.queue_for_reversification(ctx.channel)) - if not self.reversifying: - self.bot.loop.create_task(self._reversify()) - self.reversifying = True - - print('STARTED : auto-reversifying in #{}'.format(ctx.channel.name)) - await ctx.send('**Auto-reversifying all images in** {}'.format(ctx.channel.mention), delete_after=5) - else: - await ctx.send('**Already auto-reversifying in {}.** Type `stop r(eversifying)` to stop.'.format(ctx.channel.mention), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - async def _get_pool(self, ctx, *, destination, booru='e621', query=[]): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: - raise exc.Abort(match) - return False - - def on_message(msg): - return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel - - posts = {} - pool = {} - - try: - pools = [] - pool_request = await u.fetch('https://{}.net/pool/index.json'.format(booru), params={'query': ' '.join(query)}, json=True) - if len(pool_request) > 1: - for pool in pool_request: - pools.append(pool['name']) - match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match.\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) - for future in done: - selection = future.result() - - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - await selection.delete() - pool = {'name': tempool['name'], 'id': tempool['id']} - - await destination.trigger_typing() - elif pool_request: - tempool = pool_request[0] - pool = {'name': pool_request[0] - ['name'], 'id': pool_request[0]['id']} - else: - raise exc.NotFound - - page = 1 - while len(posts) < tempool['post_count']: - posts_request = await u.fetch('https://{}.net/pool/show.json'.format(booru), params={'id': tempool['id'], 'page': page}, json=True) - for post in posts_request['posts']: - posts[post['id']] = {'artist': ', '.join( - post['artist']), 'file_url': post['file_url'], 'score': post['score']} - page += 1 - - return pool, posts - - except exc.Abort as e: - await e.message.edit(content='\N{NO ENTRY SIGN}') - raise exc.Continue - - # Messy code that checks image limit and tags in blacklists - async def _get_posts(self, ctx, *, booru='e621', tags=[], limit=1, previous={}): - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - blacklist = set() - # Creates temp blacklist based on context - for bl in (self.blacklists['global_blacklist'], self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set()), self.blacklists['user_blacklist'].get(ctx.author.id, set())): - for tag in bl: - blacklist.update([tag] + list(self.aliases[tag])) - # Checks for, assigns, and removes first order in tags if possible - order = [tag for tag in tags if 'order:' in tag] - if order: - order = order[0] - tags.remove(order) - else: - order = 'order:random' - # Checks if tags are in local blacklists - if tags: - if (len(tags) > 5 and booru == 'e621') or (len(tags) > 4 and booru == 'e926'): - raise exc.TagBoundsError(formatter.tostring(tags[5:])) - for tag in tags: - if tag == 'swf' or tag == 'webm' or tag in blacklist: - raise exc.TagBlacklisted(tag) - - # Checks for blacklisted tags in endpoint blacklists - try/except is for continuing the parent loop - posts = {} - temposts = len(posts) - empty = 0 - c = 0 - while len(posts) < limit: - if c == limit * 5 + (self.LIMIT / 5): - raise exc.Timeout - request = await u.fetch('https://{}.net/post/index.json'.format(booru), params={'tags': ','.join([order] + tags), 'limit': int(self.LIMIT * limit)}, json=True) - if len(request) == 0: - raise exc.NotFound(formatter.tostring(tags)) - if len(request) < limit: - limit = len(request) - - for post in request: - if 'swf' in post['file_ext'] or 'webm' in post['file_ext']: - continue - try: - for tag in blacklist: - if tag in post['tags']: - raise exc.Continue - except exc.Continue: - continue - if post['id'] not in posts.keys() and post['id'] not in previous.keys(): - posts[post['id']] = {'artist': ', '.join( - post['artist']), 'file_url': post['file_url'], 'score': post['score']} - if len(posts) == limit: - break - - if len(posts) == temposts: - empty += 1 - if empty == 5: - break - else: - empty = 0 - temposts = len(posts) - c += 1 - - if posts: - return posts, order - else: - raise exc.NotFound(formatter.tostring(tags)) - - # Creates reaction-based paginator for linked pools - @cmds.command(name='poolpage', aliases=['poolp', 'pp', 'e621pp', 'e6pp', '6pp'], brief='e621 pool paginator', description='e621 | NSFW\nShow pools in a page format') - async def pool_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, query = kwargs['destination'], kwargs['remaining'] - hearted = {} - c = 1 - - await dest.trigger_typing() - - pool, posts = await self._get_pool(ctx, destination=dest, booru='e621', query=query) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=dest.me.color if isinstance(dest.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=pool['name'], - url='https://e621.net/pool/show?id={}'.format(pool['id']), icon_url=ctx.author.avatar_url) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted: - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content='\N{INPUT SYMBOL FOR NUMBERS}') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{WHITE HEAVY CHECK MARK}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound: - await ctx.send('**Pool not found**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Continue: - pass - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - @cmds.command(name='e621page', aliases=['e621p', 'e6p', '6p']) - @checks.is_nsfw() - async def e621_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - limit = self.LIMIT / 5 - hearted = {} - c = 1 - - tags = self._get_favorites(ctx, tags) - - await ctx.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted.keys(): - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content=f'`{c} / {len(posts)}`') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - try: - if c % limit == 0: - await dest.trigger_typing() - temposts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit, previous=posts) - posts.update(temposts) - - keys = list(posts.keys()) - values = list(posts.values()) - - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.NotFound: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - # @e621_paginator.error - # async def e621_paginator_error(self, ctx, error): - # if isinstance(error, exc.NSFW): - # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{NO ENTRY}') - - @cmds.command(name='e926page', aliases=['e926p', 'e9p', '9p']) - async def e926_paginator(self, ctx, *args): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): - raise exc.Abort - elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel - - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - limit = self.LIMIT / 5 - hearted = {} - c = 1 - - tags = self._get_favorites(ctx, tags) - - await ctx.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e926.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=values[c - 1]['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e926.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - - paginator = await dest.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - while not self.bot.is_closed(): - try: - await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) - - except exc.Save: - if keys[c - 1] not in hearted: - hearted[keys[c - 1]] = copy.deepcopy(embed) - - await paginator.edit(content='\N{HEAVY BLACK HEART}') - else: - del hearted[keys[c - 1]] - - await paginator.edit(content='\N{BROKEN HEART}') - - except exc.Left: - if c > 1: - c -= 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') - - except exc.GoTo: - await paginator.edit(content=f'`{c} / {len(posts)}`') - number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await number.delete() - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - - except exc.Right: - try: - if c % limit == 0: - await dest.trigger_typing() - temposts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit, previous=posts) - posts.update(temposts) - - keys = list(posts.keys()) - values = list(posts.values()) - - if c < len(keys): - c += 1 - embed.title = values[c - 1]['artist'] - embed.url = 'https://e926.net/post/show/{}'.format( - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_score(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) - else: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.NotFound: - await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') - - except exc.Abort: - try: - await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') - except UnboundLocalError: - await dest.send('\N{WHITE HEAVY CHECK MARK}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await dest.send('\N{HOURGLASS}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - finally: - if hearted: - await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await asyncio.sleep(self.RATE_LIMIT) - - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - # Searches for and returns images from e621.net given tags when not blacklisted - @cmds.command(aliases=['e6', '6'], brief='e621 | NSFW', description='e621 | NSFW\nTag-based search for e621.net\n\nYou can only search 5 tags and 6 images at once for now.\ne6 [tags...] ([# of images])') - @checks.is_nsfw() - async def e621(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=3) - dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] - - tags = self._get_favorites(ctx, args) - - await dest.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) - - for ident, post in posts.items(): - embed = d.Embed(title=post['artist'], url='https://e621.net/post/show/{}'.format(ident), - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - message = await dest.send(embed=embed) - - self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) - - except exc.TagBlacklisted as e: - await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - # @e621.error - # async def e621_error(self, ctx, error): - # if isinstance(error, exc.NSFW): - # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) - # await ctx.message.add_reaction('\N{NO ENTRY}') - - # Searches for and returns images from e926.net given tags when not blacklisted - @cmds.command(aliases=['e9', '9'], brief='e926 | SFW', description='e926 | SFW\nTag-based search for e926.net\n\nYou can only search 5 tags and 6 images at once for now.\ne9 [tags...] ([# of images])') - async def e926(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args, limit=3) - dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] - - tags = self._get_favorites(ctx, args) - - await dest.trigger_typing() - - posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) - - for ident, post in posts.items(): - embed = d.Embed(title=post['artist'], url='https://e926.net/post/show/{}'.format(ident), - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=post['file_url']) - embed.set_author(name=formatter.tostring(tags, order=order), - url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_score(post['score'])) - - message = await dest.send(embed=embed) - - self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) - - except exc.TagBlacklisted as e: - await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - @cmds.group(aliases=['fave', 'fav', 'f']) - async def favorite(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to manage favorites.**\n*Type* `{}help fav` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @favorite.error - async def favorite_error(self, ctx, error): - pass - - @favorite.group(name='get', aliases=['g']) - async def _get_favorite(self, ctx): - pass - - @_get_favorite.command(name='tags', aliases=['t']) - async def __get_favorite_tags(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{WHITE MEDIUM STAR} {}**\'s favorite tags:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.favorites.get(ctx.author.id, {}).get('tags', set()))), delete_after=7) - - @_get_favorite.command(name='posts', aliases=['p']) - async def __get_favorite_posts(self, ctx): - pass - - @favorite.group(name='add', aliases=['a']) - async def _add_favorite(self, ctx): - pass - - @_add_favorite.command(name='tags', aliases=['t']) - async def __add_favorite_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - if tag in self.blacklists['user_blacklist'].get(ctx.author.id, set()): - raise exc.TagBlacklisted(tag) - with suppress(KeyError): - if len(self.favorites[ctx.author.id]['tags']) + len(tags) > 5: - raise exc.BoundsError - - self.favorites.setdefault(ctx.author.id, {}).setdefault( - 'tags', set()).update(tags) - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{} **added to their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except exc.BoundsError: - await ctx.send('**Favorites list currently limited to:** `5`', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**', delete_after=7) - await ctx.message.add_reaction('\N{NO ENTRY SIGN}') - - @_add_favorite.command(name='posts', aliases=['p']) - async def __add_favorite_posts(self, ctx, *posts): - pass - - @favorite.group(name='remove', aliases=['r']) - async def _remove_favorite(self, ctx): - pass - - @_remove_favorite.command(name='tags', aliases=['t']) - async def __remove_favorite_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.favorites[ctx.author.id].get( - 'tags', set()).remove(tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{} **removed from their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except KeyError: - await ctx.send('**You do not have any favorites**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except exc.TagError as e: - await ctx.send('`{}` **not in favorites**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_favorite.command(name='posts', aliases=['p']) - async def __remove_favorite_posts(self, ctx): - pass - - @favorite.group(name='clear', aliases=['c']) - async def _clear_favorite(self, ctx): - pass - - @_clear_favorite.command(name='tags', aliases=['t']) - async def __clear_favorite_tags(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - with suppress(KeyError): - del self.favorites[ctx.author.id] - u.dump(self.favorites, 'cogs/favorites.pkl') - - await dest.send('{}**\'s favorites cleared**'.format(ctx.author.mention), delete_after=5) - - @_clear_favorite.command(name='posts', aliases=['p']) - async def __clear_favorite_posts(self, ctx): - pass - - # Umbrella command structure to manage global, channel, and user blacklists - @cmds.group(aliases=['bl', 'b'], brief='(G) Manage blacklists', description='Manage channel or personal blacklists\n\nUsage:\n\{p\}bl get \{blacklist\} to show a blacklist\n\{p\}bl clear \{blacklist\} to clear a blacklist\n\{p\}bl add \{blacklist\} \{tags...\} to add tag(s) to a blacklist\n\{p\}bl remove \{blacklist\} \{tags...\} to remove tags from a blacklist') - async def blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Use a flag to manage blacklists.**\n*Type* `{}help bl` *for more info.*'.format(ctx.prefix), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - # @blacklist.error - # async def blacklist_error(self, ctx, error): - # if isinstance(error, KeyError): - # return await ctx.send('**Blacklist does not exist**', delete_after=7) - - @blacklist.group(name='get', aliases=['g'], brief='(G) Get a blacklist\n\nUsage:\n\{p\}bl get \{blacklist\}') - async def _get_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_get_blacklist.command(name='global', aliases=['gl', 'g'], brief='Get current global blacklist', description='Get current global blacklist\n\nThis applies to all booru commands, in accordance with Discord\'s ToS agreement\n\nExample:\n\{p\}bl get global') - async def __get_global_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} **Global blacklist:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']))) - - @_get_blacklist.command(name='channel', aliases=['ch', 'c'], brief='Get current channel blacklist', description='Get current channel blacklist\n\nThis is based on context - the channel where the command was executed\n\nExample:\{p\}bl get channel') - async def __get_channel_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) - - @_get_blacklist.command(name='me', aliases=['m'], brief='Get your personal blacklist', description='Get your personal blacklist\n\nYour blacklist is not viewable by anyone but you, except if you call this command in a public channel. The blacklist will be deleted soon after for your privacy\n\nExample:\n\{p\}bl get me') - async def __get_user_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} {}**\'s blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.blacklists['user_blacklist'].get(ctx.author.id, set()))), delete_after=7) - - @_get_blacklist.command(name='here', aliases=['h'], brief='Get current global and channel blacklists', description='Get current global and channel blacklists in a single message\n\nExample:\{p\}bl get here') - async def __get_here_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} **__Blacklisted:__**\n\n**Global:**\n```\n{}```\n**{}:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']), ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) - - @_get_blacklist.group(name='all', aliases=['a'], hidden=True) - async def __get_all_blacklists(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**') - await ctx.message.add_reaction('\N{CROSS MARK}') - - @__get_all_blacklists.command(name='guild', aliases=['g']) - @cmds.has_permissions(manage_channels=True) - async def ___get_all_guild_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.send('\N{NO ENTRY SIGN} **__{} blacklists:__**\n\n{}'.format(guild.name, formatter.dict_tostring(self.blacklists['guild_blacklist'].get(guild.id, {})))) - - @__get_all_blacklists.command(name='user', aliases=['u', 'member', 'm']) - @cmds.is_owner() - async def ___get_all_user_blacklists(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - await dest.send('\N{NO ENTRY SIGN} **__User blacklists:__**\n\n{}'.format(formatter.dict_tostring(self.blacklists['user_blacklist']))) - - @blacklist.group(name='add', aliases=['a'], brief='(G) Add tag(s) to a blacklist\n\nUsage:\n\{p\}bl add \{blacklist\} \{tags...\}') - async def _add_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_add_tags.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __add_global_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - await dest.trigger_typing() - - self.blacklists['global_blacklist'].update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('**Added to global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) - - @_add_tags.command(name='channel', aliases=['ch', 'c'], brief='@manage_channel@ Add tag(s) to the current channel blacklist (requires manage_channel)', description='Add tag(s) to the current channel blacklist ') - @cmds.has_permissions(manage_channels=True) - async def __add_channel_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - await dest.trigger_typing() - - self.blacklists['guild_blacklist'].setdefault( - guild.id, {}).setdefault(ctx.channel.id, set()).update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('**Added to** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags)), delete_after=5) - - @_add_tags.command(name='me', aliases=['m']) - async def __add_user_tags(self, ctx, *args): - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - await dest.trigger_typing() - - self.blacklists['user_blacklist'].setdefault( - ctx.author.id, set()).update(tags) - for tag in tags: - alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) - if alias_request: - for dic in alias_request: - self.aliases.setdefault(tag, set()).add(dic['name']) - else: - self.aliases.setdefault(tag, set()) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - u.dump(self.aliases, 'cogs/aliases.pkl') - - await dest.send('{} **added to their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - @blacklist.group(name='remove', aliases=['rm', 'r']) - async def _remove_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __remove_global_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.blacklists['global_blacklist'].remove(tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Removed from global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='channel', aliases=['ch', 'c']) - @cmds.has_permissions(manage_channels=True) - async def __remove_channel_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - for tag in tags: - try: - self.blacklists['guild_blacklist'][guild.id][ctx.channel.id].remove( - tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Removed from** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags), delete_after=5)) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_remove_tags.command(name='me', aliases=['m']) - async def __remove_user_tags(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - dest, tags = kwargs['destination'], kwargs['remaining'] - - for tag in tags: - try: - self.blacklists['user_blacklist'][ctx.author.id].remove( - tag) - - except KeyError: - raise exc.TagError(tag) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{} **removed from their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) - - except exc.TagError as e: - await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @blacklist.group(name='clear', aliases=['cl', 'c']) - async def _clear_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('**Invalid blacklist**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_clear_blacklist.command(name='global', aliases=['gl', 'g']) - @cmds.is_owner() - async def __clear_global_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - self.blacklists['global_blacklist'].clear() - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('**Global blacklist cleared**', delete_after=5) - - @_clear_blacklist.command(name='channel', aliases=['ch', 'c']) - @cmds.has_permissions(manage_channels=True) - async def __clear_channel_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - guild = ctx.guild if isinstance( - ctx.guild, d.Guild) else ctx.channel - - with suppress(KeyError): - del self.blacklists['guild_blacklist'][guild.id][ctx.channel.id] - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{} **blacklist cleared**'.format(ctx.channel.mention), delete_after=5) - - @_clear_blacklist.command(name='me', aliases=['m']) - async def __clear_user_blacklist(self, ctx, *args): - dest = u.get_kwargs(ctx, args)['destination'] - - with suppress(KeyError): - del self.blacklists['user_blacklist'][ctx.author.id] - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await dest.send('{}**\'s blacklist cleared**'.format(ctx.author.mention), delete_after=5) +import asyncio +import json +import re +import sys +import traceback as tb +from contextlib import suppress +from datetime import datetime as dt +from datetime import timedelta as td +from fractions import gcd +import copy + +import discord as d +from discord import errors as err +from discord import reaction +from discord.ext import commands as cmds +from discord.ext.commands import errors as errext + +from cogs import tools +from misc import exceptions as exc +from misc import checks +from utils import utils as u +from utils import formatter, scraper + + +class MsG: + + def __init__(self, bot): + self.bot = bot + self.LIMIT = 100 + self.HISTORY_LIMIT = 150 + self.RATE_LIMIT = u.RATE_LIMIT + self.reversiqueue = asyncio.Queue() + self.heartqueue = asyncio.Queue() + self.reversifying = False + self.updating = False + self.hearting = False + + time = (dt.utcnow() - td(days=29)).strftime('%d/%m/%Y/%H:%M:%S') + self.suggested = u.setdefault('cogs/suggested.pkl', 7) + # self.suggested = u.setdefault('cogs/suggested.pkl', {'last_update': 'test', 'tags': {}, 'total': 1}) + print(self.suggested) + self.favorites = u.setdefault('cogs/favorites.pkl', {}) + self.blacklists = u.setdefault( + 'cogs/blacklists.pkl', {'global_blacklist': set(), 'guild_blacklist': {}, 'user_blacklist': {}}) + self.aliases = u.setdefault('cogs/aliases.pkl', {}) + + if not self.hearting: + self.hearting = True + self.bot.loop.create_task(self._send_hearts()) + print('STARTED : hearting') + if u.tasks['auto_rev']: + for channel in u.tasks['auto_rev']: + temp = self.bot.get_channel(channel) + self.bot.loop.create_task(self.queue_for_reversification(temp)) + print('STARTED : auto-reversifying in #{}'.format(temp.name)) + self.reversifying = True + self.bot.loop.create_task(self._reversify()) + if u.tasks['auto_hrt']: + for channel in u.tasks['auto_hrt']: + temp = self.bot.get_channel(channel) + self.bot.loop.create_task(self.queue_for_hearts(channel=temp)) + print(f'STARTED : auto-hearting in #{temp.name}') + # if not self.updating: + # self.updating = True + # self.bot.loop.create_task(self._update_suggested()) + + async def _update_suggested(self): + while self.updating: + print('Checking for tag updates...') + print(self.suggested) + + time = dt.utcnow() + last_update = dt.strptime(self.suggested['last_update'], '%d/%m/%Y/%H:%M:%S') + delta = time - last_update + print(delta.days) + + if delta.days < 30: + print('Up to date.') + else: + page = 1 + pages = len(list(self.suggested['tags'].keys())) + + print(f'Last updated: {self.suggested["last_update"]}') + print('Updating tags...') + + content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) + while content: + for tag in content: + self.suggested['tags'][tag['name']] = tag['count'] + self.suggested['total'] += tag['count'] + print(f' UPDATED : PAGE {page} / {pages}', end='\r') + + page += 1 + content = await u.fetch('https://e621.net/tag/index.json', params={'order': 'count', 'limit': 500, 'page': page}, json=True) + + u.dump(self.suggested, 'cogs/suggested.pkl') + self.suggested['last_update'] = time.strftime('%d/%m/%Y/%H:%M:%S') + + print('\nFinished updating tags.') + + await asyncio.sleep(24 * 60 * 60) + + def _get_favorites(self, ctx, args): + if '-f' in args or '-favs' in args or '-faves' in args or '-favorites' in args: + if self.favorites.get(ctx.author.id, {}).get('tags', set()): + args = ['~{}'.format(tag) + for tag in self.favorites[ctx.author.id]['tags']] + else: + raise exc.FavoritesNotFound + + return args + + def _get_score(self, score): + if score < 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/pouting-face_1f621.png' + elif score == 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/mozilla/36/pile-of-poo_1f4a9.png' + elif 10 > score > 0: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/white-medium-star_2b50.png' + elif 50 > score >= 10: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/glowing-star_1f31f.png' + elif 100 > score >= 50: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/dizzy-symbol_1f4ab.png' + elif score >= 100: + return 'https://emojipedia-us.s3.amazonaws.com/thumbs/320/twitter/103/sparkles_2728.png' + return None + + async def _send_hearts(self): + while self.hearting: + temp = await self.heartqueue.get() + + if isinstance(temp[1], d.Embed): + await temp[0].send(embed=temp[1]) + + await asyncio.sleep(self.RATE_LIMIT) + elif isinstance(temp[1], d.Message): + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', temp[1].content): + await temp[0].send(match) + + await asyncio.sleep(self.RATE_LIMIT) + + for attachment in temp[1].attachments: + await temp[0].send(attachment.url) + await asyncio.sleep(self.RATE_LIMIT) + + print('STOPPED : hearting') + + async def queue_for_hearts(self, *, message=None, send=None, channel=None, reaction=True, timeout=60 * 60): + def on_reaction(reaction, user): + if reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == message.id and not user.bot: + raise exc.Save(user) + return False + def on_reaction_channel(reaction, user): + if reaction.message.channel.id == channel.id and not user.bot: + if reaction.emoji == '\N{OCTAGONAL SIGN}' and user.permissions_in(reaction.message.channel).administrator: + raise exc.Abort + if reaction.emoji == '\N{HEAVY BLACK HEART}' and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', reaction.message.content) or reaction.message.attachments): + raise exc.Save(user, reaction.message) + return False + + if message: + try: + if reaction: + await message.add_reaction('\N{HEAVY BLACK HEART}') + await asyncio.sleep(1) + + while self.hearting: + try: + await self.bot.wait_for('reaction_add', check=on_reaction, timeout=timeout) + + except exc.Save as e: + await self.heartqueue.put((e.user, send if send else message)) + + except asyncio.TimeoutError: + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + else: + try: + while self.hearting: + try: + await self.bot.wait_for('reaction_add', check=on_reaction_channel) + + except exc.Save as e: + await self.heartqueue.put((e.user, message)) + + except exc.Abort: + u.tasks['auto_hrt'].remove(channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + print('STOPPED : auto-hearting in #{}'.format(channel.name)) + await channel.send('**Stopped queueing messages for hearting in** {}'.format(channel.mention), delete_after=5) + + @cmds.command(name='autoheart', aliases=['autohrt']) + @cmds.has_permissions(administrator=True) + async def auto_heart(self, ctx): + try: + if ctx.channel.id not in u.tasks['auto_hrt']: + u.tasks['auto_hrt'].append(ctx.channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + self.bot.loop.create_task(self.queue_for_hearts(channel=ctx.channel)) + print('STARTED : auto-hearting in #{}'.format(ctx.channel.name)) + await ctx.send('**Auto-hearting all messages in {}**'.format(ctx.channel.mention), delete_after=5) + else: + raise exc.Exists + + except exc.Exists: + message = await ctx.send('**Already auto-hearting in {}.** React with \N{OCTAGONAL SIGN} to stop.'.format(ctx.channel.mention)) + await message.add_reaction('\N{OCTAGONAL SIGN}') + + # @cmds.command() + # async def auto_post(self, ctx): + # try: + # if ctx.channel.id not in u.tasks['auto_post']: + # u.tasks['auto_post'].append(ctx.channel.id) + # u.dump(u.tasks, 'cogs/tasks.pkl') + # self.bot.loop.create_task(self.queue_for_posting(ctx.channel)) + # if not self.posting: + # self.bot.loop.create_task(self._post()) + # self.posting = True + # + # print('STARTED : auto-posting in #{}'.format(ctx.channel.name)) + # await ctx.send('**Auto-posting all images in {}**'.format(ctx.channel.mention), delete_after=5) + # else: + # raise exc.Exists + # + # except exc.Exists: + # await ctx.send('**Already auto-posting in {}.** Type `stop` to stop.'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['tag', 't'], brief='(G) Get info on tags', description='Group command for obtaining info on tags\n\nUsage:\n\{p\}tag \{flag\} \{tag(s)\}') + async def tags(self, ctx): + pass + + # Tag search + @tags.command(name='related', aliases=['relate', 'rel', 'r'], brief='(tags) Search for related tags', description='Return related tags for given tag(s)\n\nExample:\n\{p\}tag related wolf') + async def _tags_related(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + related = [] + c = 0 + + await dest.trigger_typing() + + for tag in tags: + try: + tag_request = await u.fetch('https://e621.net/tag/related.json', params={'tags': tag}, json=True) + for rel in tag_request.get(tag, []): + related.append(rel[0]) + + if related: + await dest.send('`{}` **related tags:**\n```\n{}```'.format(tag, formatter.tostring(related))) + else: + await ctx.send(f'**No related tags found for:** `{tag}`', delete_after=7) + + related.clear() + c += 1 + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + # Tag aliases + @tags.command(name='aliases', aliases=['alias', 'als', 'a'], brief='(tags) Search for tag aliases', description='Return aliases for given tag(s)\n\nExample:\n\{p\}tag alias wolf') + async def _tags_aliases(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + aliases = [] + c = 0 + + await dest.trigger_typing() + + for tag in tags: + try: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + for dic in alias_request: + aliases.append(dic['name']) + + if aliases: + await dest.send('`{}` **aliases:**\n```\n{}```'.format(tag, formatter.tostring(aliases))) + else: + await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) + + aliases.clear() + c += 1 + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['g'], brief='(G) Get e621 elements', description='Group command for obtaining various elements like post info\n\nUsage:\n\{p\}get \{flag\} \{args\}') + async def get(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to get items.**\n*Type* `{}help get` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='info', aliases=['i'], brief='(get) Get info from post', description='Return info for given post URL or ID\n\nExample:\n\{p\}get info 1145042') + async def _get_info(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, posts = kwargs['destination'], kwargs['remaining'] + + if not posts: + raise exc.MissingArgument + + for ident in posts: + try: + await dest.trigger_typing() + + ident = ident if not ident.isdigit() else re.search( + 'show/([0-9]+)', ident).group(1) + post = await u.fetch('https://e621.net/post/show.json', params={'id': ident}, json=True) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_thumbnail(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + # except + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + except exc.MissingArgument: + await ctx.send('**Invalid url**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='image', aliases=['img'], brief='(get) Get direct image from post', description='Return direct image URL for given post\n\nExample:\n\{p\}get image 1145042') + async def _get_image(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, urls = kwargs['destination'], kwargs['remaining'] + c = 0 + + if not urls: + raise exc.MissingArgument + + for url in urls: + try: + await dest.trigger_typing() + + await dest.send(await scraper.get_image(url)) + + c += 1 + + # except + # await ctx.send(f'**No aliases found for:** `{tag}`', delete_after=7) + + finally: + await asyncio.sleep(self.RATE_LIMIT) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.MissingArgument: + await ctx.send('**Invalid url or file**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @get.command(name='pool', aliases=['p'], brief='(get) Get pool from query', description='Return pool info for given query\n\nExample:\n\{p\}get pool 1145042') + async def _get_pool(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: + raise exc.Abort(match) + return False + + def on_message(msg): + return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, query = kwargs['destination'], kwargs['remaining'] + ident = None + + await dest.trigger_typing() + + pools = [] + pool_request = await u.fetch('https://e621.net/pool/index.json', params={'query': ' '.join(query)}, json=True) + if len(pool_request) > 1: + for pool in pool_request: + pools.append(pool['name']) + match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) + for future in done: + selection = future.result() + + await match.delete() + tempool = [pool for pool in pool_request if pool['name'] + == pools[int(selection.content) - 1]][0] + await selection.delete() + elif pool_request: + tempool = pool_request[0] + else: + raise exc.NotFound + + await ctx.send(f'**{tempool["name"]}**\nhttps://e621.net/pool/show/{tempool["id"]}') + + except exc.Abort as e: + await e.message.edit(content='\N{NO ENTRY SIGN}', delete_after=7) + + # Reverse image searches a linked image using the public iqdb + @cmds.command(name='reverse', aliases=['rev', 'ris'], brief='Reverse image search from e621', description='NSFW\nReverse-search an image with given URL') + async def reverse(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, urls = kwargs['destination'], kwargs['remaining'] + c = 0 + + if not urls and not ctx.message.attachments: + raise exc.MissingArgument + + for attachment in ctx.message.attachments: + urls.append(attachment.url) + + for url in urls: + try: + await dest.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + await dest.send('**Probable match**', embed=embed) + + c += 1 + + except exc.MatchError as e: + await ctx.send('**No probable match for:** `{}`'.format(e), delete_after=7) + + if not c: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.MissingArgument: + await ctx.send('**Invalid url or file.** Be sure the link directs to an image file', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.SizeError as e: + await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.command(name='reversify', aliases=['revify', 'risify', 'rify']) + async def reversify(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=self.HISTORY_LIMIT / 5) + dest, remove, limit = kwargs['destination'], kwargs['remove'], kwargs['limit'] + links = {} + c = 0 + + if not ctx.author.permissions_in(ctx.channel).manage_messages: + dest = ctx.author + + async for message in ctx.channel.history(limit=self.HISTORY_LIMIT * limit): + if c >= limit: + break + if message.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content) is not None or message.embeds or message.attachments): + links[message] = [] + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): + links[message].append(match.group(0)) + for embed in message.embeds: + if embed.image.url is not d.Embed.Empty: + links[message].append(embed.image.url) + for attachment in message.attachments: + links[message].append(attachment.url) + + await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + c += 1 + + if not links: + raise exc.NotFound + + n = 1 + for message, urls in links.items(): + for url in urls: + try: + await dest.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + await dest.send(f'**Probable match from** {message.author.display_name}', embed=embed) + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + if remove: + with suppress(err.NotFound): + await message.delete() + + except exc.MatchError as e: + await ctx.send('`{} / {}` **No probable match for:** `{}`'.format(n, len(links), e), delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + c -= 1 + except exc.SizeError as e: + await ctx.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + c -= 1 + + finally: + n += 1 + + if c <= 0: + await ctx.message.add_reaction('\N{CROSS MARK}') + + except exc.NotFound: + await ctx.send('**No matches found**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **invalid limit.** Query limited to 30'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + async def _reversify(self): + while self.reversifying: + message = await self.reversiqueue.get() + urls = [] + + for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', message.content): + urls.append(match.group(0)) + for embed in message.embeds: + if embed.image.url is not d.Embed.Empty: + urls.append(embed.image.url) + for attachment in message.attachments: + urls.append(attachment.url) + + for url in urls: + try: + await message.channel.trigger_typing() + + post = await scraper.get_post(url) + + embed = d.Embed( + title=', '.join(post['artist']), url=f'https://e621.net/post/show/{post["id"]}', color=message.channel.guild.me.color if isinstance(message.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=f'{post["width"]} x {post["height"]}', + url=f'https://e621.net/post?tags=ratio:{post["width"]/post["height"]:.2f}', icon_url=message.author.avatar_url) + embed.set_footer(text=post['score'], + icon_url=self._get_score(post['score'])) + + await message.channel.send('**Probable match from** {}'.format(message.author.display_name), embed=embed) + + await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') + + await asyncio.sleep(self.RATE_LIMIT) + + with suppress(err.NotFound): + await message.delete() + + except exc.MatchError as e: + await message.channel.send('**No probable match for:** `{}`'.format(e), delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + except exc.SizeError as e: + await message.channel.send(f'`{e}` **too large.** Maximum is 8 MB', delete_after=7) + await message.add_reaction('\N{CROSS MARK}') + except Exception: + await message.channel.send(f'**An unknown error occurred.**', delete_after=7) + await message.add_reaction('\N{WARNING SIGN}') + + print('STOPPED : reversifying') + + async def queue_for_reversification(self, channel): + def check(msg): + if 'stop r' in msg.content.lower() and msg.channel is channel and msg.author.guild_permissions.administrator: + raise exc.Abort + elif msg.channel is channel and msg.author.id != self.bot.user.id and (re.search('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', msg.content) is not None or msg.attachments or msg.embeds): + return True + return False + + try: + while self.reversifying: + message = await self.bot.wait_for('message', check=check) + await self.reversiqueue.put(message) + await message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + except exc.Abort: + u.tasks['auto_rev'].remove(channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + if not u.tasks['auto_rev']: + self.reversifying = False + print('STOPPED : reversifying #{}'.format(channel.name)) + await channel.send('**Stopped queueing messages for reversification in** {}'.format(channel.mention), delete_after=5) + + @cmds.command(name='autoreversify', aliases=['autorev']) + @cmds.has_permissions(manage_channels=True) + async def auto_reversify(self, ctx): + if ctx.channel.id not in u.tasks['auto_rev']: + u.tasks['auto_rev'].append(ctx.channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + self.bot.loop.create_task( + self.queue_for_reversification(ctx.channel)) + if not self.reversifying: + self.bot.loop.create_task(self._reversify()) + self.reversifying = True + + print('STARTED : auto-reversifying in #{}'.format(ctx.channel.name)) + await ctx.send('**Auto-reversifying all images in** {}'.format(ctx.channel.mention), delete_after=5) + else: + await ctx.send('**Already auto-reversifying in {}.** Type `stop r(eversifying)` to stop.'.format(ctx.channel.mention), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + async def _get_pool(self, ctx, *, destination, booru='e621', query=[]): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user is ctx.author: + raise exc.Abort(match) + return False + + def on_message(msg): + return msg.content.isdigit() and int(msg.content) <= len(pools) and int(msg.content) > 0 and msg.author is ctx.author and msg.channel is ctx.channel + + posts = {} + pool = {} + + try: + pools = [] + pool_request = await u.fetch('https://{}.net/pool/index.json'.format(booru), params={'query': ' '.join(query)}, json=True) + if len(pool_request) > 1: + for pool in pool_request: + pools.append(pool['name']) + match = await ctx.send('**Multiple pools found for `{}`.** Type the number of the correct match.\n```\n{}```'.format(' '.join(query), '\n'.join(['{} {}'.format(c, elem) for c, elem in enumerate(pools, 1)]))) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + done, pending = await asyncio.wait([self.bot.wait_for('reaction_add', check=on_reaction, timeout=60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=60), self.bot.wait_for('message', check=on_message, timeout=60)], return_when=asyncio.FIRST_COMPLETED) + for future in done: + selection = future.result() + + await match.delete() + tempool = [pool for pool in pool_request if pool['name'] + == pools[int(selection.content) - 1]][0] + await selection.delete() + pool = {'name': tempool['name'], 'id': tempool['id']} + + await destination.trigger_typing() + elif pool_request: + tempool = pool_request[0] + pool = {'name': pool_request[0] + ['name'], 'id': pool_request[0]['id']} + else: + raise exc.NotFound + + page = 1 + while len(posts) < tempool['post_count']: + posts_request = await u.fetch('https://{}.net/pool/show.json'.format(booru), params={'id': tempool['id'], 'page': page}, json=True) + for post in posts_request['posts']: + posts[post['id']] = {'artist': ', '.join( + post['artist']), 'file_url': post['file_url'], 'score': post['score']} + page += 1 + + return pool, posts + + except exc.Abort as e: + await e.message.edit(content='\N{NO ENTRY SIGN}') + raise exc.Continue + + # Messy code that checks image limit and tags in blacklists + async def _get_posts(self, ctx, *, booru='e621', tags=[], limit=1, previous={}): + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + blacklist = set() + # Creates temp blacklist based on context + for bl in (self.blacklists['global_blacklist'], self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set()), self.blacklists['user_blacklist'].get(ctx.author.id, set())): + for tag in bl: + blacklist.update([tag] + list(self.aliases[tag])) + # Checks for, assigns, and removes first order in tags if possible + order = [tag for tag in tags if 'order:' in tag] + if order: + order = order[0] + tags.remove(order) + else: + order = 'order:random' + # Checks if tags are in local blacklists + if tags: + if (len(tags) > 5 and booru == 'e621') or (len(tags) > 4 and booru == 'e926'): + raise exc.TagBoundsError(formatter.tostring(tags[5:])) + for tag in tags: + if tag == 'swf' or tag == 'webm' or tag in blacklist: + raise exc.TagBlacklisted(tag) + + # Checks for blacklisted tags in endpoint blacklists - try/except is for continuing the parent loop + posts = {} + temposts = len(posts) + empty = 0 + c = 0 + while len(posts) < limit: + if c == limit * 5 + (self.LIMIT / 5): + raise exc.Timeout + request = await u.fetch('https://{}.net/post/index.json'.format(booru), params={'tags': ','.join([order] + tags), 'limit': int(self.LIMIT * limit)}, json=True) + if len(request) == 0: + raise exc.NotFound(formatter.tostring(tags)) + if len(request) < limit: + limit = len(request) + + for post in request: + if 'swf' in post['file_ext'] or 'webm' in post['file_ext']: + continue + try: + for tag in blacklist: + if tag in post['tags']: + raise exc.Continue + except exc.Continue: + continue + if post['id'] not in posts.keys() and post['id'] not in previous.keys(): + posts[post['id']] = {'artist': ', '.join( + post['artist']), 'file_url': post['file_url'], 'score': post['score']} + if len(posts) == limit: + break + + if len(posts) == temposts: + empty += 1 + if empty == 5: + break + else: + empty = 0 + temposts = len(posts) + c += 1 + + if posts: + return posts, order + else: + raise exc.NotFound(formatter.tostring(tags)) + + # Creates reaction-based paginator for linked pools + @cmds.command(name='poolpage', aliases=['poolp', 'pp', 'e621pp', 'e6pp', '6pp'], brief='e621 pool paginator', description='e621 | NSFW\nShow pools in a page format') + async def pool_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, query = kwargs['destination'], kwargs['remaining'] + hearted = {} + c = 1 + + await dest.trigger_typing() + + pool, posts = await self._get_pool(ctx, destination=dest, booru='e621', query=query) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=dest.me.color if isinstance(dest.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=pool['name'], + url='https://e621.net/pool/show?id={}'.format(pool['id']), icon_url=ctx.author.avatar_url) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted: + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content='\N{INPUT SYMBOL FOR NUMBERS}') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text='{} / {}'.format(c, len(posts)), + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{WHITE HEAVY CHECK MARK}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound: + await ctx.send('**Pool not found**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Continue: + pass + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + @cmds.command(name='e621page', aliases=['e621p', 'e6p', '6p']) + @checks.is_nsfw() + async def e621_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + limit = self.LIMIT / 5 + hearted = {} + c = 1 + + tags = self._get_favorites(ctx, tags) + + await ctx.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e621.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted.keys(): + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content=f'`{c} / {len(posts)}`') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + try: + if c % limit == 0: + await dest.trigger_typing() + temposts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit, previous=posts) + posts.update(temposts) + + keys = list(posts.keys()) + values = list(posts.values()) + + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e621.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.NotFound: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + # @e621_paginator.error + # async def e621_paginator_error(self, ctx, error): + # if isinstance(error, exc.NSFW): + # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{NO ENTRY}') + + @cmds.command(name='e926page', aliases=['e926p', 'e9p', '9p']) + async def e926_paginator(self, ctx, *args): + def on_reaction(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user is ctx.author or user.permissions_in(reaction.message.channel).manage_messages): + raise exc.Abort + elif reaction.emoji == '\N{HEAVY BLACK HEART}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Save + elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Left + elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.GoTo + elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user is ctx.author: + raise exc.Right + return False + + def on_message(msg): + return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author is ctx.author and msg.channel is ctx.channel + + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + limit = self.LIMIT / 5 + hearted = {} + c = 1 + + tags = self._get_favorites(ctx, tags) + + await ctx.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) + keys = list(posts.keys()) + values = list(posts.values()) + + embed = d.Embed( + title=values[c - 1]['artist'], url='https://e926.net/post/show/{}'.format(keys[c - 1]), color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=values[c - 1]['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e926.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + + paginator = await dest.send(embed=embed) + + for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}', '\N{BLACK RIGHTWARDS ARROW}'): + await paginator.add_reaction(emoji) + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + while not self.bot.is_closed(): + try: + await asyncio.gather(*[self.bot.wait_for('reaction_add', check=on_reaction, timeout=7 * 60), + self.bot.wait_for('reaction_remove', check=on_reaction, timeout=7 * 60)]) + + except exc.Save: + if keys[c - 1] not in hearted: + hearted[keys[c - 1]] = copy.deepcopy(embed) + + await paginator.edit(content='\N{HEAVY BLACK HEART}') + else: + del hearted[keys[c - 1]] + + await paginator.edit(content='\N{BROKEN HEART}') + + except exc.Left: + if c > 1: + c -= 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{BLACK RIGHTWARDS ARROW}') + + except exc.GoTo: + await paginator.edit(content=f'`{c} / {len(posts)}`') + number = await self.bot.wait_for('message', check=on_message, timeout=7 * 60) + + if int(number.content) != 0: + c = int(number.content) + + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await number.delete() + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + + except exc.Right: + try: + if c % limit == 0: + await dest.trigger_typing() + temposts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit, previous=posts) + posts.update(temposts) + + keys = list(posts.keys()) + values = list(posts.values()) + + if c < len(keys): + c += 1 + embed.title = values[c - 1]['artist'] + embed.url = 'https://e926.net/post/show/{}'.format( + keys[c - 1]) + embed.set_footer(text=values[c - 1]['score'], + icon_url=self._get_score(values[c - 1]['score'])) + embed.set_image(url=values[c - 1]['file_url']) + + await paginator.edit(content='\N{HEAVY BLACK HEART}' if keys[c - 1] in hearted.keys() else None, embed=embed) + else: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.NotFound: + await paginator.edit(content='\N{LEFTWARDS BLACK ARROW}') + + except exc.Abort: + try: + await paginator.edit(content='\N{WHITE HEAVY CHECK MARK}') + except UnboundLocalError: + await dest.send('\N{WHITE HEAVY CHECK MARK}') + except asyncio.TimeoutError: + try: + await paginator.edit(content='\N{HOURGLASS}') + except UnboundLocalError: + await dest.send('\N{HOURGLASS}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + finally: + if hearted: + await ctx.message.add_reaction('\N{HOURGLASS WITH FLOWING SAND}') + + n = 1 + for embed in hearted.values(): + await asyncio.sleep(self.RATE_LIMIT) + + await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) + n += 1 + + # Searches for and returns images from e621.net given tags when not blacklisted + @cmds.command(aliases=['e6', '6'], brief='e621 | NSFW', description='e621 | NSFW\nTag-based search for e621.net\n\nYou can only search 5 tags and 6 images at once for now.\ne6 [tags...] ([# of images])') + @checks.is_nsfw() + async def e621(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=3) + dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] + + tags = self._get_favorites(ctx, args) + + await dest.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e621', tags=tags, limit=limit) + + for ident, post in posts.items(): + embed = d.Embed(title=post['artist'], url='https://e621.net/post/show/{}'.format(ident), + color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + message = await dest.send(embed=embed) + + self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) + + except exc.TagBlacklisted as e: + await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + # @e621.error + # async def e621_error(self, ctx, error): + # if isinstance(error, exc.NSFW): + # await ctx.send('\N{NO ENTRY} {} **is not an NSFW channel**'.format(ctx.channel.mention), delete_after=7) + # await ctx.message.add_reaction('\N{NO ENTRY}') + + # Searches for and returns images from e926.net given tags when not blacklisted + @cmds.command(aliases=['e9', '9'], brief='e926 | SFW', description='e926 | SFW\nTag-based search for e926.net\n\nYou can only search 5 tags and 6 images at once for now.\ne9 [tags...] ([# of images])') + async def e926(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args, limit=3) + dest, args, limit = kwargs['destination'], kwargs['remaining'], kwargs['limit'] + + tags = self._get_favorites(ctx, args) + + await dest.trigger_typing() + + posts, order = await self._get_posts(ctx, booru='e926', tags=tags, limit=limit) + + for ident, post in posts.items(): + embed = d.Embed(title=post['artist'], url='https://e926.net/post/show/{}'.format(ident), + color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) + embed.set_image(url=post['file_url']) + embed.set_author(name=formatter.tostring(tags, order=order), + url='https://e621.net/post?tags={}'.format(','.join(tags)), icon_url=ctx.author.avatar_url) + embed.set_footer( + text=post['score'], icon_url=self._get_score(post['score'])) + + message = await dest.send(embed=embed) + + self.bot.loop.create_task(self.queue_for_hearts(message=message, send=embed)) + + except exc.TagBlacklisted as e: + await ctx.send('`{}` **blacklisted**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.BoundsError as e: + await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBoundsError as e: + await ctx.send('`{}` **out of bounds.** Tags limited to 5.'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.NotFound as e: + await ctx.send('`{}` **not found**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.FavoritesNotFound: + await ctx.send('**You have no favorite tags**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.Timeout: + await ctx.send('**Request timed out**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + @cmds.group(aliases=['fave', 'fav', 'f']) + async def favorite(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to manage favorites.**\n*Type* `{}help fav` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @favorite.error + async def favorite_error(self, ctx, error): + pass + + @favorite.group(name='get', aliases=['g']) + async def _get_favorite(self, ctx): + pass + + @_get_favorite.command(name='tags', aliases=['t']) + async def __get_favorite_tags(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{WHITE MEDIUM STAR} {}**\'s favorite tags:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.favorites.get(ctx.author.id, {}).get('tags', set()))), delete_after=7) + + @_get_favorite.command(name='posts', aliases=['p']) + async def __get_favorite_posts(self, ctx): + pass + + @favorite.group(name='add', aliases=['a']) + async def _add_favorite(self, ctx): + pass + + @_add_favorite.command(name='tags', aliases=['t']) + async def __add_favorite_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + if tag in self.blacklists['user_blacklist'].get(ctx.author.id, set()): + raise exc.TagBlacklisted(tag) + with suppress(KeyError): + if len(self.favorites[ctx.author.id]['tags']) + len(tags) > 5: + raise exc.BoundsError + + self.favorites.setdefault(ctx.author.id, {}).setdefault( + 'tags', set()).update(tags) + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{} **added to their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except exc.BoundsError: + await ctx.send('**Favorites list currently limited to:** `5`', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagBlacklisted as e: + await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**', delete_after=7) + await ctx.message.add_reaction('\N{NO ENTRY SIGN}') + + @_add_favorite.command(name='posts', aliases=['p']) + async def __add_favorite_posts(self, ctx, *posts): + pass + + @favorite.group(name='remove', aliases=['r']) + async def _remove_favorite(self, ctx): + pass + + @_remove_favorite.command(name='tags', aliases=['t']) + async def __remove_favorite_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.favorites[ctx.author.id].get( + 'tags', set()).remove(tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{} **removed from their favorites:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except KeyError: + await ctx.send('**You do not have any favorites**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except exc.TagError as e: + await ctx.send('`{}` **not in favorites**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_favorite.command(name='posts', aliases=['p']) + async def __remove_favorite_posts(self, ctx): + pass + + @favorite.group(name='clear', aliases=['c']) + async def _clear_favorite(self, ctx): + pass + + @_clear_favorite.command(name='tags', aliases=['t']) + async def __clear_favorite_tags(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + with suppress(KeyError): + del self.favorites[ctx.author.id] + u.dump(self.favorites, 'cogs/favorites.pkl') + + await dest.send('{}**\'s favorites cleared**'.format(ctx.author.mention), delete_after=5) + + @_clear_favorite.command(name='posts', aliases=['p']) + async def __clear_favorite_posts(self, ctx): + pass + + # Umbrella command structure to manage global, channel, and user blacklists + @cmds.group(aliases=['bl', 'b'], brief='(G) Manage blacklists', description='Manage channel or personal blacklists\n\nUsage:\n\{p\}bl get \{blacklist\} to show a blacklist\n\{p\}bl clear \{blacklist\} to clear a blacklist\n\{p\}bl add \{blacklist\} \{tags...\} to add tag(s) to a blacklist\n\{p\}bl remove \{blacklist\} \{tags...\} to remove tags from a blacklist') + async def blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Use a flag to manage blacklists.**\n*Type* `{}help bl` *for more info.*'.format(ctx.prefix), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + # @blacklist.error + # async def blacklist_error(self, ctx, error): + # if isinstance(error, KeyError): + # return await ctx.send('**Blacklist does not exist**', delete_after=7) + + @blacklist.group(name='get', aliases=['g'], brief='(G) Get a blacklist\n\nUsage:\n\{p\}bl get \{blacklist\}') + async def _get_blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_get_blacklist.command(name='global', aliases=['gl', 'g'], brief='Get current global blacklist', description='Get current global blacklist\n\nThis applies to all booru commands, in accordance with Discord\'s ToS agreement\n\nExample:\n\{p\}bl get global') + async def __get_global_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} **Global blacklist:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']))) + + @_get_blacklist.command(name='channel', aliases=['ch', 'c'], brief='Get current channel blacklist', description='Get current channel blacklist\n\nThis is based on context - the channel where the command was executed\n\nExample:\{p\}bl get channel') + async def __get_channel_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) + + @_get_blacklist.command(name='me', aliases=['m'], brief='Get your personal blacklist', description='Get your personal blacklist\n\nYour blacklist is not viewable by anyone but you, except if you call this command in a public channel. The blacklist will be deleted soon after for your privacy\n\nExample:\n\{p\}bl get me') + async def __get_user_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} {}**\'s blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(self.blacklists['user_blacklist'].get(ctx.author.id, set()))), delete_after=7) + + @_get_blacklist.command(name='here', aliases=['h'], brief='Get current global and channel blacklists', description='Get current global and channel blacklists in a single message\n\nExample:\{p\}bl get here') + async def __get_here_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} **__Blacklisted:__**\n\n**Global:**\n```\n{}```\n**{}:**\n```\n{}```'.format(formatter.tostring(self.blacklists['global_blacklist']), ctx.channel.mention, formatter.tostring(self.blacklists['guild_blacklist'].get(guild.id, {}).get(ctx.channel.id, set())))) + + @_get_blacklist.group(name='all', aliases=['a'], hidden=True) + async def __get_all_blacklists(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**') + await ctx.message.add_reaction('\N{CROSS MARK}') + + @__get_all_blacklists.command(name='guild', aliases=['g']) + @cmds.has_permissions(manage_channels=True) + async def ___get_all_guild_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.send('\N{NO ENTRY SIGN} **__{} blacklists:__**\n\n{}'.format(guild.name, formatter.dict_tostring(self.blacklists['guild_blacklist'].get(guild.id, {})))) + + @__get_all_blacklists.command(name='user', aliases=['u', 'member', 'm']) + @cmds.is_owner() + async def ___get_all_user_blacklists(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + await dest.send('\N{NO ENTRY SIGN} **__User blacklists:__**\n\n{}'.format(formatter.dict_tostring(self.blacklists['user_blacklist']))) + + @blacklist.group(name='add', aliases=['a'], brief='(G) Add tag(s) to a blacklist\n\nUsage:\n\{p\}bl add \{blacklist\} \{tags...\}') + async def _add_tags(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_add_tags.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __add_global_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + await dest.trigger_typing() + + self.blacklists['global_blacklist'].update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('**Added to global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) + + @_add_tags.command(name='channel', aliases=['ch', 'c'], brief='@manage_channel@ Add tag(s) to the current channel blacklist (requires manage_channel)', description='Add tag(s) to the current channel blacklist ') + @cmds.has_permissions(manage_channels=True) + async def __add_channel_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + await dest.trigger_typing() + + self.blacklists['guild_blacklist'].setdefault( + guild.id, {}).setdefault(ctx.channel.id, set()).update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('**Added to** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags)), delete_after=5) + + @_add_tags.command(name='me', aliases=['m']) + async def __add_user_tags(self, ctx, *args): + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + await dest.trigger_typing() + + self.blacklists['user_blacklist'].setdefault( + ctx.author.id, set()).update(tags) + for tag in tags: + alias_request = await u.fetch('https://e621.net/tag_alias/index.json', params={'aliased_to': tag, 'approved': 'true'}, json=True) + if alias_request: + for dic in alias_request: + self.aliases.setdefault(tag, set()).add(dic['name']) + else: + self.aliases.setdefault(tag, set()) + u.dump(self.blacklists, 'cogs/blacklists.pkl') + u.dump(self.aliases, 'cogs/aliases.pkl') + + await dest.send('{} **added to their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + @blacklist.group(name='remove', aliases=['rm', 'r']) + async def _remove_tags(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __remove_global_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.blacklists['global_blacklist'].remove(tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Removed from global blacklist:**\n```\n{}```'.format(formatter.tostring(tags)), delete_after=5) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='channel', aliases=['ch', 'c']) + @cmds.has_permissions(manage_channels=True) + async def __remove_channel_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + for tag in tags: + try: + self.blacklists['guild_blacklist'][guild.id][ctx.channel.id].remove( + tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Removed from** {} **blacklist:**\n```\n{}```'.format(ctx.channel.mention, formatter.tostring(tags), delete_after=5)) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_remove_tags.command(name='me', aliases=['m']) + async def __remove_user_tags(self, ctx, *args): + try: + kwargs = u.get_kwargs(ctx, args) + dest, tags = kwargs['destination'], kwargs['remaining'] + + for tag in tags: + try: + self.blacklists['user_blacklist'][ctx.author.id].remove( + tag) + + except KeyError: + raise exc.TagError(tag) + + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{} **removed from their blacklist:**\n```\n{}```'.format(ctx.author.mention, formatter.tostring(tags)), delete_after=5) + + except exc.TagError as e: + await ctx.send('`{}` **not in blacklist**'.format(e), delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @blacklist.group(name='clear', aliases=['cl', 'c']) + async def _clear_blacklist(self, ctx): + if not ctx.invoked_subcommand: + await ctx.send('**Invalid blacklist**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_clear_blacklist.command(name='global', aliases=['gl', 'g']) + @cmds.is_owner() + async def __clear_global_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + self.blacklists['global_blacklist'].clear() + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('**Global blacklist cleared**', delete_after=5) + + @_clear_blacklist.command(name='channel', aliases=['ch', 'c']) + @cmds.has_permissions(manage_channels=True) + async def __clear_channel_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + guild = ctx.guild if isinstance( + ctx.guild, d.Guild) else ctx.channel + + with suppress(KeyError): + del self.blacklists['guild_blacklist'][guild.id][ctx.channel.id] + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{} **blacklist cleared**'.format(ctx.channel.mention), delete_after=5) + + @_clear_blacklist.command(name='me', aliases=['m']) + async def __clear_user_blacklist(self, ctx, *args): + dest = u.get_kwargs(ctx, args)['destination'] + + with suppress(KeyError): + del self.blacklists['user_blacklist'][ctx.author.id] + u.dump(self.blacklists, 'cogs/blacklists.pkl') + + await dest.send('{}**\'s blacklist cleared**'.format(ctx.author.mention), delete_after=5) diff --git a/src/cogs/management.py b/src/cogs/management.py index 7d6b2f2..ec62cce 100644 --- a/src/cogs/management.py +++ b/src/cogs/management.py @@ -1,235 +1,235 @@ -import asyncio -import traceback as tb -from contextlib import suppress -from datetime import datetime as dt - -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 - -from misc import exceptions as exc -from misc import checks -from utils import utils as u - - -class Administration: - - def __init__(self, bot): - self.bot = bot - self.RATE_LIMIT = u.RATE_LIMIT - self.queue = asyncio.Queue() - self.deleting = False - - if u.tasks['auto_del']: - for channel in u.tasks['auto_del']: - temp = self.bot.get_channel(channel) - self.bot.loop.create_task(self.queue_for_deletion(temp)) - print('STARTED : auto-deleting in #{}'.format(temp.name)) - self.deleting = True - self.bot.loop.create_task(self.delete()) - - @cmds.group(aliases=['pru', 'purge', 'pur', 'clear', 'cl'], hidden=True) - @cmds.is_owner() - async def prune(self, ctx): - pass - - @prune.group(name='user', aliases=['u', 'member', 'm']) - async def _prune_user(self, ctx): - pass - - @_prune_user.command(name='channel', aliases=['channels', 'chans', 'chan', 'ch', 'c']) - async def _prune_user_channel(self, ctx, user: d.User, *channels: d.TextChannel): - def confirm(r, u): - if u is ctx.author: - if r.emoji == '\N{OCTAGONAL SIGN}': - raise exc.Abort - if r.emoji == '\N{THUMBS UP SIGN}': - return True - return False - - if not channels: - channels = [ctx.channel] - - try: - pruning = await ctx.send(f'\N{HOURGLASS} **Pruning** {user.mention}**\'s messages from** {"**,** ".join([channel.mention for channel in channels])} **might take some time.** Proceed, {ctx.author.mention}?') - await pruning.add_reaction('\N{THUMBS UP SIGN}') - await pruning.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - await self.bot.wait_for('reaction_add', check=confirm, timeout=10 * 60) - - deleting = await ctx.send(f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages...**') - await asyncio.sleep(1) - - c = 0 - for channel in channels: - await deleting.edit(content=f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages from** {channel.mention}') - - deleted = await channel.purge(check=lambda m: m.author.id == user.id, before=pruning, limit=None) - c += len(deleted) - - await asyncio.sleep(1) - - for channel in channels: - missed = 0 - async for message in channel.history(before=pruning, limit=None): - if message.author.id == user.id: - missed += 1 - - if missed > 0: - await ctx.send(f'\N{DOUBLE EXCLAMATION MARK} `{missed}` **messages were not deleted in** {channel.mention}') - - await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **Finished deleting** `{c}` **of** {user.mention}**\'s messages**') - - except exc.Abort: - await ctx.send('**Deletion aborted**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except TimeoutError: - await ctx.send('**Deletion timed out**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - @_prune_user.command(name='all', aliases=['a'], brief='Prune a user\'s messages from the guild', description='about flag centers on message 50 of 101 messages\n\npfg \{user id\} [before|after|about] [\{message id\}]\n\nExample:\npfg \{user id\} before \{message id\}', hidden=True) - @cmds.is_owner() - async def _prune_user_all(self, ctx, user: d.User): - def confirm(r, u): - if u is ctx.author: - if r.emoji == '\N{OCTAGONAL SIGN}': - raise exc.Abort - if r.emoji == '\N{THUMBS UP SIGN}': - return True - return False - - try: - pruning = await ctx.send(f'\N{HOURGLASS} **Pruning** {user.mention}**\'s messages might take some time.** Proceed, {ctx.author.mention}?') - await pruning.add_reaction('\N{THUMBS UP SIGN}') - await pruning.add_reaction('\N{OCTAGONAL SIGN}') - await asyncio.sleep(1) - - await self.bot.wait_for('reaction_add', check=confirm, timeout=10 * 60) - - deleting = await ctx.send(f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages...**') - await asyncio.sleep(1) - - c = 0 - for channel in ctx.guild.text_channels: - await deleting.edit(content=f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages from** {channel.mention}') - - deleted = await channel.purge(check=lambda m: m.author.id == user.id, before=pruning, limit=None) - c += len(deleted) - - await asyncio.sleep(1) - - for channel in ctx.guild.text_channels: - missed = 0 - async for message in channel.history(before=pruning, limit=None): - if message.author.id == user.id: - missed += 1 - - if missed > 0: - await ctx.send(f'\N{DOUBLE EXCLAMATION MARK} `{missed}` **messages were not deleted in** {channel.mention}') - - await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **Finished deleting** `{c}` **of** {user.mention}**\'s messages**') - - except exc.Abort: - await ctx.send('**Deletion aborted**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - except TimeoutError: - 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() - await asyncio.sleep(self.RATE_LIMIT) - with suppress(err.NotFound): - if not message.pinned: - await message.delete() - - print('STOPPED : deleting') - - async def queue_for_deletion(self, channel): - def check(msg): - 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 - return False - - try: - async for message in channel.history(limit=None): - 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) - - while not self.bot.is_closed(): - message = await self.bot.wait_for('message', check=check) - await self.queue.put(message) - - except exc.Abort: - u.tasks['auto_del'].remove(channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - if not u.tasks['auto_del']: - self.deleting = False - print('STOPPED : deleting #{}'.format(channel.name)) - await channel.send('**Stopped queueing messages for deletion in** {}'.format(channel.mention), delete_after=5) - - @cmds.command(name='autodelete', aliases=['autodel']) - @cmds.has_permissions(administrator=True) - async def auto_delete(self, ctx): - try: - if ctx.channel.id not in u.tasks['auto_del']: - u.tasks['auto_del'].append(ctx.channel.id) - u.dump(u.tasks, 'cogs/tasks.pkl') - self.bot.loop.create_task(self.queue_for_deletion(ctx.channel)) - if not self.deleting: - self.bot.loop.create_task(self.delete()) - self.deleting = True - print('STARTED : auto-deleting in #{}'.format(ctx.channel.name)) - await ctx.send('**Auto-deleting all messages in {}**'.format(ctx.channel.mention), delete_after=5) - else: - raise exc.Exists - - except exc.Exists: - 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']) - @cmds.has_permissions(administrator=True) - async def settings(self, ctx): - pass - - @settings.command(name='deletecommands', aliases=['delcmds', 'delcmd']) - 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: - u.settings['del_ctx'].remove(ctx.guild.id) - u.dump(u.settings, 'settings.pkl') - - await ctx.send('**Delete command invocations:** `{}`'.format(ctx.guild.id in u.settings['del_ctx'])) - - @settings.command(name='prefix', aliases=['pre', 'p']) - async def _settings_prefix(self, ctx, *prefixes): - if prefixes: - u.settings['prefixes'][ctx.guild.id] = prefixes - else: - with suppress(KeyError): - 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"]}`') +import asyncio +import traceback as tb +from contextlib import suppress +from datetime import datetime as dt + +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 + +from misc import exceptions as exc +from misc import checks +from utils import utils as u + + +class Administration: + + def __init__(self, bot): + self.bot = bot + self.RATE_LIMIT = u.RATE_LIMIT + self.queue = asyncio.Queue() + self.deleting = False + + if u.tasks['auto_del']: + for channel in u.tasks['auto_del']: + temp = self.bot.get_channel(channel) + self.bot.loop.create_task(self.queue_for_deletion(temp)) + print('STARTED : auto-deleting in #{}'.format(temp.name)) + self.deleting = True + self.bot.loop.create_task(self.delete()) + + @cmds.group(aliases=['pru', 'purge', 'pur', 'clear', 'cl'], hidden=True) + @cmds.is_owner() + async def prune(self, ctx): + pass + + @prune.group(name='user', aliases=['u', 'member', 'm']) + async def _prune_user(self, ctx): + pass + + @_prune_user.command(name='channel', aliases=['channels', 'chans', 'chan', 'ch', 'c']) + async def _prune_user_channel(self, ctx, user: d.User, *channels: d.TextChannel): + def confirm(r, u): + if u is ctx.author: + if r.emoji == '\N{OCTAGONAL SIGN}': + raise exc.Abort + if r.emoji == '\N{THUMBS UP SIGN}': + return True + return False + + if not channels: + channels = [ctx.channel] + + try: + pruning = await ctx.send(f'\N{HOURGLASS} **Pruning** {user.mention}**\'s messages from** {"**,** ".join([channel.mention for channel in channels])} **might take some time.** Proceed, {ctx.author.mention}?') + await pruning.add_reaction('\N{THUMBS UP SIGN}') + await pruning.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + await self.bot.wait_for('reaction_add', check=confirm, timeout=10 * 60) + + deleting = await ctx.send(f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages...**') + await asyncio.sleep(1) + + c = 0 + for channel in channels: + await deleting.edit(content=f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages from** {channel.mention}') + + deleted = await channel.purge(check=lambda m: m.author.id == user.id, before=pruning, limit=None) + c += len(deleted) + + await asyncio.sleep(1) + + for channel in channels: + missed = 0 + async for message in channel.history(before=pruning, limit=None): + if message.author.id == user.id: + missed += 1 + + if missed > 0: + await ctx.send(f'\N{DOUBLE EXCLAMATION MARK} `{missed}` **messages were not deleted in** {channel.mention}') + + await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **Finished deleting** `{c}` **of** {user.mention}**\'s messages**') + + except exc.Abort: + await ctx.send('**Deletion aborted**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except TimeoutError: + await ctx.send('**Deletion timed out**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + @_prune_user.command(name='all', aliases=['a'], brief='Prune a user\'s messages from the guild', description='about flag centers on message 50 of 101 messages\n\npfg \{user id\} [before|after|about] [\{message id\}]\n\nExample:\npfg \{user id\} before \{message id\}', hidden=True) + @cmds.is_owner() + async def _prune_user_all(self, ctx, user: d.User): + def confirm(r, u): + if u is ctx.author: + if r.emoji == '\N{OCTAGONAL SIGN}': + raise exc.Abort + if r.emoji == '\N{THUMBS UP SIGN}': + return True + return False + + try: + pruning = await ctx.send(f'\N{HOURGLASS} **Pruning** {user.mention}**\'s messages might take some time.** Proceed, {ctx.author.mention}?') + await pruning.add_reaction('\N{THUMBS UP SIGN}') + await pruning.add_reaction('\N{OCTAGONAL SIGN}') + await asyncio.sleep(1) + + await self.bot.wait_for('reaction_add', check=confirm, timeout=10 * 60) + + deleting = await ctx.send(f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages...**') + await asyncio.sleep(1) + + c = 0 + for channel in ctx.guild.text_channels: + await deleting.edit(content=f'\N{WASTEBASKET} **Deleting** {user.mention}**\'s messages from** {channel.mention}') + + deleted = await channel.purge(check=lambda m: m.author.id == user.id, before=pruning, limit=None) + c += len(deleted) + + await asyncio.sleep(1) + + for channel in ctx.guild.text_channels: + missed = 0 + async for message in channel.history(before=pruning, limit=None): + if message.author.id == user.id: + missed += 1 + + if missed > 0: + await ctx.send(f'\N{DOUBLE EXCLAMATION MARK} `{missed}` **messages were not deleted in** {channel.mention}') + + await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **Finished deleting** `{c}` **of** {user.mention}**\'s messages**') + + except exc.Abort: + await ctx.send('**Deletion aborted**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + except TimeoutError: + 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() + await asyncio.sleep(self.RATE_LIMIT) + with suppress(err.NotFound): + if not message.pinned: + await message.delete() + + print('STOPPED : deleting') + + async def queue_for_deletion(self, channel): + def check(msg): + 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 + return False + + try: + async for message in channel.history(limit=None): + 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) + + while not self.bot.is_closed(): + message = await self.bot.wait_for('message', check=check) + await self.queue.put(message) + + except exc.Abort: + u.tasks['auto_del'].remove(channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + if not u.tasks['auto_del']: + self.deleting = False + print('STOPPED : deleting #{}'.format(channel.name)) + await channel.send('**Stopped queueing messages for deletion in** {}'.format(channel.mention), delete_after=5) + + @cmds.command(name='autodelete', aliases=['autodel']) + @cmds.has_permissions(administrator=True) + async def auto_delete(self, ctx): + try: + if ctx.channel.id not in u.tasks['auto_del']: + u.tasks['auto_del'].append(ctx.channel.id) + u.dump(u.tasks, 'cogs/tasks.pkl') + self.bot.loop.create_task(self.queue_for_deletion(ctx.channel)) + if not self.deleting: + self.bot.loop.create_task(self.delete()) + self.deleting = True + print('STARTED : auto-deleting in #{}'.format(ctx.channel.name)) + await ctx.send('**Auto-deleting all messages in {}**'.format(ctx.channel.mention), delete_after=5) + else: + raise exc.Exists + + except exc.Exists: + 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']) + @cmds.has_permissions(administrator=True) + async def settings(self, ctx): + pass + + @settings.command(name='deletecommands', aliases=['delcmds', 'delcmd']) + 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: + u.settings['del_ctx'].remove(ctx.guild.id) + u.dump(u.settings, 'settings.pkl') + + await ctx.send('**Delete command invocations:** `{}`'.format(ctx.guild.id in u.settings['del_ctx'])) + + @settings.command(name='prefix', aliases=['pre', 'p']) + async def _settings_prefix(self, ctx, *prefixes): + if prefixes: + u.settings['prefixes'][ctx.guild.id] = prefixes + else: + with suppress(KeyError): + 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/owner.py b/src/cogs/owner.py index 03915fd..6c5135f 100644 --- a/src/cogs/owner.py +++ b/src/cogs/owner.py @@ -1,245 +1,245 @@ -import asyncio -import code -import io -import os -import re -import sys -import traceback as tb -from contextlib import redirect_stdout, suppress - -import discord as d -import pyrasite as pyr -from discord.ext import commands as cmds - -from misc import exceptions as exc -from misc import checks -from utils import utils as u - - -class Bot: - - def __init__(self, bot): - self.bot = bot - - # Close connection to Discord - immediate offline - @cmds.command(name=',die', aliases=[',d'], brief='Kills the bot', description='BOT OWNER ONLY\nCloses the connection to Discord', hidden=True) - @cmds.is_owner() - async def die(self, ctx): - await ctx.message.add_reaction('\N{CRESCENT MOON}') - - await self.bot.get_channel(u.config['info_channel']).send('**Shutting down** \N{CRESCENT MOON} . . .') - - chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' - u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) - u.dump(u.temp, 'temp/temp.pkl') - - # loop = self.bot.loop.all_tasks() - # for task in loop: - # task.cancel() - await self.bot.logout() - u.close(self.bot.loop) - print('\n< < < < < < < < < < < <\nD I S C O N N E C T E D\n< < < < < < < < < < < <\n') - # u.notify('D I S C O N N E C T E D') - - @cmds.command(name=',restart', aliases=[',res', ',r'], hidden=True) - @cmds.is_owner() - async def restart(self, ctx): - await ctx.message.add_reaction('\N{SLEEPING SYMBOL}') - - print('\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\nR E S T A R T I N G\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n') - await self.bot.get_channel(u.config['info_channel']).send('**Restarting** \N{SLEEPING SYMBOL} . . .') - # u.notify('R E S T A R T I N G') - - chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' - u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) - u.dump(u.temp, 'temp/temp.pkl') - - # loop = self.bot.loop.all_tasks() - # for task in loop: - # task.cancel() - await self.bot.logout() - u.close(self.bot.loop) - os.execl(sys.executable, 'python3', 'run.py') - - # Invite bot to bot owner's server - @cmds.command(name=',invite', aliases=[',inv', ',link'], brief='Invite the bot', description='BOT OWNER ONLY\nInvite the bot to a server (Requires admin)', hidden=True) - @cmds.is_owner() - async def invite(self, ctx): - await ctx.message.add_reaction('\N{ENVELOPE}') - - await ctx.send('https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}'.format(u.config['client_id'], u.config['permissions']), delete_after=5) - - @cmds.command(name=',guilds', aliases=[',glds', ',servers', ',servs']) - @cmds.is_owner() - async def guilds(self, ctx): - paginator = cmds.Paginator() - - for guild in self.bot.guilds: - paginator.add_line(guild.name) - - for page in paginator.pages: - await ctx.send(f'**Guilds:**\n{page}') - - @cmds.command(name=',status', aliases=[',presence', ',game'], hidden=True) - @cmds.is_owner() - async def change_status(self, ctx, *, game=None): - if game: - await self.bot.change_presence(game=d.Game(name=game)) - u.config['playing'] = game - u.dump(u.config, 'config.json', json=True) - await ctx.send(f'**Game changed to** `{game}`') - else: - await self.bot.change_presence(game=None) - u.config['playing'] = '' - u.dump(u.config, 'config.json', json=True) - await ctx.send('**Game changed to** ` `') - - @cmds.command(name=',username', aliases=[',user'], hidden=True) - @cmds.is_owner() - async def change_username(self, ctx, *, username=None): - if username: - await self.bot.user.edit(username=username) - await ctx.send(f'**Username changed to** `{username}`') - else: - await ctx.send('**Invalid string**', delete_after=7) - await ctx.message.add_reaction('\N{CROSS MARK}') - - -class Tools: - - def __init__(self, bot): - self.bot = bot - - def format(self, i='', o=''): - if len(o) > 1: - return '>>> {}\n{}'.format(i, o) - else: - return '>>> {}'.format(i) - - async def generate(self, d, i='', o=''): - return await d.send('```python\n{}```'.format(self.format(i, o))) - - async def refresh(self, m, i='', o=''): - output = m.content[9:-3] - if len(re.findall('\n', output)) <= 20: - await m.edit(content='```python\n{}\n{}\n>>>```'.format(output, self.format(i, o))) - else: - await m.edit(content='```python\n{}```'.format(self.format(i, o))) - - async def generate_err(self, d, o=''): - return await d.send('```\n{}```'.format(o)) - - async def refresh_err(self, m, o=''): - await m.edit(content='```\n{}```'.format(o)) - - @cmds.command(name=',console', aliases=[',con', ',c'], hidden=True) - @cmds.is_owner() - async def console(self, ctx): - def execute(msg): - if msg.content.lower().startswith('exec ') and msg.author is ctx.author and msg.channel is ctx.channel: - msg.content = msg.content[5:] - return True - return False - - def evaluate(msg): - if msg.content.lower().startswith('eval ') and msg.author is ctx.author and msg.channel is ctx.channel: - msg.content = msg.content[5:] - return True - return False - - def exit(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and user is ctx.author and reaction.message.id == ctx.message.id: - raise exc.Abort - return False - - try: - console = await self.generate(ctx) - exception = await self.generate_err(ctx) - - await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') - - while not self.bot.is_closed(): - try: - done, pending = await asyncio.wait([self.bot.wait_for('message', check=execute), self.bot.wait_for('message', check=evaluate), self.bot.wait_for('reaction_add', check=exit)], return_when=asyncio.FIRST_COMPLETED) - - message = done.pop().result() - print(message.content) - - except exc.Execute: - try: - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - exec(message.content) - - except Exception: - await self.refresh_err(exception, tb.format_exc(limit=1)) - - finally: - await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - with suppress(d.NotFound): - await message.delete() - - except exc.Evaluate: - try: - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - eval(message.content) - - except Exception: - await self.refresh_err(exception, tb.format_exc(limit=1)) - - finally: - await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - with suppress(d.NotFound): - await message.delete() - - except exc.Abort: - pass - - finally: - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - print('RESET : sys.std output/error') - - @cmds.command(name=',execute', aliases=[',exec'], hidden=True) - @cmds.is_owner() - async def execute(self, ctx, *, exe): - try: - with io.StringIO() as buff, redirect_stdout(buff): - exec(exe) - await self.generate(ctx, exe, f'\n{buff.getvalue()}') - - except Exception: - await self.generate(ctx, exe, f'\n{tb.format_exc()}') - - @cmds.command(name=',evaluate', aliases=[',eval'], hidden=True) - @cmds.is_owner() - async def evaluate(self, ctx, *, evl): - try: - with io.StringIO() as buff, redirect_stdout(buff): - eval(evl) - await self.generate(ctx, evl, f'\n{buff.getvalue()}') - - except Exception: - await self.generate(ctx, evl, f'\n{tb.format_exc()}') - - @cmds.group(aliases=[',db'], hidden=True) - @cmds.is_owner() - async def debug(self, ctx): - console = await self.generate(ctx) - - @debug.command(name='inject', aliases=['inj']) - async def _inject(self, ctx, *, input_): - pass - - @debug.command(name='inspect', aliases=['ins']) - async def _inspect(self, ctx, *, input_): - pass - - # @cmds.command(name='endpoint', aliases=['end']) - # async def get_endpoint(self, ctx, *args): - # await ctx.send(f'```\n{await u.fetch(f"https://{args[0]}/{args[1]}/{args[2]}", params={args[3]: args[4], "limit": 1}, json=True)}```') +import asyncio +import code +import io +import os +import re +import sys +import traceback as tb +from contextlib import redirect_stdout, suppress + +import discord as d +import pyrasite as pyr +from discord.ext import commands as cmds + +from misc import exceptions as exc +from misc import checks +from utils import utils as u + + +class Bot: + + def __init__(self, bot): + self.bot = bot + + # Close connection to Discord - immediate offline + @cmds.command(name=',die', aliases=[',d'], brief='Kills the bot', description='BOT OWNER ONLY\nCloses the connection to Discord', hidden=True) + @cmds.is_owner() + async def die(self, ctx): + await ctx.message.add_reaction('\N{CRESCENT MOON}') + + await self.bot.get_channel(u.config['info_channel']).send('**Shutting down** \N{CRESCENT MOON} . . .') + + chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' + u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) + u.dump(u.temp, 'temp/temp.pkl') + + # loop = self.bot.loop.all_tasks() + # for task in loop: + # task.cancel() + await self.bot.logout() + u.close(self.bot.loop) + print('\n< < < < < < < < < < < <\nD I S C O N N E C T E D\n< < < < < < < < < < < <\n') + # u.notify('D I S C O N N E C T E D') + + @cmds.command(name=',restart', aliases=[',res', ',r'], hidden=True) + @cmds.is_owner() + async def restart(self, ctx): + await ctx.message.add_reaction('\N{SLEEPING SYMBOL}') + + print('\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\nR E S T A R T I N G\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n') + await self.bot.get_channel(u.config['info_channel']).send('**Restarting** \N{SLEEPING SYMBOL} . . .') + # u.notify('R E S T A R T I N G') + + chantype = 'guild' if isinstance(ctx.channel, d.TextChannel) else 'private' + u.temp['startup'] = (chantype, ctx.channel.id if chantype == 'guild' else ctx.author.id, ctx.message.id) + u.dump(u.temp, 'temp/temp.pkl') + + # loop = self.bot.loop.all_tasks() + # for task in loop: + # task.cancel() + await self.bot.logout() + u.close(self.bot.loop) + os.execl(sys.executable, 'python3', 'run.py') + + # Invite bot to bot owner's server + @cmds.command(name=',invite', aliases=[',inv', ',link'], brief='Invite the bot', description='BOT OWNER ONLY\nInvite the bot to a server (Requires admin)', hidden=True) + @cmds.is_owner() + async def invite(self, ctx): + await ctx.message.add_reaction('\N{ENVELOPE}') + + await ctx.send('https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}'.format(u.config['client_id'], u.config['permissions']), delete_after=5) + + @cmds.command(name=',guilds', aliases=[',glds', ',servers', ',servs']) + @cmds.is_owner() + async def guilds(self, ctx): + paginator = cmds.Paginator() + + for guild in self.bot.guilds: + paginator.add_line(guild.name) + + for page in paginator.pages: + await ctx.send(f'**Guilds:**\n{page}') + + @cmds.command(name=',status', aliases=[',presence', ',game'], hidden=True) + @cmds.is_owner() + async def change_status(self, ctx, *, game=None): + if game: + await self.bot.change_presence(game=d.Game(name=game)) + u.config['playing'] = game + u.dump(u.config, 'config.json', json=True) + await ctx.send(f'**Game changed to** `{game}`') + else: + await self.bot.change_presence(game=None) + u.config['playing'] = '' + u.dump(u.config, 'config.json', json=True) + await ctx.send('**Game changed to** ` `') + + @cmds.command(name=',username', aliases=[',user'], hidden=True) + @cmds.is_owner() + async def change_username(self, ctx, *, username=None): + if username: + await self.bot.user.edit(username=username) + await ctx.send(f'**Username changed to** `{username}`') + else: + await ctx.send('**Invalid string**', delete_after=7) + await ctx.message.add_reaction('\N{CROSS MARK}') + + +class Tools: + + def __init__(self, bot): + self.bot = bot + + def format(self, i='', o=''): + if len(o) > 1: + return '>>> {}\n{}'.format(i, o) + else: + return '>>> {}'.format(i) + + async def generate(self, d, i='', o=''): + return await d.send('```python\n{}```'.format(self.format(i, o))) + + async def refresh(self, m, i='', o=''): + output = m.content[9:-3] + if len(re.findall('\n', output)) <= 20: + await m.edit(content='```python\n{}\n{}\n>>>```'.format(output, self.format(i, o))) + else: + await m.edit(content='```python\n{}```'.format(self.format(i, o))) + + async def generate_err(self, d, o=''): + return await d.send('```\n{}```'.format(o)) + + async def refresh_err(self, m, o=''): + await m.edit(content='```\n{}```'.format(o)) + + @cmds.command(name=',console', aliases=[',con', ',c'], hidden=True) + @cmds.is_owner() + async def console(self, ctx): + def execute(msg): + if msg.content.lower().startswith('exec ') and msg.author is ctx.author and msg.channel is ctx.channel: + msg.content = msg.content[5:] + return True + return False + + def evaluate(msg): + if msg.content.lower().startswith('eval ') and msg.author is ctx.author and msg.channel is ctx.channel: + msg.content = msg.content[5:] + return True + return False + + def exit(reaction, user): + if reaction.emoji == '\N{OCTAGONAL SIGN}' and user is ctx.author and reaction.message.id == ctx.message.id: + raise exc.Abort + return False + + try: + console = await self.generate(ctx) + exception = await self.generate_err(ctx) + + await ctx.message.add_reaction('\N{OCTAGONAL SIGN}') + + while not self.bot.is_closed(): + try: + done, pending = await asyncio.wait([self.bot.wait_for('message', check=execute), self.bot.wait_for('message', check=evaluate), self.bot.wait_for('reaction_add', check=exit)], return_when=asyncio.FIRST_COMPLETED) + + message = done.pop().result() + print(message.content) + + except exc.Execute: + try: + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + exec(message.content) + + except Exception: + await self.refresh_err(exception, tb.format_exc(limit=1)) + + finally: + await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + with suppress(d.NotFound): + await message.delete() + + except exc.Evaluate: + try: + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + eval(message.content) + + except Exception: + await self.refresh_err(exception, tb.format_exc(limit=1)) + + finally: + await self.refresh(console, message.content, sys.stdout.getvalue() if sys.stdout.getvalue() != console.content else None) + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + with suppress(d.NotFound): + await message.delete() + + except exc.Abort: + pass + + finally: + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + print('RESET : sys.std output/error') + + @cmds.command(name=',execute', aliases=[',exec'], hidden=True) + @cmds.is_owner() + async def execute(self, ctx, *, exe): + try: + with io.StringIO() as buff, redirect_stdout(buff): + exec(exe) + await self.generate(ctx, exe, f'\n{buff.getvalue()}') + + except Exception: + await self.generate(ctx, exe, f'\n{tb.format_exc()}') + + @cmds.command(name=',evaluate', aliases=[',eval'], hidden=True) + @cmds.is_owner() + async def evaluate(self, ctx, *, evl): + try: + with io.StringIO() as buff, redirect_stdout(buff): + eval(evl) + await self.generate(ctx, evl, f'\n{buff.getvalue()}') + + except Exception: + await self.generate(ctx, evl, f'\n{tb.format_exc()}') + + @cmds.group(aliases=[',db'], hidden=True) + @cmds.is_owner() + async def debug(self, ctx): + console = await self.generate(ctx) + + @debug.command(name='inject', aliases=['inj']) + async def _inject(self, ctx, *, input_): + pass + + @debug.command(name='inspect', aliases=['ins']) + async def _inspect(self, ctx, *, input_): + pass + + # @cmds.command(name='endpoint', aliases=['end']) + # async def get_endpoint(self, ctx, *args): + # await ctx.send(f'```\n{await u.fetch(f"https://{args[0]}/{args[1]}/{args[2]}", params={args[3]: args[4], "limit": 1}, json=True)}```') diff --git a/src/utils/utils.py b/src/utils/utils.py index a2d8f92..f7236f1 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1,191 +1,191 @@ -import asyncio -import json as jsn -import os -import pickle as pkl -import subprocess -from contextlib import suppress -from fractions import gcd -import math -import gmusicapi as gpm - -import aiohttp -import discord as d - -from misc import exceptions as exc - -# from pync import Notifier - - -print('\nPID : {}\n'.format(os.getpid())) - - -# def notify(message): -# subprocess.run(['terminal-notifier', '-message', message, '-title', -# 'Modumind', '-activate', 'com.apple.Terminal', '-appIcon', 'icon.png', '-sound', 'Ping'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - -try: - with open('config.json') as infile: - config = jsn.load(infile) - print('LOADED : config.json') - -except FileNotFoundError: - with open('config.json', 'w') as outfile: - jsn.dump({'client_id': 0, 'info_channel': 0, 'owner_id': 0, 'permissions': 126016, - 'playing': 'a game', 'prefix': [',', 'm,'], 'selfbot': False, 'token': 'str'}, outfile, indent=4, sort_keys=True) - print('FILE NOT FOUND : config.json created with abstract values. Restart run.py with correct values') - - -def setdefault(filename, default=None, json=False): - if json: - try: - with open(filename, 'r') as infile: - print(f'LOADED : {filename}') - return jsn.load(infile) - - except FileNotFoundError: - with open(filename, 'w+') as iofile: - print(f'FILE NOT FOUND : {filename} created and loaded with default values') - jsn.dump(default, iofile) - iofile.seek(0) - return jsn.load(iofile) - else: - try: - with open(filename, 'rb') as infile: - print(f'LOADED : {filename}') - return pkl.load(infile) - - except FileNotFoundError: - with open(filename, 'wb+') as iofile: - print(f'FILE NOT FOUND : {filename} created and loaded with default values') - pkl.dump(default, iofile) - iofile.seek(0) - return pkl.load(iofile) - - -def load(filename, *, json=False): - if not json: - with open(filename, 'rb') as infile: - return pkl.load(infile) - else: - with open(filename) as infile: - return jsn.load(infile) - - -def dump(obj, filename, *, json=False): - if not json: - with open(filename, 'wb') as outfile: - pkl.dump(obj, outfile) - else: - with open(filename, 'w') as outfile: - jsn.dump(obj, outfile, indent=4, sort_keys=True) - - -settings = setdefault('misc/settings.pkl', default={'del_ctx': [], 'del_resp': [], 'prefixes': {}}) -tasks = setdefault('cogs/tasks.pkl', default={'auto_del': [], 'auto_hrt': [], 'auto_rev': [], 'periodic_gpm': []}) -temp = setdefault('temp/temp.pkl', default={'startup': ()}) -secrets = setdefault('secrets.json', default={'client_secrets': {'client_id': '', 'client_secret': ''}}, json=True) - -RATE_LIMIT = 2.2 -color = d.Color(0x1A1A1A) -session = aiohttp.ClientSession() -last_commands = {} - - -async def fetch(url, *, params={}, json=False, response=False): - async with session.get(url, params=params, headers={'User-Agent': 'Myned/Modufur'}) as r: - if response: - return r - elif json: - return await r.json() - return await r.read() - - -# async def clear(obj, interval=10 * 60, replace=None): -# if replace is None: -# if type(obj) is list: -# replace = [] -# elif type(obj) is dict: -# replace = {} -# elif type(obj) is int: -# replace = 0 -# elif type(obj) is str: -# replace = '' -# -# while True: -# obj = replace -# asyncio.sleep(interval) - - -def close(loop): - if session: - session.close() - - loop.stop() - pending = asyncio.Task.all_tasks() - for task in pending: - task.cancel() - # with suppress(asyncio.CancelledError): - # loop.run_until_complete(task) - # loop.close() - - print('Finished cancelling tasks.') - - -def generate_embed(ctx, *, title=d.Embed.Empty, kind='rich', description=d.Embed.Empty, url=d.Embed.Empty, timestamp=d.Embed.Empty, colour=color, footer={}, image=d.Embed.Empty, thumbnail=d.Embed.Empty, author={}, fields=[]): - embed = d.Embed(title=title, type=kind, description=description, url=url, timestamp=timestamp, colour=colour if isinstance(ctx.channel, d.TextChannel) else color) - - if footer: - embed.set_footer(text=footer.get('text', d.Embed.Empty), icon_url=footer.get('icon_url', d.Embed.Empty)) - if image: - embed.set_image(url=image) - if thumbnail: - embed.set_thumbnail(url=thumbnail) - if author: - embed.set_author(name=author.get('name', d.Embed.Empty), url=author.get('url', d.Embed.Empty), icon_url=author.get('icon_url', d.Embed.Empty)) - for field in fields: - embed.add_field(name=field.get('name', d.Embed.Empty), value=field.get('value', d.Embed.Empty), inline=field.get('inline', True)) - - return embed - -def get_kwargs(ctx, args, *, limit=False): - destination = ctx - remaining = list(args[:]) - rm = False - lim = 1 - - for flag in ('-d', '-dm'): - if flag in remaining: - destination = ctx.author - - remaining.remove(flag) - - for flag in ('-r', '-rm', '-remove', '-re', '-repl', '-replace'): - if flag in remaining and ctx.author.permissions_in(ctx.channel).manage_messages: - rm = True - - remaining.remove(flag) - - if limit: - for arg in remaining: - if arg.isdigit(): - if 1 <= int(arg) <= limit: - lim = int(arg) - remaining.remove(arg) - break - else: - raise exc.BoundsError(arg) - - return {'destination': destination, 'remaining': remaining, 'remove': rm, 'limit': lim} - - -def get_aspectratio(a, b): - divisor = gcd(a, b) - return f'{int(a / divisor)}:{int(b / divisor)}' - - -def ci(pos, n): - z = 1.96 - phat = float(pos) / n - - return (phat + z*z/(2*n) - z * math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) +import asyncio +import json as jsn +import os +import pickle as pkl +import subprocess +from contextlib import suppress +from fractions import gcd +import math +import gmusicapi as gpm + +import aiohttp +import discord as d + +from misc import exceptions as exc + +# from pync import Notifier + + +print('\nPID : {}\n'.format(os.getpid())) + + +# def notify(message): +# subprocess.run(['terminal-notifier', '-message', message, '-title', +# 'Modumind', '-activate', 'com.apple.Terminal', '-appIcon', 'icon.png', '-sound', 'Ping'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + +try: + with open('config.json') as infile: + config = jsn.load(infile) + print('LOADED : config.json') + +except FileNotFoundError: + with open('config.json', 'w') as outfile: + jsn.dump({'client_id': 0, 'info_channel': 0, 'owner_id': 0, 'permissions': 126016, + 'playing': 'a game', 'prefix': [',', 'm,'], 'selfbot': False, 'token': 'str'}, outfile, indent=4, sort_keys=True) + print('FILE NOT FOUND : config.json created with abstract values. Restart run.py with correct values') + + +def setdefault(filename, default=None, json=False): + if json: + try: + with open(filename, 'r') as infile: + print(f'LOADED : {filename}') + return jsn.load(infile) + + except FileNotFoundError: + with open(filename, 'w+') as iofile: + print(f'FILE NOT FOUND : {filename} created and loaded with default values') + jsn.dump(default, iofile) + iofile.seek(0) + return jsn.load(iofile) + else: + try: + with open(filename, 'rb') as infile: + print(f'LOADED : {filename}') + return pkl.load(infile) + + except FileNotFoundError: + with open(filename, 'wb+') as iofile: + print(f'FILE NOT FOUND : {filename} created and loaded with default values') + pkl.dump(default, iofile) + iofile.seek(0) + return pkl.load(iofile) + + +def load(filename, *, json=False): + if not json: + with open(filename, 'rb') as infile: + return pkl.load(infile) + else: + with open(filename) as infile: + return jsn.load(infile) + + +def dump(obj, filename, *, json=False): + if not json: + with open(filename, 'wb') as outfile: + pkl.dump(obj, outfile) + else: + with open(filename, 'w') as outfile: + jsn.dump(obj, outfile, indent=4, sort_keys=True) + + +settings = setdefault('misc/settings.pkl', default={'del_ctx': [], 'del_resp': [], 'prefixes': {}}) +tasks = setdefault('cogs/tasks.pkl', default={'auto_del': [], 'auto_hrt': [], 'auto_rev': [], 'periodic_gpm': []}) +temp = setdefault('temp/temp.pkl', default={'startup': ()}) +secrets = setdefault('secrets.json', default={'client_secrets': {'client_id': '', 'client_secret': ''}}, json=True) + +RATE_LIMIT = 2.2 +color = d.Color(0x1A1A1A) +session = aiohttp.ClientSession() +last_commands = {} + + +async def fetch(url, *, params={}, json=False, response=False): + async with session.get(url, params=params, headers={'User-Agent': 'Myned/Modufur'}) as r: + if response: + return r + elif json: + return await r.json() + return await r.read() + + +# async def clear(obj, interval=10 * 60, replace=None): +# if replace is None: +# if type(obj) is list: +# replace = [] +# elif type(obj) is dict: +# replace = {} +# elif type(obj) is int: +# replace = 0 +# elif type(obj) is str: +# replace = '' +# +# while True: +# obj = replace +# asyncio.sleep(interval) + + +def close(loop): + if session: + session.close() + + loop.stop() + pending = asyncio.Task.all_tasks() + for task in pending: + task.cancel() + # with suppress(asyncio.CancelledError): + # loop.run_until_complete(task) + # loop.close() + + print('Finished cancelling tasks.') + + +def generate_embed(ctx, *, title=d.Embed.Empty, kind='rich', description=d.Embed.Empty, url=d.Embed.Empty, timestamp=d.Embed.Empty, colour=color, footer={}, image=d.Embed.Empty, thumbnail=d.Embed.Empty, author={}, fields=[]): + embed = d.Embed(title=title, type=kind, description=description, url=url, timestamp=timestamp, colour=colour if isinstance(ctx.channel, d.TextChannel) else color) + + if footer: + embed.set_footer(text=footer.get('text', d.Embed.Empty), icon_url=footer.get('icon_url', d.Embed.Empty)) + if image: + embed.set_image(url=image) + if thumbnail: + embed.set_thumbnail(url=thumbnail) + if author: + embed.set_author(name=author.get('name', d.Embed.Empty), url=author.get('url', d.Embed.Empty), icon_url=author.get('icon_url', d.Embed.Empty)) + for field in fields: + embed.add_field(name=field.get('name', d.Embed.Empty), value=field.get('value', d.Embed.Empty), inline=field.get('inline', True)) + + return embed + +def get_kwargs(ctx, args, *, limit=False): + destination = ctx + remaining = list(args[:]) + rm = False + lim = 1 + + for flag in ('-d', '-dm'): + if flag in remaining: + destination = ctx.author + + remaining.remove(flag) + + for flag in ('-r', '-rm', '-remove', '-re', '-repl', '-replace'): + if flag in remaining and ctx.author.permissions_in(ctx.channel).manage_messages: + rm = True + + remaining.remove(flag) + + if limit: + for arg in remaining: + if arg.isdigit(): + if 1 <= int(arg) <= limit: + lim = int(arg) + remaining.remove(arg) + break + else: + raise exc.BoundsError(arg) + + return {'destination': destination, 'remaining': remaining, 'remove': rm, 'limit': lim} + + +def get_aspectratio(a, b): + divisor = gcd(a, b) + return f'{int(a / divisor)}:{int(b / divisor)}' + + +def ci(pos, n): + z = 1.96 + phat = float(pos) / n + + return (phat + z*z/(2*n) - z * math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)