diff --git a/.gitignore b/.gitignore index b19851c..d2a0bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,5 @@ -# Custom -*.json -*.pyo -*.pyc -*.DS_Store -*.pkl -*.png -*.bat -*.lock +# CUSTOM +config.toml # Byte-compiled / optimized / DLL files __pycache__/ @@ -30,6 +23,7 @@ parts/ sdist/ var/ wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg @@ -48,13 +42,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ +.pytest_cache/ +cover/ # Translations *.mo @@ -62,9 +60,9 @@ coverage.xml # Django stuff: *.log -.static_storage/ -.media/ local_settings.py +db.sqlite3 +db.sqlite3-journal # Flask stuff: instance/ @@ -77,16 +75,41 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints -# pyenv -.python-version +# IPython +profile_default/ +ipython_config.py -# celery beat schedule file +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py @@ -112,3 +135,21 @@ venv.bak/ # mypy .mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 679dc5b..0000000 --- a/Pipfile +++ /dev/null @@ -1,28 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - -[[source]] -url = "https://pypi.org/simple" -name = "pypi" -verify_ssl = true - -[[source]] -url = "https://www.piwheels.org/simple" -name = "piwheels" -verify_ssl = true - -[requires] - -[packages] -beautifulsoup4 = "*" -"discord.py" = {extras = ["voice"],git = "https://github.com/Rapptz/discord.py"} -"hurry.filesize" = "*" -requests = "*" -html5lib = "*" -tldextract = "*" - -[dev-packages] -lxml = "*" -selenium = "*" diff --git a/README.md b/README.md index 6256cb2..67a06f1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,34 @@ # Modufur -Discord booru bot with a side of management and tasking. - -Credits: -Rapptz/discord.py +An experimental [Hikari](https://github.com/hikari-py/hikari) Discord bot for reverse image searching using [SauceNAO](https://saucenao.com) & [Kheina](https://kheina.com) +## Requirements +[Python](https://www.python.org) 3.10+\ +[Poetry](https://python-poetry.org) +## Installation +``` +git clone https://github.com/Myned/Modufur.git +``` +``` +cd Modufur +``` +``` +poetry install +``` +## Usage +``` +poetry run python run.py +``` +## Configuration +`config.toml` +``` +guilds = [] # guild IDs to register commands, empty for global +client = 0 # bot application ID +token = "" # bot token +activity = "" # bot status +saucenao = "" # saucenao token +e621 = "" # e621 token +``` +## Credits +[hikari](https://github.com/hikari-py/hikari)\ +[hikari-lightbulb](https://github.com/tandemdude/hikari-lightbulb)\ +[hikari-miru](https://github.com/HyperGH/hikari-miru)\ +[pysaucenao](https://github.com/FujiMakoto/pysaucenao) diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/commands/booru.py b/commands/booru.py new file mode 100644 index 0000000..cf6bc36 --- /dev/null +++ b/commands/booru.py @@ -0,0 +1,111 @@ +import urlextract +import hikari +import lightbulb +import pysaucenao + +from tools import components, scraper + + +plugin = lightbulb.Plugin('booru') +extractor = urlextract.URLExtract() + + +@plugin.command +#@lightbulb.option('attachment', 'Attachment(s) to reverse', required=False) +@lightbulb.option('url', 'URL(s) to reverse, separated by space') +@lightbulb.command('reverse', 'Reverse image search using SauceNAO & Kheina', ephemeral=True) +@lightbulb.implements(lightbulb.SlashCommand, lightbulb.MessageCommand) +async def reverse(context): + match context: + case lightbulb.SlashContext(): + urls = extractor.find_urls(context.options.url or '', only_unique=True, with_schema_only=True) + + if not urls: + await context.respond('**Invalid URL(s).**') + return + + await _reverse(context, urls) + case lightbulb.MessageContext(): + urls = extractor.find_urls(context.options.target.content or '', only_unique=True, with_schema_only=True) + urls += [attachment.url for attachment in context.options.target.attachments if attachment.url not in urls] + + if not urls: + await context.respond('**No images found.**') + return + + selector = None + + if len(urls) > 1: + selector = components.Selector( + pages=[f'**Select {urls.index(url) + 1} out of {len(urls)} potential images to search:**\n{url}' for url in urls], + buttons=[components.Select(), components.Back(), components.Forward(), components.Confirm()], + urls=urls) + + await selector.send(context.interaction, ephemeral=True) + await selector.wait() + + if selector.timed_out: + return + + urls = selector.selected + + await _reverse(context, urls, selector=selector) + +@reverse.set_error_handler() +async def on_reverse_error(event): + error = None + + match event.exception.__cause__: + case pysaucenao.ShortLimitReachedException(): + error = '**API limit reached. Please try again in a minute.**' + case pysaucenao.DailyLimitReachedException(): + error = '**Daily API limit reached. Please try again tomorrow.**' + case pysaucenao.FileSizeLimitException() as url: + error = f'**Image file size too large:**\n{url}' + case pysaucenao.ImageSizeException() as url: + error = f'**Image resolution too small:**\n{url}' + case pysaucenao.InvalidImageException() as url: + error = f'**Invalid image:**\n{url}' + case pysaucenao.UnknownStatusCodeException(): + error = '**An unknown SauceNAO error has occurred. The service may be down.**' + + if error: + await event.context.respond(error) + return True + +async def _reverse(context, urls, *, selector=None): + if not selector: + await context.respond(hikari.ResponseType.DEFERRED_MESSAGE_CREATE) + + matches = await scraper.reverse(urls) + + if not matches: + if selector: + await context.interaction.edit_initial_response('**No matches found.**', components=None) + else: + await context.respond('**No matches found.**') + return + + pages = [(hikari.Embed( + title=match['artist'], url=match['source'], color=context.get_guild().get_my_member().get_top_role().color) + .set_author(name=f'{match["similarity"]}% Match') + .set_image(match['thumbnail']) + .set_footer(match['index'])) if match else f'**No match found for:**\n{urls[index]}' for index, match in enumerate(matches)] + + if len(pages) > 1: + selector = components.Selector( + pages=pages, + buttons=[components.Back(), components.Forward()], + timeout=900) + + await selector.send_edit(context.interaction) + else: + if selector: + await context.interaction.edit_initial_response(content=None, embed=pages[0], components=None) + else: + await context.respond(pages[0]) + +def load(bot): + bot.add_plugin(plugin) +def unload(bot): + bot.remove_plugin(plugin) diff --git a/commands/master.py b/commands/master.py new file mode 100644 index 0000000..8fa763e --- /dev/null +++ b/commands/master.py @@ -0,0 +1,32 @@ +import os +import lightbulb + + +plugin = lightbulb.Plugin('master') + + +@plugin.command +@lightbulb.option('command', 'What is your command, master?', required=False, choices=('reload', 'sleep')) +@lightbulb.command('master', 'Commands my master can demand of me', ephemeral=True) +@lightbulb.implements(lightbulb.SlashCommand) +async def master(context): + if context.user.id == context.bot.application.owner.id: + match context.options.command: + case 'reload': + context.bot.reload_extensions(*context.bot.extensions) + + extensions = [os.path.splitext(extension)[1][1:] for extension in context.bot.extensions] + await context.respond(f'**Reloaded `{"`, `".join(extensions[:-1])}`, and `{extensions[-1]}` for you, master.**') + case 'sleep': + await context.respond('**Goodnight, master.**') + await context.bot.close() + case _: + await context.respond(f'**Hello, master.**') + else: + await context.respond(f'**{context.bot.application.owner.mention} is my master. 🐺**') + + +def load(bot): + bot.add_plugin(plugin) +def unload(bot): + bot.remove_plugin(plugin) diff --git a/config.py b/config.py new file mode 100644 index 0000000..72b0ee7 --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +import toml + +ERROR = '```❗ An internal error has occurred. This has been reported to my master. 🐺```' +CONFIG = '''\ +guilds = [] # guild IDs to register commands, empty for global +client = 0 # bot application ID +token = "" # bot token +activity = "" # bot status +saucenao = "" # saucenao token +e621 = "" # e621 token +''' + + +try: + config = toml.load('config.toml') +except FileNotFoundError: + with open('config.toml', 'w') as f: + f.write(CONFIG) + print('config.toml created with default values. Restart when modified.') + exit() + + +def error(event): + exception = event.exception.__cause__ or event.exception + + return (f'**`{event.context.command.name}` in {event.context.get_channel().mention}' + f'```❗ {type(exception).__name__}: {exception}```**') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9b87ff5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "modufur" +version = "0.1.0" +description = "Modufur Discord Bot" +authors = ["Myned "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~3.10" +toml = "*" +uvloop = "*" +aiohttp = "*" +urlextract = "*" +tldextract = "*" +hikari = {extras = ["speedups"], version = "*"} +hikari-lightbulb = "*" +hikari-miru = "*" +pysaucenao = {git = "https://github.com/Myned/pysaucenao.git"} + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/run.py b/run.py new file mode 100644 index 0000000..36dd8da --- /dev/null +++ b/run.py @@ -0,0 +1,34 @@ +import os +import hikari +import lightbulb +import miru + +import config as c + + +# Unix optimizations +# https://github.com/hikari-py/hikari#uvloop +if os.name != 'nt': + import uvloop + uvloop.install() + +bot = lightbulb.BotApp( + token=c.config['token'], + default_enabled_guilds=c.config['guilds']) + + +@bot.listen(lightbulb.CommandErrorEvent) +async def on_error(event): + await bot.application.owner.send(c.error(event)) + + try: + await event.context.respond(c.ERROR) + except: + pass + + raise event.exception + + +miru.load(bot) +bot.load_extensions_from('tools', 'commands') +bot.run(activity=hikari.Activity(name=c.config['activity'], type=hikari.ActivityType.LISTENING)) diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/cogs/__init__.py b/src/cogs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/cogs/booru.py b/src/cogs/booru.py deleted file mode 100644 index 005a2fd..0000000 --- a/src/cogs/booru.py +++ /dev/null @@ -1,1544 +0,0 @@ -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.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(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - self.LIMIT = 100 - self.HISTORY_LIMIT = 150 - 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': {}, 'channel': {}, 'user': {}}) - - 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()) - - def _get_icon(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 _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(f'https://e621.net/tag/index.json?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(f'https://e621.net/tag/index.json?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 - - 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]) - - elif isinstance(temp[1], d.Message): - for match in re.finditer('(https?:\/\/[^ ]*\.(?:gif|png|jpg|jpeg))', temp[1].content): - await temp[0].send(match) - - for attachment in temp[1].attachments: - await temp[0].send(attachment.url) - - print('STOPPED : hearting') - - async def queue_for_hearts(self, *, message=None, send=None, channel=None, reaction=True, timeout=60 * 60 * 24): - 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)) - - @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)) - 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)) - # else: - # raise exc.Exists - # - # except exc.Exists: - # await ctx.send('**Already auto-posting in {}.** Type `stop` to stop.'.format(ctx.channel.mention)) - # await u.add_reaction(ctx.message, '\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) - tags = kwargs['remaining'] - related = [] - c = 0 - - await ctx.trigger_typing() - - for tag in tags: - tag_request = await u.fetch(f'https://e621.net/tag/related.json?tags={tag}', json=True) - for rel in tag_request.get(tag, []): - related.append(rel[0]) - - if related: - await ctx.send('`{}` **related tags:**\n```\n{}```'.format(tag, ' '.join(related))) - else: - await ctx.send(f'**No related tags found for:** `{tag}`') - - related.clear() - c += 1 - - if not c: - await u.add_reaction(ctx.message, '\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) - tags = kwargs['remaining'] - aliases = [] - c = 0 - - await ctx.trigger_typing() - - for tag in tags: - alias_request = await u.fetch(f'https://e621.net/tag_alias/index.json?aliased_to={tag}&approved=true', json=True) - for dic in alias_request: - aliases.append(dic['name']) - - if aliases: - await ctx.send('`{}` **aliases:**\n```\n{}```'.format(tag, ' '.join(aliases))) - else: - await ctx.send(f'**No aliases found for:** `{tag}`') - - aliases.clear() - c += 1 - - if not c: - await u.add_reaction(ctx.message, '\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)) - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @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) - posts = kwargs['remaining'] - - if not posts: - raise exc.MissingArgument - - for ident in posts: - await ctx.trigger_typing() - - ident = ident if not ident.isdigit() else re.search( - 'show/([0-9]+)', ident).group(1) - post = await u.fetch(f'https://e621.net/posts/{ident}.json', json=True) - post = post['post'] - - embed = d.Embed( - title=', '.join(post['tags']['artist']), url=f'https://e621.net/posts/{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["file"]["width"]} x {post["file"]["height"]}', - url=f'https://e621.net/posts?tags=ratio:{post["file"]["width"]/post["file"]["height"]:.2f}', icon_url=ctx.author.avatar_url) - embed.set_footer(text=post['score']['total'], - icon_url=self._get_icon(post['score']['total'])) - - except exc.MissingArgument: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid url**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @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) - urls = kwargs['remaining'] - c = 0 - - if not urls: - raise exc.MissingArgument - - for url in urls: - await ctx.trigger_typing() - - await ctx.send(await scraper.get_image(url)) - - c += 1 - - # except - # await ctx.send(f'**No aliases found for:** `{tag}`') - - if not c: - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - except exc.MissingArgument: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid url or file**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @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.id is ctx.author.id: - 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.id is ctx.author.id and msg.channel.id is ctx.channel.id - - try: - kwargs = u.get_kwargs(ctx, args) - query = kwargs['remaining'] - ident = None - - await ctx.trigger_typing() - - pools = [] - pool_request = await u.fetch(f'https://e621.net/pools.json?search[name_matches]={" ".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 u.add_reaction(ctx.message, '\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() - - with suppress(err.Forbidden): - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - with suppress(err.Forbidden): - await selection.delete() - elif pool_request: - tempool = pool_request[0] - else: - raise exc.NotFound - - await ctx.send(f'**{tempool["name"]}**\nhttps://e621.net/pools/{tempool["id"]}') - - except exc.Abort as e: - await e.message.edit(content='\N{NO ENTRY SIGN}') - - # Reverse image searches a linked image using the public iqdb - @cmds.cooldown(1, 5, cmds.BucketType.member) - @cmds.command(name='reverse', aliases=['rev', 'ris'], brief='Reverse image search from Kheina and SauceNAO', description='NSFW\nReverse-search an image with given URL') - async def reverse(self, ctx, *args): - try: - kwargs = u.get_kwargs(ctx, args) - urls, remove = kwargs['remaining'], kwargs['remove'] - c = 0 - - if not urls and not ctx.message.attachments: - raise exc.MissingArgument - - for attachment in ctx.message.attachments: - urls.append(attachment.url) - - async with ctx.channel.typing(): - for url in urls: - try: - result = await scraper.get_post(url) - - embed = d.Embed( - title=result['artist'], - url=result['source'], - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=result['thumbnail']) - embed.set_author(name=result['similarity'] + '% Match', icon_url=ctx.author.avatar_url) - embed.set_footer(text=result['database']) - - await ctx.send(embed=embed) - - c += 1 - - except exc.MatchError as e: - await ctx.send('**No probable match for:** `{}`'.format(e)) - - if not c: - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - elif remove: - with suppress(err.NotFound): - await ctx.message.delete() - - except exc.MissingArgument: - await ctx.send( - '\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid url or file.**\n' - 'Be sure the link directs to an image file') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.SizeError as e: - await ctx.send(f'`{e}` **too large.**\nMaximum is 8 MB') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except err.HTTPException: - await ctx.send( - '\N{HEAVY EXCLAMATION MARK SYMBOL} **Search engines returned an unexpected result.**\n' - 'They may be offline') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.ImageError: - await ctx.send( - '\N{HEAVY EXCLAMATION MARK SYMBOL} **Search engines were denied access to this file.**\n' - 'Try opening it in a browser and uploading the file to Discord') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @cmds.command(name='reversify', aliases=['revify', 'risify', 'rify']) - @cmds.cooldown(1, 5, cmds.BucketType.member) - async def reversify(self, ctx, *args): - try: - dest = ctx - kwargs = u.get_kwargs(ctx, args, limit=5) - remove, limit = 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 - async with ctx.channel.typing(): - for message, urls in links.items(): - for url in urls: - try: - result = await scraper.get_post(url) - - embed = d.Embed( - title=result['artist'], - url=result['source'], - color=ctx.me.color if isinstance(ctx.channel, d.TextChannel) else u.color) - embed.set_image(url=result['thumbnail']) - embed.set_author(name=result['similarity'] + '% Match', icon_url=ctx.author.avatar_url) - embed.set_footer(text=result['database']) - - await dest.send(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 dest.send('`{} / {}` **No probable match for:** `{}`'.format(n, len(links), e)) - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - except exc.SizeError as e: - await dest.send(f'`{e}` **too large.**\nMaximum is 8 MB') - await message.add_reaction('\N{CROSS MARK}') - c -= 1 - - finally: - n += 1 - - if c <= 0: - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - except exc.NotFound: - await dest.send('**No matches found**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.BoundsError as e: - await dest.send('`{}` **invalid limit.**\nQuery limited to 5'.format(e)) - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except err.HTTPException: - await dest.send( - '\N{HEAVY EXCLAMATION MARK SYMBOL} **Search engines returned an unexpected result.**\n' - 'They may be offline') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.ImageError: - await ctx.send( - '\N{HEAVY EXCLAMATION MARK SYMBOL} **Search engines were denied access to this file.**\n' - 'Try opening it in a browser and uploading the file to Discord') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - 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) - - async with message.channel.typing(): - for url in urls: - try: - result = await scraper.get_post(url) - - embed = d.Embed( - title=result['artist'], - url=result['source'], - color=message.me.color if isinstance(message.channel, d.TextChannel) else u.color) - embed.set_image(url=result['thumbnail']) - embed.set_author(name=result['similarity'] + '% Match', icon_url=message.author.avatar_url) - embed.set_footer(text=result['database']) - - await message.channel.send(embed=embed) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - with suppress(err.NotFound): - await message.delete() - - except exc.MatchError as e: - await message.channel.send('**No probable match for:** `{}`'.format(e)) - await message.add_reaction('\N{CROSS MARK}') - except exc.SizeError as e: - await message.channel.send(f'`{e}` **too large.** Maximum is 8 MB') - await message.add_reaction('\N{HEAVY EXCLAMATION MARK SYMBOL}') - except Exception: - await message.channel.send(f'**An unknown error occurred.**') - 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)) - - @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)) - else: - await ctx.send('**Already auto-reversifying in {}.** Type `stop r(eversifying)` to stop.'.format(ctx.channel.mention)) - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - async def _get_pool(self, ctx, *, booru='e621', query=[]): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and user.id is ctx.author.id: - 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.id is ctx.author.id and msg.channel.id is ctx.channel.id - - posts = {} - pool = {} - - try: - pools = [] - pool_request = await u.fetch(f'https://{booru}.net/pools.json?search[name_matches]={" ".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 u.add_reaction(ctx.message, '\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() - - with suppress(err.Forbidden): - await match.delete() - tempool = [pool for pool in pool_request if pool['name'] - == pools[int(selection.content) - 1]][0] - with suppress(err.Forbidden): - await selection.delete() - pool = {'name': tempool['name'], 'id': tempool['id']} - - await ctx.trigger_typing() - elif pool_request: - tempool = pool_request[0] - pool = {'name': pool_request[0] - ['name'], 'id': pool_request[0]['id']} - else: - raise exc.NotFound - - for ident in tempool['post_ids']: - post = await u.fetch(f'https://{booru}.net/posts/{ident}.json', json=True) - post = post['post'] - posts[post['id']] = {'artist': ', '.join( - post['tags']['artist']), 'file_url': post['file']['url'], 'score': post['score']['total']} - - await asyncio.sleep(0.5) - - return pool, posts - - except exc.Abort as e: - await e.message.delete() - 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={}): - blacklist = set() - # Creates temp blacklist based on context - for lst in ('blacklist', 'aliases'): - default = set() if lst == 'blacklist' else {} - - for bl in (self.blacklists['global'].get(lst, default), - self.blacklists['channel'].get(ctx.channel.id, {}).get(lst, default), - self.blacklists['user'].get(ctx.author.id, {}).get(lst, default)): - if lst == 'aliases': - temp = list(bl.keys()) + [tag for tags in bl.values() for tag in tags] - temp = set(temp) - else: - temp = bl - - blacklist.update(temp) - # 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) > 40): - raise exc.TagBoundsError(' '.join(tags[40:])) - 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(f'https://{booru}.net/posts.json?tags={"+".join([order] + tags)}&limit={int(320)}', json=True) - if len(request['posts']) == 0: - raise exc.NotFound(' '.join(tags)) - if len(request['posts']) < limit: - limit = len(request['posts']) - - for post in request['posts']: - if 'swf' in post['file']['ext'] or 'webm' in post['file']['ext']: - continue - try: - post_tags = [tag for tags in post['tags'].values() for tag in tags] - 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['tags']['artist']) if post['tags']['artist'] else 'unknown', - 'file_url': post['file']['url'], - 'score': post['score']['total']} - 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(' '.join(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') - @cmds.cooldown(1, 5, cmds.BucketType.member) - 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.id is ctx.author.id 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.id is ctx.author.id: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user.id is ctx.author.id: - raise exc.Left - elif reaction.emoji == '\N{NUMBER SIGN}\N{COMBINING ENCLOSING KEYCAP}' and reaction.message.id == paginator.id and user.id is ctx.author.id: - raise exc.GoTo - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user.id is ctx.author.id: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author.id is ctx.author.id and msg.channel.id is ctx.channel.id - - try: - kwargs = u.get_kwargs(ctx, args) - query = kwargs['remaining'] - hearted = {} - c = 1 - - if not args: - raise exc.MissingArgument - - async with ctx.channel.typing(): - pool, posts = await self._get_pool(ctx, booru='e621', query=query) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://e621.net/posts/{}'.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=pool['name'], - url='https://e621.net/pools/{}'.format(pool['id']), icon_url=ctx.author.avatar_url) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_icon(values[c - 1]['score'])) - - paginator = await ctx.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 u.add_reaction(ctx.message, '\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=8*60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=8*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/posts/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_icon(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=8*60) - - if int(number.content) != 0: - c = int(number.content) - - embed.title = values[c - 1]['artist'] - embed.url = 'https://e621.net/posts/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_icon(values[c - 1]['score'])) - embed.set_image(url=values[c - 1]['file_url']) - - if ctx.channel is d.TextChannel: - with suppress(errext.CheckFailure): - 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/posts/{}'.format( - keys[c - 1]) - embed.set_footer(text='{} / {}'.format(c, len(posts)), - icon_url=self._get_icon(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 ctx.send('\N{WHITE HEAVY CHECK MARK}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await ctx.send('\N{HOURGLASS}') - except exc.MissingArgument: - await ctx.send('**Missing argument**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.NotFound: - await ctx.send('**Pool not found**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.Continue: - pass - - finally: - if hearted: - await u.add_reaction(ctx.message, '\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - async def _get_paginator(self, ctx, args, booru='e621'): - def on_reaction(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and reaction.message.id == ctx.message.id and (user.id is ctx.author.id 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.id is ctx.author.id: - raise exc.Save - elif reaction.emoji == '\N{LEFTWARDS BLACK ARROW}' and reaction.message.id == paginator.id and user.id is ctx.author.id: - raise exc.Left - elif reaction.emoji == '\N{BLACK RIGHTWARDS ARROW}' and reaction.message.id == paginator.id and user.id is ctx.author.id: - raise exc.Right - return False - - def on_message(msg): - return msg.content.isdigit() and 0 <= int(msg.content) <= len(posts) and msg.author.id is ctx.author.id and msg.channel.id is ctx.channel.id - - try: - kwargs = u.get_kwargs(ctx, args) - tags = 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=booru, tags=tags, limit=limit) - keys = list(posts.keys()) - values = list(posts.values()) - - embed = d.Embed( - title=values[c - 1]['artist'], url='https://{}.net/posts/{}'.format(booru, 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=' '.join(tags) if tags else order, - url='https://{}.net/posts?tags={}'.format(booru, '+'.join(tags) if tags else order), icon_url=ctx.author.avatar_url) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_icon(values[c - 1]['score'])) - - paginator = await ctx.send(embed=embed) - - for emoji in ('\N{HEAVY BLACK HEART}', '\N{LEFTWARDS BLACK ARROW}', '\N{BLACK RIGHTWARDS ARROW}'): - await paginator.add_reaction(emoji) - await u.add_reaction(ctx.message, '\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=8*60), - self.bot.wait_for('reaction_remove', check=on_reaction, timeout=8*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://{}.net/posts/{}'.format( - booru, - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_icon(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.Right: - try: - if c % limit == 0: - await ctx.trigger_typing() - temposts, order = await self._get_posts(ctx, booru=booru, 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://{}.net/posts/{}'.format( - booru, - keys[c - 1]) - embed.set_footer(text=values[c - 1]['score'], - icon_url=self._get_icon(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 ctx.send('\N{HOURGLASS}') - except asyncio.TimeoutError: - try: - await paginator.edit(content='\N{HOURGLASS}') - except UnboundLocalError: - await ctx.send('\N{HOURGLASS}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e)) - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.TagBlacklisted as e: - await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**'.format(e)) - await u.add_reaction(ctx.message, '\N{NO ENTRY SIGN}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 40.'.format(e)) - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - finally: - if hearted: - await u.add_reaction(ctx.message, '\N{HOURGLASS WITH FLOWING SAND}') - - n = 1 - for embed in hearted.values(): - await ctx.author.send(content=f'`{n} / {len(hearted)}`', embed=embed) - n += 1 - - @cmds.command(name='e621page', aliases=['e621p', 'e6p', '6p']) - @checks.is_nsfw() - @cmds.cooldown(1, 5, cmds.BucketType.member) - async def e621_paginator(self, ctx, *args): - await self._get_paginator(ctx, args, booru='e621') - - - @cmds.command(name='e926page', aliases=['e926p', 'e9p', '9p']) - @cmds.cooldown(1, 5, cmds.BucketType.member) - async def e926_paginator(self, ctx, *args): - await self._get_paginator(ctx, args, booru='e926') - - async def _get_images(self, ctx, args, booru='e621'): - try: - kwargs = u.get_kwargs(ctx, args, limit=3) - args, limit = kwargs['remaining'], kwargs['limit'] - - tags = self._get_favorites(ctx, args) - - await ctx.trigger_typing() - - posts, order = await self._get_posts(ctx, booru=booru, tags=tags, limit=limit) - - for ident, post in posts.items(): - embed = d.Embed(title=post['artist'], url='https://{}.net/posts/{}'.format(booru, 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=' '.join(tags) if tags else order, - url='https://{}.net/posts?tags={}'.format(booru, '+'.join(tags) if tags else order), icon_url=ctx.author.avatar_url) - embed.set_footer( - text=post['score'], icon_url=self._get_icon(post['score'])) - - message = await ctx.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)) - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.BoundsError as e: - await ctx.send('`{}` **out of bounds.** Images limited to 3.'.format(e)) - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.TagBoundsError as e: - await ctx.send('`{}` **out of bounds.** Tags limited to 40.'.format(e)) - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - except exc.NotFound as e: - await ctx.send('`{}` **not found**'.format(e)) - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.FavoritesNotFound: - await ctx.send('**You have no favorite tags**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except exc.Timeout: - await ctx.send('**Request timed out**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - # 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() - @cmds.cooldown(1, 5, cmds.BucketType.member) - async def e621(self, ctx, *args): - await self._get_images(ctx, args, booru='e621') - - # 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])') - @cmds.cooldown(1, 5, cmds.BucketType.member) - async def e926(self, ctx, *args): - await self._get_images(ctx, args, booru='e926') - - # @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)) - # await u.add_reaction(ctx.message, '\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): - # await ctx.send('\N{WHITE MEDIUM STAR} {}**\'s favorite tags:**\n```\n{}```'.format(ctx.author.mention, ' '.join(self.favorites.get(ctx.author.id, {}).get('tags', set())))) - # - # @_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) - # tags = 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 ctx.send('{} **added to their favorites:**\n```\n{}```'.format(ctx.author.mention, ' '.join(tags))) - # - # except exc.BoundsError: - # await ctx.send('**Favorites list currently limited to:** `5`') - # await u.add_reaction(ctx.message, '\N{CROSS MARK}') - # except exc.TagBlacklisted as e: - # await ctx.send('\N{NO ENTRY SIGN} `{}` **blacklisted**') - # await u.add_reaction(ctx.message, '\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) - # tags = 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 ctx.send('{} **removed from their favorites:**\n```\n{}```'.format(ctx.author.mention, ' '.join(tags))) - # - # except KeyError: - # await ctx.send('**You do not have any favorites**') - # await u.add_reaction(ctx.message, '\N{CROSS MARK}') - # except exc.TagError as e: - # await ctx.send('`{}` **not in favorites**'.format(e)) - # await u.add_reaction(ctx.message, '\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): - # with suppress(KeyError): - # del self.favorites[ctx.author.id] - # u.dump(self.favorites, 'cogs/favorites.pkl') - # - # await ctx.send('{}**\'s favorites cleared**'.format(ctx.author.mention)) - # - # @_clear_favorite.command(name='posts', aliases=['p']) - # async def __clear_favorite_posts(self, ctx): - # pass - - @cmds.group( - aliases=['bl', 'b'], - brief='(G) Manage blacklists', - description='Manage global, guild (WIP), channel, and personal blacklists', - usage='[option] [blacklist] [--aliases|-a] [tags...]') - async def blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send( - '**Use a flag to manage blacklists.**\n' - f'*Type* `{ctx.prefix}help bl` *for more info.*') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - elif not ctx.args: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Missing arguments**') - - @blacklist.group( - name='get', - aliases=['g'], - brief='Get a blacklist', - description='Get global, channel, or personal blacklists', - usage='[blacklist]') - async def get_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid blacklist**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @get_blacklist.command( - name='global', - aliases=['gl', 'g'], - brief='Get global blacklist', - description='Get global blacklist\n\n' - 'In accordance with Discord\'s ToS: cub, related tags, and their aliases are blacklisted') - async def get_global_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - blacklist = self.blacklists['global'].get(lst, default) - - if blacklist: - await formatter.paginate( - ctx, - blacklist, - start=f'\N{NO ENTRY SIGN} **Global {lst}:**') - else: - await ctx.send(f'\N{CROSS MARK} **No global {lst} found**') - - @get_blacklist.command( - name='channel', - aliases=['chan', 'ch', 'c'], - brief='Get channel blacklist', - description='Get channel blacklist') - async def get_channel_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - blacklist = self.blacklists['channel'].get(ctx.channel.id, {}).get(lst, default) - - if blacklist: - await formatter.paginate( - ctx, - blacklist, - start=f'\N{NO ENTRY SIGN} {ctx.channel.mention} **{lst}:**') - else: - await ctx.send(f'\N{CROSS MARK} **No {lst} found for {ctx.channel.mention}**') - - @get_blacklist.command( - name='me', - aliases=['m'], - brief='Get your personal blacklist', - description='Get your personal blacklist') - async def get_user_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - blacklist = self.blacklists['user'].get(ctx.author.id, {}).get(lst, default) - - if blacklist: - await formatter.paginate( - ctx, - blacklist, - start=f'\N{NO ENTRY SIGN} {ctx.author.mention}**\'s {lst}:**') - else: - await ctx.send(f'\N{CROSS MARK} **No {lst} found for {ctx.author.mention}**') - - @blacklist.group( - name='add', - aliases=['a'], - brief='Add tags to a blacklist', - description='Add tags to global, channel, or personal blacklists', - usage='[blacklist] [tags...]') - async def add_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('\N{CROSS MARK} **Invalid blacklist**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - async def _add(self, tags, lst, alias=False): - if not alias: - if tags: - lst.update(tags) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - return tags - else: - aliases = {} - - if tags: - for tag in tags: - request = await u.fetch( - f'https://e621.net/tag_alias/index.json?aliased_to={tag}&approved=true', - json=True) - - for elem in request: - if elem['name']: - aliases.setdefault(tag, set()).add(elem['name']) - - if aliases: - lst.update(aliases) - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - return list(aliases.keys()) - - @add_tags.command( - name='global', - aliases=['gl', 'g'], - brief='Add tags to global blacklist', - description='Add tags to global blacklist', - usage='[tags...]') - @cmds.is_owner() - async def add_global_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - added = await self._add( - tags, - self.blacklists['global'].setdefault(lst, default), - alias=True if lst == 'aliases' else False) - - await formatter.paginate( - ctx, - added, - start=f'\N{WHITE HEAVY CHECK MARK} **Added to global {lst}:**') - - @add_tags.command( - name='channel', - aliases=['chan', 'ch', 'c'], - brief='Add tags to channel blacklist', - description='Add tags to channel blacklist', - usage='[tags...]') - @cmds.has_permissions(manage_channels=True) - async def add_channel_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - added = await self._add( - tags, - self.blacklists['channel'].setdefault(ctx.channel.id, {}).setdefault(lst, default), - alias=True if lst == 'aliases' else False) - - await formatter.paginate( - ctx, - added, - start=f'\N{WHITE HEAVY CHECK MARK} **Added to {ctx.channel.mention} {lst}:**') - - @add_tags.command( - name='me', - aliases=['m'], - brief='Add tags to personal blacklist', - description='Add tags to personal blacklist', - usage='[tags...]') - async def add_user_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - added = await self._add( - tags, - self.blacklists['user'].setdefault(ctx.author.id, {}).setdefault(lst, default), - alias=True if lst == 'aliases' else False) - - await formatter.paginate( - ctx, - added, - start=f'\N{WHITE HEAVY CHECK MARK} **Added to {ctx.author.mention}\'s {lst}:**') - - @blacklist.group( - name='remove', - aliases=['rm', 'r'], - brief='Remove tags from a blacklist', - description='Remove tags from global, channel, or personal blacklists', - usage='[blacklist] [tags...]') - async def remove_tags(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid blacklist**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - def _remove(self, remove, lst): - removed = set() - - if remove: - if type(lst) is set: - for tag in remove: - with suppress(KeyError): - lst.remove(tag) - removed.add(tag) - else: - temp = copy.deepcopy(lst) - for k in temp.keys(): - if k in remove: - with suppress(KeyError): - del lst[k] - removed.add(k) - else: - lst[k] = set([tag for tag in lst[k] if tag not in remove]) - lst = temp - removed.update([tag for k, v in lst.items() for tag in v if tag in remove]) - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - return removed - - @remove_tags.command( - name='global', - aliases=['gl', 'g'], - brief='Remove tags from global blacklist', - description='Remove tags from global blacklist', - usage='[tags...]') - @cmds.is_owner() - async def remove_global_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - removed = self._remove( - tags, - self.blacklists['global'].get(lst, default)) - - await formatter.paginate( - ctx, - removed, - start=f'\N{WHITE HEAVY CHECK MARK} **Removed from global {lst}:**') - - @remove_tags.command( - name='channel', - aliases=['ch', 'c'], - brief='Remove tags from channel blacklist', - description='Remove tags from channel blacklist', - usage='[tags...]') - @cmds.has_permissions(manage_channels=True) - async def remove_channel_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - removed = self._remove( - tags, - self.blacklists['channel'].get(ctx.channel.id, {}).get(lst, default)) - - await formatter.paginate( - ctx, - removed, - start=f'\N{WHITE HEAVY CHECK MARK} **Removed from {ctx.channel.mention} {lst}:**') - - @remove_tags.command( - name='me', - aliases=['m'], - brief='Remove tags from personal blacklist', - description='Remove tags from personal blacklist', - usage='[tags...]') - async def remove_user_tags(self, ctx, *args): - tags, lst = u.kwargs(args) - default = set() if lst == 'blacklist' else {} - - async with ctx.channel.typing(): - removed = self._remove( - tags, - self.blacklists['user'].get(ctx.author.id, {}).get(lst, default)) - - await formatter.paginate( - ctx, - removed, - start=f'\N{WHITE HEAVY CHECK MARK} **Removed from {ctx.author.mention}\'s {lst}:**') - - @blacklist.group( - name='clear', - aliases=['cl', 'c'], - brief='Delete a blacklist', - description='Delete global, channel, or personal blacklists', - usage='[blacklist]') - async def clear_blacklist(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send('\N{HEAVY EXCLAMATION MARK SYMBOL} **Invalid blacklist**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @clear_blacklist.command( - name='global', - aliases=['gl', 'g'], - brief='Delete global blacklist', - description='Delete global blacklist') - @cmds.is_owner() - async def clear_global_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - - async with ctx.channel.typing(): - with suppress(KeyError): - del self.blacklists['global'][lst] - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **Global {lst} cleared**') - - @clear_blacklist.command( - name='channel', - aliases=['ch', 'c'], - brief='Delete channel blacklist', - description='Delete channel blacklist') - @cmds.has_permissions(manage_channels=True) - async def clear_channel_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - - async with ctx.channel.typing(): - with suppress(KeyError): - del self.blacklists['channel'][ctx.channel.id][lst] - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **{ctx.channel.mention} {lst} cleared**') - - @clear_blacklist.command( - name='me', - aliases=['m'], - brief='Delete your personal blacklist', - description='Delete your personal blacklist') - async def clear_user_blacklist(self, ctx, *args): - args, lst = u.kwargs(args) - - async with ctx.channel.typing(): - with suppress(KeyError): - del self.blacklists['user'][ctx.author.id][lst] - - u.dump(self.blacklists, 'cogs/blacklists.pkl') - - await ctx.send(f'\N{WHITE HEAVY CHECK MARK} **{ctx.author.mention}\'s {lst} cleared**') diff --git a/src/cogs/help.py b/src/cogs/help.py deleted file mode 100644 index b0b6d67..0000000 --- a/src/cogs/help.py +++ /dev/null @@ -1,5 +0,0 @@ -from discord.ext import commands as cmds - - -class Help(cmds.HelpCommand): - pass diff --git a/src/cogs/info.py b/src/cogs/info.py deleted file mode 100644 index ef741eb..0000000 --- a/src/cogs/info.py +++ /dev/null @@ -1,41 +0,0 @@ -import asyncio -import traceback as tb - -import discord as d -from discord.ext import commands as cmds - -from misc import exceptions as exc -from utils import utils as u - - -class Info(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - - # @cmds.command(name='helptest', aliases=['h'], hidden=True) - # async def list_commands(self, ctx): - # embed = d.Embed(title='All possible commands:', color=ctx.me.color) - # embed.set_author(name=ctx.me.display_name, icon_url=ctx.me.avatar_url) - # embed.add_field( - # name='Booru', value='\n{}bl umbrella command for managing blacklists'.format(u.config['prefix'])) - # - # await ctx.send(embed=embed) - - @cmds.group(name='info', aliases=['i'], hidden=True) - async def info(self, ctx): - if ctx.invoked_subcommand is None: - await ctx.send('BOT INFO') - - @info.command(aliases=['g'], brief='Provides info about a guild') - async def guild(self, ctx, guild_id: int): - guild = d.utils.get(self.bot.guilds, id=guild_id) - - if guild: - await ctx.send(guild.name) - else: - await ctx.send(f'**Not in any guilds by the id of: ** `{guild_id}`') - - @info.command(aliases=['u'], brief='Provides info about a user') - async def user(self, ctx, user: d.User): - pass diff --git a/src/cogs/management.py b/src/cogs/management.py deleted file mode 100644 index fb6d61f..0000000 --- a/src/cogs/management.py +++ /dev/null @@ -1,233 +0,0 @@ -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 Admin(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - 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.id is ctx.author.id: - 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**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except TimeoutError: - await ctx.send('**Deletion timed out**') - await u.add_reaction(ctx.message, '\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.id is ctx.author.id: - 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**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - except TimeoutError: - await ctx.send('**Deletion timed out**') - await u.add_reaction(ctx.message, '\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() - 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)) - - @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)) - else: - raise exc.Exists - - except exc.Exists: - await ctx.send('**Already auto-deleting in {}.** Type `stop d(eleting)` to stop.'.format(ctx.channel.mention)) - await u.add_reaction(ctx.message, '\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 deleted file mode 100644 index a59c60b..0000000 --- a/src/cogs/owner.py +++ /dev/null @@ -1,322 +0,0 @@ -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 -from discord.ext import commands as cmds - -from misc import exceptions as exc -from misc import checks -from utils import utils as u -from utils import formatter - - -class Bot(cmds.Cog): - - 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 u.add_reaction(ctx.message, '\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() - print('\n< < < < < < < < < < < <\nD I S C O N N E C T E D\n< < < < < < < < < < < <\n') - await self.bot.logout() - - @cmds.command(name=',restart', aliases=[',res', ',r'], hidden=True) - @cmds.is_owner() - async def restart(self, ctx): - await u.add_reaction(ctx.message, '\N{SLEEPING SYMBOL}') - - print('\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\nR E S T A R T I N G\n^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n') - - 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() - 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 u.add_reaction(ctx.message, '\N{ENVELOPE}') - - await ctx.send('https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}'.format(u.config['client_id'], u.config['permissions'])) - - @cmds.command(name=',guilds', aliases=[',gs']) - @cmds.is_owner() - async def guilds(self, ctx): - paginator = cmds.Paginator() - - for guild in self.bot.guilds: - paginator.add_line(f'{guild.name}: {guild.id}\n' - f' @{guild.owner}: {guild.owner.id}') - - for page in paginator.pages: - await ctx.send(f'**Guilds:**\n{page}') - - @cmds.group(name=',block', aliases=[',bl', ',b']) - @cmds.is_owner() - async def block(self, ctx): - pass - - @block.group(name='list', aliases=['l']) - async def block_list(self, ctx): - pass - - @block_list.command(name='guilds', aliases=['g']) - async def block_list_guilds(self, ctx): - await formatter.paginate(ctx, u.block['guild_ids']) - - @block.command(name='user', aliases=['u']) - async def block_user(self, ctx, *users: d.User): - for user in users: - u.block['user_ids'].append(user.id) - - u.dump(u.block, 'cogs/block.json', json=True) - - @block.command(name='guild', aliases=['g']) - async def block_guild(self, ctx, *guilds): - for guild in guilds: - u.block['guild_ids'].append(guild) - - u.dump(u.block, 'cogs/block.json', json=True) - - @cmds.group(name=',unblock', aliases=[',unbl', ',unb']) - @cmds.is_owner() - async def unblock(self, ctx): - pass - - @unblock.command(name='user', aliases=['u']) - async def unblock_user(self, ctx, *users: d.User): - for user in users: - u.block['user_ids'].remove(user.id) - - u.dump(u.block, 'cogs/block.json', json=True) - - await ctx.send('\N{WHITE HEAVY CHECK MARK} **Unblocked users**') - - @unblock.command(name='guild', aliases=['g']) - async def unblock_guild(self, ctx, *guilds): - for guild in guilds: - u.block['guild_ids'].remove(guild) - - u.dump(u.block, 'cogs/block.json', json=True) - - await ctx.send('\N{WHITE HEAVY CHECK MARK} **Unblocked guilds**') - - @cmds.command(name=',leave', aliases=[',l']) - @cmds.is_owner() - async def leave(self, ctx, *guilds): - for guild in guilds: - temp = d.utils.get(self.bot.guilds, id=int(guild)) - - await temp.leave() - - @cmds.command(name=',permissions', aliases=[',permission', ',perms', ',perm']) - @cmds.is_owner() - async def permissions(self, ctx, *args: d.Member): - members = list(args) - permissions = {} - - if not members: - members.append(ctx.guild.me) - - for member in members: - permissions[member.mention] = [] - - for k, v in dict(ctx.channel.permissions_for(member)).items(): - if v: - permissions[member.mention].append(k) - - await formatter.paginate(ctx, permissions) - - @cmds.command(name=',tasks', aliases=[',task']) - @cmds.is_owner() - async def tasks(self, ctx): - tasks = [task for task in asyncio.Task.all_tasks() if not task.done()] - - await ctx.send(f'**Tasks active:** `{int((len(tasks) - 6) / 3)}`') - - @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**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - -class Tools(cmds.Cog): - - 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.id is ctx.author.id and msg.channel.id is ctx.channel.id: - msg.content = msg.content[5:] - return True - return False - - def evaluate(msg): - if msg.content.lower().startswith('eval ') and msg.author.id is ctx.author.id and msg.channel.id is ctx.channel.id: - msg.content = msg.content[5:] - return True - return False - - def exit(reaction, user): - if reaction.emoji == '\N{OCTAGONAL SIGN}' and user.id is ctx.author.id 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 u.add_reaction(ctx.message, '\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/cogs/periodic.py b/src/cogs/periodic.py deleted file mode 100644 index 000c1bb..0000000 --- a/src/cogs/periodic.py +++ /dev/null @@ -1,25 +0,0 @@ -import asyncio -import json -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 Post(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - - async def _check_posts(self, user, channel): - pass - - @cmds.group(aliases=['update', 'up', 'u']) - async def updates(self, ctx): - pass diff --git a/src/cogs/tools.py b/src/cogs/tools.py deleted file mode 100644 index ba70955..0000000 --- a/src/cogs/tools.py +++ /dev/null @@ -1,79 +0,0 @@ -import asyncio -from datetime import datetime as dt -import mimetypes -import os -import tempfile -import traceback as tb -import webbrowser - -import discord as d -from discord.ext import commands as cmds - -#from run import config -from cogs import booru -from misc import exceptions as exc -from misc import checks -from utils import utils as u -from utils import formatter - -youtube = None - -tempfile.tempdir = os.getcwd() - - -class Utils(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - - @cmds.command(name='lastcommand', aliases=['last', 'l', ','], brief='Reinvokes last successful command', description='Executes last successfully executed command') - async def last_command(self, ctx, arg='None'): - try: - context = u.last_commands[ctx.author.id] - - if arg == 'show' or arg == 'sh' or arg == 's': - await ctx.send(f'`{context.prefix}{context.invoked_with} {" ".join(context.args[2:])}`') - else: - await ctx.invoke(context.command, *context.args[2:], **context.kwargs) - - except KeyError: - await ctx.send('**No last command**') - await u.add_reaction(ctx.message, '\N{CROSS MARK}') - - # Displays latency - @cmds.command(aliases=['p'], brief='Pong!', description='Returns latency from bot to Discord servers, not to user') - async def ping(self, ctx): - await u.add_reaction(ctx.message, '\N{TABLE TENNIS PADDLE AND BALL}') - await ctx.send(ctx.author.mention + ' \N{TABLE TENNIS PADDLE AND BALL} `' + str(round(self.bot.latency * 1000)) + 'ms`') - - @cmds.command(aliases=['pre', 'prefixes'], brief='List bot prefixes', description='Shows all used prefixes') - async def prefix(self, ctx): - await ctx.send('**Prefix:** `{}`'.format('` or `'.join(u.settings['prefixes'][ctx.guild.id] if ctx.guild.id in u.settings['prefixes'] else u.config['prefix']))) - - @cmds.group(name=',send', aliases=[',s'], hidden=True) - @cmds.is_owner() - async def send(self, ctx): - pass - - @send.command(name='guild', aliases=['g', 'server', 's']) - async def send_guild(self, ctx, guild, channel, *, message): - try: - tempchannel = d.utils.find(lambda m: m.name == channel, d.utils.find( - lambda m: m.name == guild, self.bot.guilds).channels) - - try: - await tempchannel.send(message) - await ctx.message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - except AttributeError: - await ctx.send('**Invalid channel**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - except AttributeError: - await ctx.send('**Invalid guild**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - - @send.command(name='user', aliases=['u', 'member', 'm']) - async def send_user(self, ctx, user, *, message): - await d.utils.get(self.bot.get_all_members(), id=int(user)).send(message) - await ctx.message.add_reaction('\N{WHITE HEAVY CHECK MARK}') diff --git a/src/cogs/weeb.py b/src/cogs/weeb.py deleted file mode 100644 index 678404a..0000000 --- a/src/cogs/weeb.py +++ /dev/null @@ -1,90 +0,0 @@ -from selenium.webdriver import Chrome -from selenium.webdriver.chrome.options import Options - -import asyncio -import traceback as tb -from discord.ext import commands as cmds -from utils import utils as u - - -class Weeb(cmds.Cog): - - def __init__(self, bot): - self.bot = bot - self.weebing = False - - with open('id.json') as f: - self.id = int(f.readline()) - print('LOADED : id.json') - - if not self.weebing: - self.weebing = True - self.bot.loop.create_task(self.start()) - print('STARTED : weebing') - - async def refresh_switchmod(self, browser): - message = '' - urls = { - 'Novelties': 'https://switchmod.net/collections/ended-gbs/products/gmk-metaverse-2?variant=31671816880208', - 'Royal': 'https://switchmod.net/collections/ended-gbs/products/gmk-metaverse-2?variant=31671816945744' - } - - for item, url in urls.items(): - browser.get(url) - try: - status = browser.find_elements_by_css_selector('#addToCartText-product-template')[0].text - except IndexError: - status = 'SOLD OUT' - - if status != 'SOLD OUT': - message += f'{item} is in stock at Switchmod!\n<{url}>\n' - - await asyncio.sleep(5) - - return message - - async def refresh_deskhero(self, browser): - message = '' - url = 'https://www.deskhero.ca/products/gmk-metaverse-2' - - browser.get(url) - try: - royal_soldout = browser.find_elements_by_css_selector('#data-product-option-1-1')[0].get_attribute('data-soldout') - except IndexError: - royal_soldout = 'true' - try: - novelties_soldout = browser.find_elements_by_css_selector('#data-product-option-1-3')[0].get_attribute('data-soldout') - except IndexError: - novelties_soldout = 'true' - - if royal_soldout != 'true': - message += f'Royal is in stock at Deskhero!\n<{url}>\n' - if novelties_soldout != 'true': - message += f'Novelties is in stock at Deskhero!\n<{url}>\n' - - return message - - async def start(self): - try: - opts = Options() - opts.headless = True - browser = Chrome(executable_path='/usr/bin/chromedriver', options=opts) - - while self.weebing: - message = await self.refresh_switchmod(browser) - await asyncio.sleep(5) - message += await self.refresh_deskhero(browser) - - if message: - await self.bot.get_user(self.id).send(message) - await self.bot.get_user(u.config['owner_id']).send('Something is in stock. Restart to keep checking') - - browser.quit() - self.weebing = False - print('STOPPED : weebing') - - await asyncio.sleep(120) - - except Exception as e: - tb.print_exc() - await self.bot.get_user(u.config['owner_id']).send(f'! ERROR !\n\n{repr(e)}') diff --git a/src/misc/__init__.py b/src/misc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/misc/checks.py b/src/misc/checks.py deleted file mode 100644 index 9761f86..0000000 --- a/src/misc/checks.py +++ /dev/null @@ -1,52 +0,0 @@ -import asyncio -import json -import traceback -from contextlib import suppress - -import discord as d -from discord import errors as err -from discord.ext import commands -from discord.ext.commands import errors as errext - -from utils import utils as u - -owner_id = u.config['owner_id'] -ready = False - - -def is_owner(): - async def predicate(ctx): - return ctx.message.author.id == owner_id - return commands.check(predicate) - - -def is_admin(): - def predicate(ctx): - return ctx.message.author.guild_permissions.administrator - return commands.check(predicate) - - -def is_mod(): - def predicate(ctx): - return ctx.message.author.guild_permissions.ban_members - return commands.check(predicate) - - -def owner(ctx): - return ctx.message.author.id == owner_id - - -def admin(ctx): - return ctx.message.author.guild_permissions.administrator - - -def mod(ctx): - return ctx.message.author.guild_permissions.ban_members - - -def is_nsfw(): - def predicate(ctx): - if isinstance(ctx.message.channel, d.TextChannel): - return ctx.message.channel.is_nsfw() - return True - return commands.check(predicate) diff --git a/src/misc/exceptions.py b/src/misc/exceptions.py deleted file mode 100644 index 4c94ed2..0000000 --- a/src/misc/exceptions.py +++ /dev/null @@ -1,134 +0,0 @@ -from discord.ext.commands import errors as errext - -base = '\N{WARNING SIGN} **An internal error has occurred.** This has been reported to my master. \N{WOLF FACE}' - - -async def send_error(ctx, error): - await ctx.send('{}\n```\n{}```'.format(base, error)) - - -class Remove(Exception): - pass - - -class SizeError(Exception): - pass - - -class Wrong(Exception): - pass - - -class Add(Exception): - pass - - -class Execute(Exception): - pass - - -class Evaluate(Exception): - pass - - -class Left(Exception): - pass - - -class Right(Exception): - pass - - -class Save(Exception): - def __init__(self, user=None, message=None): - self.user = user - self.message = message - - -class GoTo(Exception): - pass - - -class Exists(errext.CommandError): - pass - - -class MissingArgument(errext.CommandError): - pass - - -class FavoritesNotFound(errext.CommandError): - pass - - -class PostError(errext.CommandError): - pass - - -class ImageError(errext.CommandError): - pass - - -class MatchError(errext.CommandError): - pass - - -class TagBlacklisted(errext.CommandError): - pass - - -class BoundsError(errext.CommandError): - pass - - -class TagBoundsError(errext.CommandError): - pass - - -class TagExists(errext.CommandError): - pass - - -class TagError(errext.CommandError): - pass - - -class FlagError(errext.CommandError): - pass - - -class BlacklistError(errext.CommandError): - pass - - -class NotFound(errext.CommandError): - pass - - -class Timeout(errext.CommandError): - pass - - -class InvalidVideoFile(errext.CommandError): - pass - - -class MissingAttachment(errext.CommandError): - pass - - -class TooManyAttachments(errext.CommandError): - pass - - -class CheckFail(errext.CommandError): - pass - - -class Abort(Exception): - def __init__(self, message=None): - self.message = message - - -class Continue(Exception): - pass diff --git a/src/run.py b/src/run.py deleted file mode 100644 index 3d4a3f5..0000000 --- a/src/run.py +++ /dev/null @@ -1,199 +0,0 @@ -import asyncio -import logging as log -import sys -import traceback as tb -from contextlib import suppress - -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 - -log.basicConfig(level=log.WARNING) - - -def get_prefix(bot, message): - with suppress(AttributeError): - return u.settings['prefixes'].get(message.guild.id, u.config['prefix']) - return u.config['prefix'] - -intents = d.Intents.default() -intents.members = True - -bot = cmds.Bot( - intents=intents, - command_prefix=get_prefix, - self_bot=u.config['selfbot'], - description='Modufur - A booru bot with a side of management and automated tasking' - '\nMade by @Myned#3985' -) - - -@bot.event -async def on_ready(): - if not checks.ready: - from cogs import weeb, booru, info, management, owner, tools - - for cog in ( - tools.Utils(bot), - owner.Bot(bot), - management.Admin(bot), - info.Info(bot), - booru.MsG(bot), - weeb.Weeb(bot)): - bot.add_cog(cog) - u.cogs[type(cog).__name__] = cog - print(f'COG : {type(cog).__name__}') - - if u.config['playing'] != '': - await bot.change_presence(activity=d.Game(u.config['playing'])) - - print('\n> > > > > > > > >' - f'\nC O N N E C T E D : {bot.user.name}' - '\n> > > > > > > > >\n') - - try: - if u.temp['startup']: - with suppress(err.NotFound): - if u.temp['startup'][0] == 'guild': - ctx = bot.get_channel(u.temp['startup'][1]) - else: - ctx = bot.get_user(u.temp['startup'][1]) - message = await ctx.fetch_message(u.temp['startup'][2]) - - await message.add_reaction('\N{WHITE HEAVY CHECK MARK}') - - u.temp['startup'] = () - u.dump(u.temp, 'temp/temp.pkl') - - checks.ready = True - except KeyError: - u.dump({'startup': ()}, 'temp/temp.pkl') - except AttributeError: - pass - else: - print('\n- - - -\nI N F O : reconnected, reinitializing tasks\n- - - -\n') - reconnect = await bot.get_user(u.config['owner_id']).send('**RECONNECTING**') - await reconnect.add_reaction('\N{SLEEPING SYMBOL}') - - if u.tasks['auto_del']: - for channel in u.tasks['auto_del']: - temp = bot.get_channel(channel) - bot.loop.create_task(u.cogs['Admin'].queue_for_deletion(temp)) - print(f'RESTARTED : auto-deleting in #{temp.name}') - u.cogs['Admin'].deleting = True - bot.loop.create_task(u.cogs['Admin'].delete()) - - if u.config['playing'] != '': - await bot.change_presence(activity=d.Game(u.config['playing'])) - - await reconnect.add_reaction('\N{WHITE HEAVY CHECK MARK}') - print('\nS U C C E S S\n') - - -@bot.event -async def on_message(message): - if not u.config['selfbot']: - if message.author is not bot.user and not message.author.bot and message.author.id not in u.block['user_ids']: - await bot.process_commands(message) - else: - if not message.author.bot: - await bot.process_commands(message) - - -@bot.event -async def on_error(error, *args, **kwargs): - print(f'\n! ! ! ! !\nE R R O R : {sys.exc_info()[1].text}\n! ! ! ! !\n', file=sys.stderr) - tb.print_exc() - await bot.get_user(u.config['owner_id']).send(f'**ERROR** \N{WARNING SIGN}\n```\n{error}```') - - if u.temp['startup']: - u.temp.clear() - u.dump(u.temp, 'temp/temp.pkl') - - await bot.logout() - - -@bot.event -async def on_command_error(ctx, error): - with suppress(err.NotFound): - if isinstance(error, err.NotFound): - print('NOT FOUND') - # elif isinstance(error, errext.CommandInvokeError): - # print(f'ERROR : {error}') - elif isinstance(error, err.Forbidden): - pass - elif isinstance(error, errext.CommandOnCooldown): - await u.add_reaction(ctx.message, '\N{HOURGLASS}') - await asyncio.sleep(error.retry_after) - await u.add_reaction(ctx.message, '\N{WHITE HEAVY CHECK MARK}') - elif isinstance(error, errext.MissingRequiredArgument): - await ctx.send('**Missing required argument**') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - elif isinstance(error, errext.BadArgument): - await ctx.send(f'**Invalid argument.** {error}') - await u.add_reaction(ctx.message, '\N{HEAVY EXCLAMATION MARK SYMBOL}') - elif isinstance(error, errext.CheckFailure): - await ctx.send('**Insufficient permissions**') - await u.add_reaction(ctx.message, '\N{NO ENTRY}') - elif isinstance(error, errext.CommandNotFound): - print(f'INVALID COMMAND : {error}', file=sys.stderr) - await u.add_reaction(ctx.message, '\N{BLACK QUESTION MARK ORNAMENT}') - else: - print('\n! ! ! ! ! ! ! ! ! ! ! !' - f'\nC O M M A N D E R R O R : {error}' - '\n! ! ! ! ! ! ! ! ! ! ! !\n', file=sys.stderr) - tb.print_exception(type(error), error, error.__traceback__, file=sys.stderr) - await bot.get_user(u.config['owner_id']).send( - '**COMMAND ERROR** \N{WARNING SIGN} ' - f'`{ctx.message.content}` ' - f'from {ctx.author.id} ' - f'in {ctx.channel.mention if isinstance(ctx.channel, d.channel.TextChannel) else "DMs"}' - '\n```\n' - f'{error}```') - await exc.send_error(ctx, error) - await u.add_reaction(ctx.message, '\N{WARNING SIGN}') - - -@bot.event -async def on_command_completion(ctx): - with suppress(err.NotFound): - with suppress(AttributeError): - if ctx.guild.id in u.settings['del_ctx'] and ctx.me.permissions_in(ctx.channel).manage_messages: - await ctx.message.delete() - - u.last_commands[ctx.author.id] = ctx - - -@bot.event -async def on_guild_join(guild): - if str(guild.id) in u.block['guild_ids']: - print(f'LEAVING : {guild.name}') - await guild.leave() - else: - print(f'JOINING : {guild.name}') - - -@bot.event -async def on_guild_remove(guild): - print(f'LEFT : {guild.name}') - - for task, idents in u.tasks.items(): - for channel in guild.channels: - if channel.id in idents: - idents.remove(channel.id) - print(f'STOPPED : {task} in #{channel.id}') - u.dump(u.tasks, 'cogs/tasks.pkl') - - -@bot.command(name=',test', hidden=True) -@cmds.is_owner() -async def test(ctx): - pass - - -bot.run(u.config['token'], bot=not u.config['selfbot']) diff --git a/src/temp/__init__.py b/src/temp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/formatter.py b/src/utils/formatter.py deleted file mode 100644 index d7950a7..0000000 --- a/src/utils/formatter.py +++ /dev/null @@ -1,73 +0,0 @@ -import copy - -from discord.ext.commands import Paginator - - -def tostring(i, *, order=None, newline=False): - o = '' - if i: - for v in i: - o += v + (' ' if newline is False else ' \n') - o = o[:-1] - elif order: - o += order - else: - o = ' ' - return o - - -def tostring_commas(i): - if i: - o = ',' - for v in i: - o += v + ',' - return o[:-1] - return '' - - -async def paginate( - ctx, - i, - start='', - prefix='', - kprefix='`', - ksuffix='`\n', - eprefix='```\n', - ejoin=' ', - esuffix='\n```', - suffix='', - end=''): - paginator = Paginator(prefix=prefix, suffix=suffix) - messages = [] - i = copy.deepcopy(i) - - if start: - paginator.add_line(start + ('' if type(i) is not dict else '\n')) - - if type(i) in (tuple, list, set): - if not i: - i = (' ') - paginator.add_line(eprefix + f'{ejoin}'.join(sorted(i)) + esuffix) - elif type(i) is dict: - if not i: - i = {' ': ' '} - for k, e in sorted(i.items()): - paginator.add_line(kprefix + k + ksuffix + eprefix + f'{ejoin}'.join(e) + esuffix) - - if end: - paginator.add_line(end) - - for page in paginator.pages: - messages.append(await ctx.send(page)) - - return messages - - -def dictelem_tostring(i): - o = '' - if i: - for dic, elem in i.items(): - o += '**__' + dic + '__**\n' - for k, v in elem.items(): - o += '***' + k + ':*** `' + tostring(v) + '`\n' - return o diff --git a/src/utils/scraper.py b/src/utils/scraper.py deleted file mode 100644 index 0cbb2c6..0000000 --- a/src/utils/scraper.py +++ /dev/null @@ -1,157 +0,0 @@ -import aiohttp -import ast -import re - -from bs4 import BeautifulSoup -import lxml -from hurry.filesize import size, alternative -import tldextract as tld - -from misc import exceptions as exc -from utils import utils as u - - -# async def get_harry(url): -# content = await u.fetch(f'https://iqdb.harry.lu?url={url}') -# soup = BeautifulSoup(content, 'html5lib') -# -# if soup.find('div', id='show1').string is 'Not the right one? ': -# parent = soup.find('th', string='Probable match:').parent.parent -# -# post = await u.fetch( -# f'https://e621.net/posts.json?id={re.search("show/([0-9]+)", parent.tr.td.a.get('href')).group(1)}', -# json=True) -# if (post['status'] == 'deleted'): -# post = await u.fetch( -# f'https://e621.net/posts.json?id={re.search("#(\\d+)", post["delreason"]).group(1)}', -# json=True) -# -# result = { -# 'source': f'https://e621.net/posts/{post["id"]}', -# 'artist': ', '.join(post['tags']['artist']), -# 'thumbnail': parent.td.a.img.get('src'), -# 'similarity': re.search('\\d+', parent.tr[4].td.string).group(0), -# 'database': 'Harry.lu' -# } -# -# return result -# else: -# return False - - -async def query_kheina(url): - try: - content = await u.fetch(f'https://api.kheina.com/v1/search', post={'url': url}, json=True) - - similarity = int(content['results'][0]['similarity']) - if similarity < 55: - return None - - if tld.extract(content['results'][0]['sources'][0]['source']).domain == 'furaffinity': - submission = re.search('\\d+$', content['results'][0]['sources'][0]['source']).group(0) - try: - export = await u.fetch(f'https://faexport.spangle.org.uk/submission/{submission}.json', json=True) - thumbnail = export['full'] - except AssertionError: - thumbnail = '' - else: - thumbnail = '' - - result = { - 'source': content['results'][0]['sources'][0]['source'], - 'artist': content['results'][0]['sources'][0]['artist'] if content['results'][0]['sources'][0]['artist'] else 'unknown', - 'thumbnail': thumbnail, - 'similarity': str(similarity), - 'database': tld.extract(content['results'][0]['sources'][0]['source']).domain - } - - return result - - except Exception: - return None - - -async def query_saucenao(url): - try: - content = await u.fetch( - f'https://saucenao.com/search.php?url={url}&api_key={u.config["saucenao_api"]}&output_type={2}', - json=True) - - if content['header'].get('message', '') in ( - 'Access to specified file was denied... ;_;', - 'Problem with remote server...', - 'image dimensions too small...'): - raise exc.ImageError - - match = content['results'][0] - - similarity = int(float(match['header']['similarity'])) - if similarity < 55: - return None - - source = match['data']['ext_urls'][0] - for e in match['data']['ext_urls']: - if 'furaffinity' in e: - source = e - break - for e in match['data']['ext_urls']: - if 'e621' in e: - source = e - break - - artist = 'unknown' - for e in ( - 'author_name', - 'member_name', - 'creator'): - if e in match['data'] and match['data'][e]: - artist = match['data'][e] - break - - result = { - 'source': source, - 'artist': artist, - 'thumbnail': match['header']['thumbnail'], - 'similarity': str(similarity), - 'database': tld.extract(source).domain - } - - return result - - except Exception: - return None - - -async def get_post(url): - try: - content = await u.fetch(url, response=True) - filesize = int(content.headers['Content-Length']) - if filesize > 8192 * 1024: - raise exc.SizeError(size(filesize, system=alternative)) - - # Prioritize SauceNAO if e621/furaffinity, Kheina>SauceNAO if not - result = await query_saucenao(url) - if result: - if not any(s in result['source'] for s in ('e621', 'furaffinity')): - kheina = await query_kheina(url) - if kheina: - result = kheina - else: - result = await query_kheina(url) - - if not result: - raise exc.MatchError(re.search('\\/([^\\/]+)$', url).group(1)) - - return result - - except aiohttp.InvalidURL: - raise exc.MissingArgument - - -async def get_image(url): - content = await u.fetch(url) - - value = lxml.html.fromstring(content).xpath( - 'string(/html/body/div[@id="content"]/div[@id="post-view"]/div[@class="content"]/div[2]/img/@src)') - - return value diff --git a/src/utils/utils.py b/src/utils/utils.py deleted file mode 100644 index 17e20a8..0000000 --- a/src/utils/utils.py +++ /dev/null @@ -1,190 +0,0 @@ -import json as jsn -import os -import pickle as pkl -from contextlib import suppress -from fractions import gcd -import math - -import aiohttp -import discord as d -from discord import errors as err - -from misc import exceptions as exc - - -print('\nPID : {}\n'.format(os.getpid())) - - -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, 'owner_id': 0, 'permissions': 126016, - 'playing': 'a game', 'prefix': [',', 'm,'], 'selfbot': False, 'token': 'str', 'saucenao_api': 'str', 'e621_api': 'str'}, outfile, indent=4, sort_keys=True) - print('FILE NOT FOUND : config.json created with default 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': []}) -temp = setdefault('temp/temp.pkl', default={'startup': ()}) -block = setdefault('cogs/block.json', default={'guild_ids': [], 'user_ids': []}, json=True) - -cogs = {} -color = d.Color(0x1A1A1A) -last_commands = {} - -asession = aiohttp.ClientSession() - - -async def fetch(url, *, post={}, response=False, text=False, json=False): - if '.json' in url and ('e621' in url or 'e926' in url): - url += f'&login=BotMyned&api_key={config["e621_api"]}' - - if post: - async with asession.post(url, data=post, headers={ - 'User-Agent': 'Myned/Modufur (https://github.com/Myned/Modufur)'}, ssl=False) as r: - assert r.status == 200 - - if response: - return r - elif text: - return await r.text() - elif json: - return await r.json() - else: - return await r.read() - else: - async with asession.get(url, headers={ - 'User-Agent': 'Myned/Modufur (https://github.com/Myned/Modufur)'}, ssl=False) as r: - if r.status != 200: - return r.status - elif response: - return r - elif text: - return await r.text() - elif json: - return await r.json() - else: - return await r.read() - - -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 kwargs(args): - params = list(args) - lst = 'blacklist' - - for switch in ('-a', '--aliases'): - if switch in params: - lst = 'aliases' - params.remove(switch) - - return params, lst - -def get_kwargs(ctx, args, *, limit=False): - remaining = list(args[:]) - rm = False - lim = 1 - - for flag in ('-r', '-rm', '--remove'): - if flag in remaining: - 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 {'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) - - -async def add_reaction(message, reaction, errors=(err.NotFound, err.Forbidden)): - sent = False - - with suppress(errors): - await message.add_reaction(reaction) - sent = True - - return sent diff --git a/tools/components.py b/tools/components.py new file mode 100644 index 0000000..b6ed31d --- /dev/null +++ b/tools/components.py @@ -0,0 +1,109 @@ +import hikari +import lightbulb +from miru.ext import nav + + +plugin = lightbulb.Plugin('components') + + +class Back(nav.PrevButton): + def __init__(self): + super().__init__( + style=hikari.ButtonStyle.SECONDARY, + label='⟵', + emoji=None) + +class Forward(nav.NextButton): + def __init__(self): + super().__init__( + style=hikari.ButtonStyle.SECONDARY, + label='⟶', + emoji=None) + +class Confirm(nav.StopButton): + def __init__(self): + super().__init__( + style=hikari.ButtonStyle.PRIMARY, + label='➤', + emoji=None) + + async def callback(self, context): + await context.edit_response(content='**Searching...**', components=None) + + self.view.stop() + + async def before_page_change(self): + self.disabled = False if self.view.selected else True + +class Select(nav.NavButton): + def __init__(self): + super().__init__( + style=hikari.ButtonStyle.DANGER, + label='✗', + emoji=None) + + async def callback(self, context): + if self.view.urls[self.view.current_page] not in self.view.selected: + self.view.selected.append(self.view.urls[self.view.current_page]) + self._button(selected=True) + else: + self.view.selected.remove(self.view.urls[self.view.current_page]) + self._button() + + await context.edit_response(components=self.view.build()) + + async def before_page_change(self): + if self.view.urls[self.view.current_page] not in self.view.selected: + self._button() + else: + self._button(selected=True) + + def _button(self, *, selected=False): + self.style = hikari.ButtonStyle.SUCCESS if selected else hikari.ButtonStyle.DANGER + self.label = '✔' if selected else '✗' + + try: + confirm = next((child for child in self.view.children if isinstance(child, Confirm))) + confirm.disabled = False if self.view.selected else True + except StopIteration: + pass + + +class Selector(nav.NavigatorView): + def __init__(self, *, pages=[], buttons=[], timeout=120, urls=[]): + super().__init__( + pages=pages, + buttons=buttons, + timeout=timeout) + self.urls = urls + self.selected = [] + self.saved = set() + self.timed_out = False + + async def on_timeout(self): + if self._inter: + for button in self.children: + button.disabled = True + + await self._inter.edit_initial_response(components=self.build()) + + self.timed_out = True + + async def send_edit(self, interaction): + self._inter = interaction + + for button in self.children: + if isinstance(button, nav.NavButton): + await button.before_page_change() + + payload = self._get_page_payload(self.pages[0]) + + await interaction.edit_initial_response(**payload) + + self.start(await interaction.fetch_initial_response()) + + +def load(bot): + bot.add_plugin(plugin) +def unload(bot): + bot.remove_plugin(plugin) diff --git a/tools/scraper.py b/tools/scraper.py new file mode 100644 index 0000000..be7c752 --- /dev/null +++ b/tools/scraper.py @@ -0,0 +1,61 @@ +import aiohttp +import tldextract +import lightbulb +import pysaucenao + +import config as c + + +plugin = lightbulb.Plugin('scraper') +sauce = pysaucenao.SauceNao(api_key=c.config['saucenao'], priority=(29, 40, 41)) # e621 > Fur Affinity > Twitter + + +async def reverse(urls): + matches = [] + + for url in urls: + saucenao = await _saucenao(url) + kheina = None + + if saucenao: + matches.append(saucenao) + else: + pass + + if not saucenao and not kheina: + matches.append(None) + + return matches + +async def _saucenao(url): + try: + results = await sauce.from_url(url) + except pysaucenao.FileSizeLimitException: + raise pysaucenao.FileSizeLimitException(url) + except pysaucenao.ImageSizeException: + raise pysaucenao.ImageSizeException(url) + except pysaucenao.InvalidImageException: + raise pysaucenao.InvalidImageException(url) + + if results: + return { + 'source': results[0].url, + 'artist': results[0].author_name or 'unknown', + 'thumbnail': results[0].thumbnail, + 'similarity': int(results[0].similarity), + 'index': tldextract.extract(results[0].index).domain} + return + +async def _kheina(url): + pass + +async def _fetch(url): + async with aiohttp.ClientSession() as session: + async with session.get(url, headers={'User-Agent': 'Myned/Modufur (https://github.com/Myned/Modufur)'}) as response: + return await response.json() if response.status == 200 else None + + +def load(bot): + bot.add_plugin(plugin) +def unload(bot): + bot.remove_plugin(plugin)