diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..974375e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,110 @@
+# Custom
+*.json
+*.pyo
+*.pyc
+*.DS_Store
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+.static_storage/
+.media/
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/__init__.py b/src/main/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/cogs/__init__.py b/src/main/cogs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/cogs/booru.py b/src/main/cogs/booru.py
new file mode 100644
index 0000000..a9faf42
--- /dev/null
+++ b/src/main/cogs/booru.py
@@ -0,0 +1,421 @@
+import asyncio
+import discord
+import json
+import requests
+import traceback
+from discord import reaction
+from discord.ext import commands
+from discord.ext.commands import errors
+from misc import checks
+from misc import exceptions as exc
+from utils import formatter, scraper
+
+try:
+ with open('global_blacklist.json') as infile:
+ global_blacklist = json.load(infile)
+ print('\"global_blacklist.json\" loaded.')
+except FileNotFoundError:
+ print('Blacklist file not found: \"global_blacklist.json\" created.')
+ with open('global_blacklist.json', 'w+') as iofile:
+ json.dump([], iofile, indent=4, sort_keys=True)
+ iofile.seek(0)
+ global_blacklist = json.load(iofile)
+try:
+ with open('guild_blacklist.json') as infile:
+ guild_blacklist = json.load(infile)
+ print('\"guild_blacklist.json\" loaded.')
+except FileNotFoundError:
+ print('Blacklist file not found: \"guild_blacklist.json\" created.')
+ with open('guild_blacklist.json', 'w+') as iofile:
+ json.dump({}, iofile, indent=4, sort_keys=True)
+ iofile.seek(0)
+ guild_blacklist = json.load(iofile)
+try:
+ with open('user_blacklist.json') as infile:
+ user_blacklist = json.load(infile)
+ print('\"user_blacklist.json\" loaded.')
+except FileNotFoundError:
+ print('Blacklist file not found: \"user_blacklist.json\" created.')
+ with open('user_blacklist.json', 'w+') as iofile:
+ json.dump({}, iofile, indent=4, sort_keys=True)
+ iofile.seek(0)
+ user_blacklist = json.load(iofile)
+
+last_command = {}
+
+class MsG:
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ # Creates reaction-based paginator for linked pools
+ @commands.command(brief='e621/e926 Pool selector', description='e621/e926 | NSFW/SFW\nShow pools in a page format', hidden=True)
+ @checks.del_ctx()
+ async def pool(self, ctx, url):
+ pool_urls = []
+
+ def check_right(reaction, user):
+ return user == ctx.message.author and str(reaction.emoji) == '➡️'
+ def check_left(reaction, user):
+ return user == ctx.message.author and str(reaction.emoji) == '⬅️'
+
+ try:
+ pool = scraper.find_pool(url)
+ for link in pool:
+ pool_urls.append(scraper.find_image_url('https://e621.net' + link))
+ except exc.PostError:
+ await ctx.send('❌ ' + ctx.message.author.mention + ' **No pool found.**')
+ except exc.ImageError:
+ await ctx.send('❌ ' + ctx.message.author.mention + ' **No image found.**')
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+ # Reverse image searches a linked image using the public iqdb
+ @commands.command(name='reverse', aliases=['rev', 'ris'], brief='e621 Reverse image search', description='e621 | NSFW\nReverse-search an image with given URL')
+ @checks.del_ctx()
+ async def reverse_image_search(self, ctx, url):
+ try:
+ await ctx.trigger_typing()
+ await ctx.send('✅ ' + ctx.message.author.mention + ' **Probable match:**\n' + scraper.check_match('http://iqdb.harry.lu/?url=' + url))
+ except exc.MatchError:
+ await ctx.send('❌ ' + ctx.message.author.mention + ' **No probable match.**')
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+ # Searches for and returns images from e621.net given tags when not blacklisted
+ @commands.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.del_ctx()
+ @checks.is_nsfw()
+ async def e621(self, ctx, *args):
+ global global_blacklist, guild_blacklist, user_blacklist
+ args = list(args)
+ try:
+ await ctx.trigger_typing()
+ await self.check_send_urls(ctx, 'e621', args)
+ except exc.TagBlacklisted as e:
+ await ctx.send('❌ `' + str(e) + '` **blacklisted.**', delete_after=10)
+ except exc.BoundsError as e:
+ await ctx.send('❌ `' + str(e) + '` **out of bounds.**', delete_after=10)
+ except exc.TagBoundsError as e:
+ await ctx.send('❌ `' + str(e) + '` **out of bounds.** Tags limited to 5, currently.', delete_after=10)
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc()
+
+ @e621.error
+ async def e621_error(self, ctx, error):
+ if isinstance(error, errors.CheckFailure):
+ return await ctx.send('❌ <#' + str(ctx.message.channel.id) + '> **is not an NSFW channel.**', delete_after=10)
+
+ # Searches for and returns images from e926.net given tags when not blacklisted
+ @commands.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])')
+ @checks.del_ctx()
+ async def e926(self, ctx, *args):
+ global global_blacklist, guild_blacklist, user_blacklist
+ args = list(args)
+ try:
+ await ctx.trigger_typing()
+ await self.check_send_urls(ctx, 'e926', args)
+ except exc.TagBlacklisted as e:
+ await ctx.send('❌ `' + str(e) + '` **blacklisted.**', delete_after=10)
+ except exc.BoundsError as e:
+ await ctx.send('❌ `' + str(e) + '` **out of bounds.**', delete_after=10)
+ except exc.TagBoundsError as e:
+ await ctx.send('❌ `' + str(e) + '` **out of bounds.** Tags limited to 5, currently.', delete_after=10)
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+ # Messy code that checks image limit and tags in blacklists
+ async def check_send_urls(self, ctx, booru, args):
+ global global_blacklist, guild_blacklist, user_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ user = ctx.message.author
+ urls = []
+ limit = 1
+ # Checks if tags are in the file blacklists
+ if args:
+ for tag in args:
+ if tag == 'swf' or tag == 'webm' or tag in global_blacklist or tag in guild_blacklist.get(str(guild.id), {}).get(str(channel.id), []) or tag in user_blacklist.get(str(user.id), []):
+ raise exc.TagBlacklisted(tag)
+ if len(args) > 5:
+ raise exc.TagBoundsError(formatter.tostring(args[5:]))
+ # Checks for, defines, and removes limit from end of args
+ if args and len(args[-1]) == 1:
+ if int(args[-1]) <= 6 and int(args[-1]) >= 1:
+ limit = int(args[-1])
+ args.pop()
+ else:
+ raise exc.BoundsError(args[-1])
+ # Checks for blacklisted tags in endpoint blacklists - try/except is for continuing the parent loop
+ while len(urls) < limit:
+ request = requests.get('https://' + booru + '.net/post/index.json?limit=6&tags=order:random' + formatter.tostring_commas(args)).json()
+ for post in request:
+ if 'swf' in post['file_ext'] or 'webm' in post['file_ext']:
+ continue
+ try:
+ for tag in global_blacklist:
+ if tag in post['tags']:
+ raise exc.Continue
+ for tag in guild_blacklist.get(str(guild.id), {}).get(str(channel.id), []):
+ if tag in post['tags']:
+ raise exc.Continue
+ for tag in user_blacklist.get(str(user.id), []):
+ if tag in post['tags']:
+ raise exc.Continue
+ except exc.Continue:
+ continue
+ if post['file_url'] not in urls:
+ urls.append(post['file_url'])
+ if len(urls) == limit:
+ break
+ for url in urls:
+ await ctx.send('`' + formatter.tostring(args) + '`\n' + url)
+
+ # Umbrella command structure to manage global, channel, and user blacklists
+ @commands.group(aliases=['bl', 'b'], brief='Manage blacklists', description='Blacklist base command for managing blacklists\n\n`bl get [blacklist]` to show a blacklist\n`bl set [blacklist] [tags]` to replace a blacklist\n`bl clear [blacklist]` to clear a blacklist\n`bl add [blacklist] [tags]` to add tags to a blacklist\n`bl remove [blacklist] [tags]` to remove tags from a blacklist', usage='[flag] [blacklist] ([tags])')
+ @checks.del_ctx()
+ async def blacklist(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Use a flag to manage blacklists.**\n*Type* `' + ctx.prefix + 'help bl` *for more info.*', delete_after=10)
+
+ @blacklist.error
+ async def blacklist_error(self, ctx, error):
+ if isinstance(error, commands.CheckFailure):
+ return await ctx.send('❌ **Insufficient permissions.**', delete_after=10)
+ if isinstance(error, exc.TagExists):
+ return await ctx.send('❌ `' + str(exc.TagExists) + '` **already in blacklist.**', delete_after=10)
+ if isinstance(error, exc.TagError):
+ return await ctx.send('❌ `' + str(exc.TagError) + '` **not in blacklist.**', delete_after=10)
+ if isinstance(error, KeyError):
+ return await ctx.send('❌ **Blacklist does not exist.**', delete_after=10)
+
+ @blacklist.command(name='update', aliases=['upd', 'up'])
+ async def _update_blacklists(self, ctx):
+ global global_blacklist, guild_blacklist, user_blacklist
+ with open('global_blacklist.json', 'w') as outfile:
+ json.dump(global_blacklist, outfile, indent=4, sort_keys=True)
+ with open('guild_blacklist.json', 'w') as outfile:
+ json.dump(guild_blacklist, outfile, indent=4, sort_keys=True)
+ with open('user_blacklist.json', 'w') as outfile:
+ json.dump(user_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Blacklists updated.**')
+
+ @blacklist.group(name='get', aliases=['g'])
+ async def _get_blacklist(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @_get_blacklist.command(name='global', aliases=['gl', 'g'])
+ async def __get_global_blacklist(self, ctx):
+ global global_blacklist
+ await ctx.send('🚫 **Global blacklist:**\n```' + formatter.tostring(global_blacklist) + '```')
+ @_get_blacklist.command(name='channel', aliases=['ch', 'c'])
+ async def __get_channel_blacklist(self, ctx):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ await ctx.send('🚫 <#' + str(channel.id) + '> **blacklist:**\n```' + formatter.tostring(guild_blacklist.get(str(guild.id), {}).get(str(channel.id), [])) + '```')
+ @_get_blacklist.command(name='me', aliases=['m'])
+ async def __get_user_blacklist(self, ctx):
+ global user_blacklist
+ user = ctx.message.author
+ await ctx.send('🚫 ' + user.mention + '**\'s blacklist:**\n```' + formatter.tostring(user_blacklist.get(str(user.id), [])) + '```', delete_after=10)
+ @_get_blacklist.command(name='here', aliases=['h'])
+ async def __get_here_blacklists(self, ctx):
+ global global_blacklist, guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ await ctx.send('🚫 **__Blacklisted:__**\n\n**Global:**\n```' + formatter.tostring(global_blacklist) + '```\n**<#' + str(channel.id) + '>:**\n```' + formatter.tostring(guild_blacklist.get(str(guild.id), {}).get(str(channel.id), [])) + '```')
+ @_get_blacklist.group(name='all', aliases=['a'])
+ async def __get_all_blacklists(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @__get_all_blacklists.command(name='guild', aliases=['g'])
+ @commands.has_permissions(manage_channels=True)
+ async def ___get_all_guild_blacklists(self, ctx):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ await ctx.send('🚫 **__' + guild.name + ' blacklists:__**\n\n' + formatter.dict_tostring(guild_blacklist.get(str(guild.id), {})))
+ @__get_all_blacklists.command(name='user', aliases=['u', 'member', 'm'])
+ @commands.is_owner()
+ async def ___get_all_user_blacklists(self, ctx):
+ global user_blacklist
+ await ctx.send('🚫 **__User blacklists:__**\n\n' + formatter.dict_tostring(user_blacklist))
+
+ @blacklist.group(name='add', aliases=['a'])
+ async def _add_tags(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @_add_tags.command(name='global', aliases=['gl', 'g'])
+ @commands.is_owner()
+ async def __add_global_tags(self, ctx, *tags):
+ global global_blacklist
+ for tag in tags:
+ if tag in global_blacklist:
+ raise exc.TagExists(tag)
+ global_blacklist.extend(tags)
+ with open('global_blacklist.json', 'w') as outfile:
+ json.dump(global_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Added to global blacklist:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+ @_add_tags.command(name='channel', aliases=['ch', 'c'])
+ @commands.has_permissions(manage_channels=True)
+ async def __add_channel_tags(self, ctx, *tags):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ for tag in tags:
+ if tag in guild_blacklist.get(str(guild.id), {}).get(str(channel.id), []):
+ raise exc.TagExists(tag)
+ guild_blacklist.setdefault(str(guild.id), {}).setdefault(str(channel.id), []).extend(tags)
+ with open('guild_blacklist.json', 'w') as outfile:
+ json.dump(guild_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Added to** <#' + str(channel.id) + '> **blacklist:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+ @_add_tags.command(name='me', aliases=['m'])
+ async def __add_user_tags(self, ctx, *tags):
+ global user_blacklist
+ user = ctx.message.author
+ for tag in tags:
+ if tag in user_blacklist.get(str(user.id), []):
+ raise exc.TagExists(tag)
+ user_blacklist.setdefault(str(user.id), []).extend(tags)
+ with open('user_blacklist.json', 'w') as outfile:
+ json.dump(user_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ ' + user.mention + ' **added:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+
+ @blacklist.group(name='remove', aliases=['rm', 'r'])
+ async def _remove_tags(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @_remove_tags.command(name='global', aliases=['gl', 'g'])
+ @commands.is_owner()
+ async def __remove_global_tags(self, ctx, *tags):
+ global global_blacklist
+ for tag in tags:
+ if tag in global_blacklist:
+ global_blacklist.remove(tag)
+ else:
+ raise exc.TagError(tag)
+ with open('global_blacklist.json', 'w') as outfile:
+ json.dump(global_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Removed from global blacklist:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+ @_remove_tags.command(name='channel', aliases=['ch', 'c'])
+ @commands.has_permissions(manage_channels=True)
+ async def __remove_channel_tags(self, ctx, *tags):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ for tag in tags:
+ if tag in guild_blacklist.get(str(guild.id), {}).get(str(channel.id), []):
+ guild_blacklist.get(str(guild.id), {})[str(channel.id)].remove(tag)
+ else:
+ raise exc.TagError(tag)
+ with open('guild_blacklist.json', 'w') as outfile:
+ json.dump(guild_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Removed from** <#' + str(channel.id) + '> **blacklist:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+ @_remove_tags.command(name='me', aliases=['m'])
+ async def __remove_user_tags(self, ctx, *tags):
+ global user_blacklist
+ user = ctx.message.author
+ for tag in tags:
+ if tag in user_blacklist.get(str(user.id), []):
+ user_blacklist.get[str(user.id)].remove(tag)
+ else:
+ raise exc.TagError(tag)
+ with open('user_blacklist.json', 'w') as outfile:
+ json.dump(user_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ ' + user.mention + ' **removed:**\n```' + formatter.tostring(tags) + '```', delete_after=5)
+
+ @blacklist.group(name='set', aliases=['s'])
+ async def _set_blacklist(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @_set_blacklist.command(name='global', aliases=['gl', 'g'])
+ @commands.is_owner()
+ async def __set_global_blacklist(self, ctx, *tags):
+ global global_blacklist
+ global_blacklist = tags[:]
+ with open('global_blacklist.json', 'w') as outfile:
+ json.dump(global_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Global blacklist set to:**\n```' + formatter.tostring(global_blacklist) + '```', delete_after=10)
+ @_set_blacklist.command(name='channel', aliases=['ch', 'c'])
+ @commands.has_permissions(manage_channels=True)
+ async def __set_channel_blacklist(self, ctx, *tags):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ guild_blacklist.setdefault(str(guild.id), {})[str(channel.id)] = tags[:]
+ with open('guild_blacklist.json', 'w') as outfile:
+ json.dump(guild_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ <#' + str(channel.id) + '> **blacklist set to:**\n```' + formatter.tostring(guild_blacklist.get(str(guild.id), {}).get(str(channel.id), [])) + '```', delete_after=10)
+ @_set_blacklist.command(name='me', aliases=['m'])
+ async def __set_user_blacklist(self, ctx, *tags):
+ global user_blacklist
+ user = ctx.message.author
+ user_blacklist[str(user.id)] = tags[:]
+ with open('user_blacklist.json', 'w') as outfile:
+ json.dump(user_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ ' + user.mention + '**\'s blacklist set to:**\n```' + formatter.tostring(user_blacklist.get(str(user.id), [])) + '```', delete_after=10)
+
+ @blacklist.group(name='clear', aliases=['cl', 'c'])
+ async def _clear_blacklist(self, ctx):
+ if ctx.invoked_subcommand is None:
+ await ctx.send('❌ **Invalid blacklist.**')
+
+ @_clear_blacklist.command(name='global', aliases=['gl', 'g'])
+ @commands.is_owner()
+ async def __clear_global_blacklist(self, ctx):
+ global global_blacklist
+ global_blacklist = []
+ with open('global_blacklist.json', 'w') as outfile:
+ json.dump(global_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ **Global blacklist cleared.**', delete_after=5)
+ @_clear_blacklist.command(name='channel', aliases=['ch', 'c'])
+ @commands.has_permissions(manage_channels=True)
+ async def __clear_channel_blacklist(self, ctx):
+ global guild_blacklist
+ if isinstance(ctx.message.guild, discord.Guild):
+ guild = ctx.message.guild
+ else:
+ guild = ctx.message.channel
+ channel = ctx.message.channel
+ guild_blacklist.get(str(guild.id), {})[str(channel.id)] = []
+ with open('guild_blacklist.json', 'w') as outfile:
+ json.dump(guild_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ <#' + str(channel.id) + '> **blacklist cleared.**', delete_after=5)
+ @_clear_blacklist.command(name='me', aliases=['m'])
+ async def __clear_user_blacklist(self, ctx):
+ global user_blacklist
+ user = ctx.message.author
+ user_blacklist[str(user.id)] = []
+ with open('user_blacklist.json', 'w') as outfile:
+ json.dump(user_blacklist, outfile, indent=4, sort_keys=True)
+ await ctx.send('✅ ' + user.mention + '**\'s blacklist cleared.**', delete_after=5)
diff --git a/src/main/cogs/info.py b/src/main/cogs/info.py
new file mode 100644
index 0000000..d86435b
--- /dev/null
+++ b/src/main/cogs/info.py
@@ -0,0 +1,32 @@
+import asyncio
+import discord
+import traceback
+from discord.ext import commands
+from misc import exceptions as exc
+
+
+class Info:
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.group(name='info', aliases=['i'])
+ async def info(self, ctx):
+ if invoked_subcommand is None:
+ await ctx.send('')
+
+ @info.command(aliases=['g', 'server', 's'], brief='Provides info about a guild', hidden=True)
+ async def guild(self, ctx):
+ try:
+ guild = ''
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+ @info.command(aliases=['u', 'member', 'm'], brief='Provides info about a user', hidden=True)
+ async def user(self, ctx):
+ try:
+ user = ''
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
diff --git a/src/main/cogs/tools.py b/src/main/cogs/tools.py
new file mode 100644
index 0000000..93ccf0c
--- /dev/null
+++ b/src/main/cogs/tools.py
@@ -0,0 +1,58 @@
+import asyncio
+import discord
+import traceback
+from discord.ext import commands
+from cogs import booru
+from misc import checks
+from misc import exceptions as exc
+from utils import formatter
+
+class Utils:
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ def last():
+ pass
+
+ @commands.command(name='last', aliases=['l', ','], brief='Reinvokes last command', description='Reinvokes previous command executed', hidden=True)
+ async def last_command(self, ctx):
+ try:
+ # await ctx.invoke(command, args)
+ await ctx.send('`' + booru.last_command[ctx.message.author.id] + '`')
+ except Exception:
+ await ctx.send(exceptions.base)
+ traceback.print_exc(limit=1)
+
+ # [prefix]ping -> Pong!
+ @commands.command(aliases=['p'], brief='Pong!', description='Returns latency from bot to Discord servers, not to user')
+ @checks.del_ctx()
+ async def ping(self, ctx):
+ try:
+ await ctx.send(ctx.message.author.mention + ' 🏓 `' + str(int(self.bot.latency * 1000)) + 'ms`', delete_after=5)
+ except Exception:
+ await ctx.send(exceptions.base)
+ traceback.print_exc(limit=1)
+
+ @commands.command(aliases=['pre'], brief='List bot prefixes', description='Shows all used prefixes')
+ @checks.del_ctx()
+ async def prefix(self, ctx):
+ try:
+ await ctx.send('**Prefix:** `,` or ' + ctx.me.mention)
+ except Exception:
+ await ctx.send(exceptions.base)
+ traceback.print_exc(limit=1)
+
+ @commands.group(name=',send', aliases=[',s'], hidden=True)
+ @commands.is_owner()
+ async def send(self, ctx):
+ pass
+
+ @send.command(name='guild', aliases=['g', 'server', 's'])
+ @checks.del_ctx()
+ async def send_guild(self, ctx, guild, channel, *message):
+ await discord.utils.get(self.bot.get_all_channels(), guild__name=guild, name=channel).send(formatter.tostring(message))
+ @send.command(name='user', aliases=['u', 'member', 'm'])
+ @checks.del_ctx()
+ async def send_user(self, ctx, user, *message):
+ await discord.utils.get(self.bot.get_all_members(), id=int(user)).send(formatter.tostring(message))
diff --git a/src/main/misc/__init__.py b/src/main/misc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/misc/checks.py b/src/main/misc/checks.py
new file mode 100644
index 0000000..7376cc6
--- /dev/null
+++ b/src/main/misc/checks.py
@@ -0,0 +1,45 @@
+import asyncio
+import discord
+import json
+import traceback
+from discord.ext import commands
+from discord.ext.commands import errors
+
+with open('config.json') as infile:
+ config = json.load(infile)
+
+owner_id = config['owner_id']
+
+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, discord.TextChannel):
+ return ctx.message.channel.is_nsfw()
+ return True
+ return commands.check(predicate)
+
+def del_ctx():
+ async def predicate(ctx):
+ if isinstance(ctx.message.channel, discord.TextChannel):
+ await ctx.message.delete()
+ return True
+ return commands.check(predicate)
diff --git a/src/main/misc/exceptions.py b/src/main/misc/exceptions.py
new file mode 100644
index 0000000..a56802a
--- /dev/null
+++ b/src/main/misc/exceptions.py
@@ -0,0 +1,24 @@
+base = '‼️ **An internal error has occurred.** Please notify my master! 🐺'
+
+class PostError(Exception):
+ pass
+class ImageError(Exception):
+ pass
+class MatchError(Exception):
+ pass
+class TagBlacklisted(Exception):
+ pass
+class BoundsError(Exception):
+ pass
+class TagBoundsError(Exception):
+ pass
+class TagExists(Exception):
+ pass
+class TagError(Exception):
+ pass
+class FlagError(Exception):
+ pass
+class BlacklistError(Exception):
+ pass
+class Continue(Exception):
+ pass
diff --git a/src/main/run.py b/src/main/run.py
new file mode 100644
index 0000000..388e229
--- /dev/null
+++ b/src/main/run.py
@@ -0,0 +1,81 @@
+import asyncio
+import discord
+import json
+import traceback
+from discord import utils
+from discord.ext import commands
+from cogs import booru, info, tools
+from misc import checks
+from misc import exceptions as exc
+
+try:
+ with open('config.json') as infile:
+ config = json.load(infile)
+ print('\"config.json\" loaded.')
+except FileNotFoundError:
+ with open('config.json', 'w') as outfile:
+ json.dump({'client_id': 'int', 'owner_id': 'int', 'permissions': 'int', 'shutdown_channel': 'int', 'startup_channel': 'int',
+ 'token': 'str'}, outfile, indent=4, sort_keys=True)
+ raise FileNotFoundError('Config file not found: \"config.json\" created with abstract values. Restart \"run.py\" with correct values.')
+
+bot = commands.Bot(command_prefix=commands.when_mentioned_or(','), description='Experimental booru bot')
+
+# Send and print ready message to #testing and console after logon
+@bot.event
+async def on_ready():
+ await bot.get_channel(config['startup_channel']).send('Hello how are? **Have day.** 🌈\n')
+ print('Connected.')
+ print('Username: ' + bot.user.name)
+ print('-------')
+
+# Close connection to Discord - immediate offline
+@bot.command(name=',die', aliases=[',d', ',close', ',kill'], brief='Kills the bot', description='BOT OWNER ONLY\nCloses the connections to Discord', hidden=True)
+@checks.del_ctx()
+@commands.is_owner()
+async def die(ctx):
+ try:
+ await ctx.send('Am go bye. **Have night.** 💤')
+ # await bot.get_channel(config['shutdown_channel']).send('')
+ await bot.close()
+ print('-------')
+ print('Closed.')
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+# Invite bot to bot owner's server
+@bot.command(name=',invite', aliases=[',inv', ',link'], brief='Invite the bot', description='BOT OWNER ONLY\nInvite the bot to a server (Requires admin)', hidden=True)
+@checks.del_ctx()
+@commands.is_owner()
+async def invite(ctx):
+ try:
+ await ctx.send('🔗 ' + utils.oauth_url(config['client_id'], permissions=config['permissions'], guild=ctx.message.guild))
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+@bot.command(brief='[IN TESTING]', description='[IN TESTING]', hidden=True)
+async def hi(ctx):
+ try:
+ hello = 'Hello, ' + ctx.message.author.mention + '.'
+ if ctx.message.author.id == checks.owner_id:
+ hello += '.. ***Master.*** uwu'
+ elif ctx.message.author.guild_permissions.administrator:
+ hello = hello[:7] + '**Admin** ' + hello[7:]
+ elif ctx.message.author.guild_permissions.ban_members:
+ hello = hello[:7] + '**Mod** ' + hello[7:]
+ await ctx.send(hello)
+ except Exception:
+ await ctx.send(exc.base)
+ traceback.print_exc(limit=1)
+
+@bot.command(hidden=True)
+@checks.del_ctx()
+async def test(ctx):
+ pass
+
+bot.add_cog(info.Info(bot))
+bot.add_cog(tools.Utils(bot))
+bot.add_cog(booru.MsG(bot))
+
+bot.run(config['token'])
diff --git a/src/main/utils/__init__.py b/src/main/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/utils/formatter.py b/src/main/utils/formatter.py
new file mode 100644
index 0000000..faa36c6
--- /dev/null
+++ b/src/main/utils/formatter.py
@@ -0,0 +1,31 @@
+def tostring(i):
+ o = ' '
+ if i:
+ for v in i:
+ o += v + ' '
+ o = o[1:-1]
+ return o
+
+def tostring_commas(i):
+ if i:
+ o = ','
+ for v in i:
+ o += v + ','
+ return o[:-1]
+ return ''
+
+def dict_tostring(i):
+ o = ''
+ if i:
+ for k, v in i.items():
+ o += '**' + k + ':** `' + tostring(v) + '`\n'
+ return o
+
+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/main/utils/scraper.py b/src/main/utils/scraper.py
new file mode 100644
index 0000000..6e3bb2e
--- /dev/null
+++ b/src/main/utils/scraper.py
@@ -0,0 +1,33 @@
+import requests
+from bs4 import BeautifulSoup
+from lxml import html
+from misc import exceptions as exc
+
+def check_match(url):
+ r = requests.get(url)
+ soup = BeautifulSoup(r.content, 'html.parser')
+ value = soup.find_all('a')[1].get('href')
+ if value != '#':
+ return value
+ else:
+ raise exc.MatchError(value)
+
+def find_pool(url):
+ r = requests.get(url)
+ tree = html.fromstring(r.content)
+ post = tree.xpath('/html/body/div[@id="content"]/div[@id="pool-show"]/div[@style="margin-top: 2em;"]/span/a/@href')
+ print(post)
+ if post:
+ return post
+ else:
+ raise exc.PostError(post)
+
+def find_image_url(url):
+ r = requests.get(url)
+ tree = html.fromstring(r.content)
+ image_url = tree.xpath('/html/body/div[@id="content"]/div[@id="post-view"]/div[@class="content"]/div/img/@src')
+ print(image_url)
+ if image_url:
+ return image_url
+ else:
+ raise exc.ImageError(image_url)