diff --git a/.gitignore b/.gitignore index 4f584dc..d41efc8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Bot Related config.json extensions/__pycache__/ +utils/__pycache__/ +.mypy_cache/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -52,6 +54,4 @@ coverage.xml # Sphinx documentation docs/_build/ -extensions/__pycache__/core.cpython-38.pyc -extensions/__pycache__/core.cpython-38.pyc -extensions/__pycache__/core.cpython-38.pyc + diff --git a/config-example.json b/config-example.json index 078d309..a489e55 100644 --- a/config-example.json +++ b/config-example.json @@ -1,23 +1,39 @@ { - "VERSION": "x.x type", - "DESCRIPTION": "a basic base for bots, designed by taciturasa.", - "REPO": "", - "SERVER": "", - "MAINTENANCE": false, + "VERSION": "1.9 testing", + "DESCRIPTION": "a minimalist search utility bot for discord, designed by taciturasa.", + "REPO": "https://github.com/taciturasa/searchbot-discord", + "SERVER": "https://discord.gg/4BpReNV", "TOKEN": "", - "DBL": "", - "DBOTS": "", - "BOD": "", - "DBLCOM": "", - "PREFIX": ["basicbot!"], + "PREFIX": ["search!"], + + "MAINTENANCE": false, "CASE_INSENSITIVE": true, "PREFIXLESS_DMS": true, "MENTION_ASSIST": true, "CUSTOM_HELP": true, - "INFO_HOOK": "", - "WARN_HOOK": "", - "ERROR_HOOK": "", - "DEBUG_HOOK": "", - "PERMS": null, - "BLOCKED": [] -} + + "PERMS": 378944, + "BLOCKED": [], + + "BOTLISTS": { + "DBL": "", + "DBOTS": "", + "BOD": "", + "DBLCOM": "" + }, + + "HOOKS": { + "INFO_HOOK": "", + "WARN_HOOK": "", + "ERROR_HOOK": "", + "DEBUG_HOOK": "" + }, + + "RETHINK": { + "DB": "", + "USERNAME": "", + "PASSWORD": "", + "HOST": "", + "PORT": null + } +} \ No newline at end of file diff --git a/extensions/botlist.py b/extensions/botlist.py index da259ec..5cedd1c 100644 --- a/extensions/botlist.py +++ b/extensions/botlist.py @@ -23,10 +23,10 @@ class BotList(commands.Cog, name='Bot List'): self.emoji = "\U0001F5F3" # List Tokens - self.dbl_token = bot.config['DBL'] - self.dbots_token = bot.config['DBOTS'] - self.bod_token = bot.config['BOD'] - self.dblcom_token = bot.config['DBLCOM'] + self.dbl_token = bot.config['BOTLISTS']['DBL'] + self.dbots_token = bot.config['BOTLISTS']['DBOTS'] + self.bod_token = bot.config['BOTLISTS']['BOD'] + self.dblcom_token = bot.config['BOTLISTS']['DBLCOM'] # top.gg client self.dbl_client = dbl.DBLClient( @@ -139,7 +139,7 @@ class BotList(commands.Cog, name='Bot List'): @tasks.loop(minutes=15.0) async def update_stats(self): """Automatically updates statistics every 15 minutes.""" - + responses = await self._update_logic() print(responses) # TODO See other todo diff --git a/extensions/core.py b/extensions/core.py index 016245f..edbc149 100644 --- a/extensions/core.py +++ b/extensions/core.py @@ -15,6 +15,7 @@ import cpuinfo import math import psutil from extensions.models.help import TaciHelpCommand +from typing import List, Optional class Core(commands.Cog): @@ -29,11 +30,37 @@ class Core(commands.Cog): # Help Command self._original_help_command = bot.help_command - if bot.config['CUSTOM_HELP']: + if bot.custom_help: bot.help_command = TaciHelpCommand() bot.help_command.cog = self - def _humanbytes(self, B) -> str: # function lifted from StackOverflow + def _create_tutorial(self, guild) -> str: + """Creates the tutorial message.""" + + prefixes: str = f"`@{self.bot.user.name}`" + if self.bot.prefix: + others: str = ', '.join(f'`{p}`' for p in self.bot.prefix) + prefixes += f', {others}' + + msg: str = ( + f"**Hi!** Thanks for adding me to `{guild.name}`.\n\n" + f"I'm **{self.bot.user.name}** - _{self.bot.description}_\n\n" + f"My prefix{'es are' if self.bot.prefix else ' is'}: " + f"{prefixes}.\n\n" + "You may find more information with `help`.\n\n" + "_Please note that this bot may log errors, guild names, " + "command calls/contents, and the names of command users " + "for debug and maintenance purposes. " + "These logs are shared with nobody " + "other than those who help develop this bot. " + "If you do not agree to this, please remove this bot._\n\n" + "_You may recall this message at any time with `tutorial`._" + ) + + return msg + + # function lifted from StackOverflow + def _humanbytes(self, B) -> str: """Return the given bytes as a human friendly KB, MB, GB, or TB string.""" B = float(B) @@ -53,6 +80,8 @@ class Core(commands.Cog): return '{0:.2f} GB'.format(B/GB) elif TB <= B: return '{0:.2f} TB'.format(B/TB) + else: + return 'ERROR' @commands.command(aliases=['info', 'source', 'server']) async def about(self, ctx): @@ -60,10 +89,10 @@ class Core(commands.Cog): msg = f"__**{self.bot.user.name}**__ - _{self.bot.description}_\n\n" msg += f"This instance by **{self.bot.appinfo.owner}.**\n\n" - if self.bot.config['REPO']: - msg += f"**Source Code:** _<{self.bot.config['REPO']}>_\n" - if self.bot.config['SERVER:']: - msg += f"**Support Server:** _<{self.bot.config['SERVER']}>_\n\n" + if self.bot.repo: + msg += f"**Source Code:** _<{self.bot.repo}>_\n" + if self.bot.support_server: + msg += f"**Support Server:** _<{self.bot.support_server}>_\n\n" msg += "_Note: Please attempt to contact the hoster of any separate instances before this server._\n" msg += f"_See **{ctx.prefix}**`help` for help, `invite` to add the bot, and `stats` for statistics._" @@ -79,13 +108,23 @@ class Core(commands.Cog): f"**" + if self.bot.perms: + msg += f"&permissions={self.bot.perms}>*" else: msg += ">*" await ctx.send(msg) + @commands.command() + async def tutorial(self, ctx): + """Resends the tutorial message.""" + if ctx.guild: + msg: str = self._create_tutorial(ctx.guild) + else: + msg: str = "**Cannot send tutorial in DMs!**" + + await ctx.send(msg) + @commands.command() async def stats(self, ctx): """Provides statistics on the bot itself.""" @@ -116,10 +155,10 @@ Number of extensions present: {len(ctx.bot.cogs)} ping = (after - before) * 1000 await pong.edit(content="`PING discordapp.com {}ms`".format(int(ping))) - @commands.group(aliases=['extensions', 'ext'], + @commands.group(aliases=['extensions', 'ext'], invoke_without_command=True) @commands.is_owner() - async def extend(self, ctx, name:str = None): + async def extend(self, ctx, name: str = None): """Provides status of extensions and lets you hotswap extensions.""" # Provides status of extension @@ -212,19 +251,81 @@ Number of extensions present: {len(ctx.bot.cogs)} @commands.is_owner() async def leave(self, ctx): """Makes the bot leave the server this was called in.""" - + if ctx.guild: await ctx.send( "\U0001F4A8 **Leaving server.** " - "_If you want me back, add me or get an admin to._") + "_If you want me back, add me or get an admin to._" + ) await ctx.guild.leave() else: await ctx.send( - "**Can't leave!** _This channel is not inside a guild._") + "**Can't leave!** _This channel is not inside a guild._" + ) def cog_unload(self): self.bot.help_command = self._original_help_command + @commands.Cog.listener() + async def on_guild_join(self, guild): + """Sends owner notification and guild tutorial.""" + + # Prerequisites + guild_msg: str = self._create_tutorial(guild) + channel: Optional[discord.TextChannel] = None + owner: discord.Member = guild.owner + + # Tutorial Message + # Get text channels + text_channels = [] + for c in guild.channels: + if type(c) is discord.TextChannel: + text_channels.append(c) + + # Sets channel to general if it exists + for c in guild.channels: + if c.name == 'general': + channel = c + + # XXX This looks like garbage + # Else posts in first open channel + if not channel: + for c in guild.channels: + if c.permissions_for(guild.me).send_messages: + channel = c + + # Send tutorial message + if channel: + await channel.send(guild_msg) + else: + guild_msg += ( + "\n\n_I am sending this message to you as there were no " + "channels I could send messages to in your server. " + "Please give me send message permissions in the channels " + "You wish to use me in!_" + ) + + await guild.owner.send(guild_msg) + return # Ends here if there are no good channels to send to + + # Owner Disclosure + # Message Building + owner_msg = ( + "**Hi there!**\n\n" + f"I am **{self.bot.user.name}** - _{self.bot.description}_\n\n" + "I am messaging you to inform you I was added to your server, " + f"`{guild.name}`, by someone " + "with **Manage Server** permissions.\n\n" + f"I have sent a tutorial message to `{channel.name}` " + "describing how I may be used.\n\n" + "If you do not wish to have me there, " + "simply kick me from the server.\n\n" + "_Thanks for your time!_" + ) + + # Send owner disclosure + await guild.owner.send(owner_msg) + def setup(bot): bot.add_cog(Core(bot)) diff --git a/extensions/search.py b/extensions/search.py index 310c64d..43d1b31 100644 --- a/extensions/search.py +++ b/extensions/search.py @@ -23,9 +23,13 @@ class Search(commands.Cog): self.info = bot.logging.info self.warn = bot.logging.warn self.request = bot.request - self.instances = bot.instances self.emoji = "\U0001F50D" + # Get Instances + with open('searxes.txt') as f: + self.instances = f.read().split('\n') + + async def _search_logic(self, query: str, is_nsfw: bool = False, category: str = None) -> str: """Provides search logic for all search commands.""" diff --git a/extensions/utils/logging.py b/extensions/utils/logging.py index 03a23ac..67c7ee2 100644 --- a/extensions/utils/logging.py +++ b/extensions/utils/logging.py @@ -22,22 +22,22 @@ class Logging(): # Sets info hook first self.info_hook = self.online.get_webhook( - bot.config['INFO_HOOK'] if bot.config['INFO_HOOK'] + bot.config['HOOKS']['INFO_HOOK'] if bot.config['HOOKS']['INFO_HOOK'] else None ) # Sets other hooks or defaults them if self.info_hook: self.warn_hook = self.online.get_webhook( - bot.config['WARN_HOOK'] if bot.config['WARN_HOOK'] + bot.config['HOOKS']['WARN_HOOK'] if bot.config['HOOKS']['WARN_HOOK'] else self.info_hook ) self.error_hook = self.online.get_webhook( - bot.config['ERROR_HOOK'] if bot.config['ERROR_HOOK'] + bot.config['HOOKS']['ERROR_HOOK'] if bot.config['HOOKS']['ERROR_HOOK'] else self.info_hook ) self.debug_hook = self.online.get_webhook( - bot.config['DEBUG_HOOK'] if bot.config['DEBUG_HOOK'] + bot.config['HOOKS']['DEBUG_HOOK'] if bot.config['HOOKS']['DEBUG_HOOK'] else self.info_hook ) diff --git a/main.py b/main.py index 4006157..5f2c2ba 100644 --- a/main.py +++ b/main.py @@ -12,10 +12,12 @@ from discord.ext import commands import traceback import json import os +import sys import asyncio import aiohttp -import logging -import random +import rethinkdb +from typing import List, Optional + class Bot(commands.Bot): """Custom Bot Class that subclasses the commands.ext one""" @@ -27,24 +29,38 @@ class Bot(commands.Bot): super().__init__(self._get_prefix_new, **options) # Setup - self.extensions_list = [] + self.extensions_list: List[str] = [] + with open('config.json') as f: self.config = json.load(f) - self.prefix = self.config['PREFIX'] - self.version = self.config['VERSION'] - self.maintenance = self.config['MAINTENANCE'] - self.description = self.config['DESCRIPTION'] - self.case_insensitive = self.config['CASE_INSENSITIVE'] - # Get Instances - with open('searxes.txt') as f: - self.instances = f.read().split('\n') + # Info + self.prefix: List[str] = self.config['PREFIX'] + self.version: str = self.config['VERSION'] + self.description: str = self.config['DESCRIPTION'] + self.repo: str = self.config['REPO'] + self.support_server: str = self.config['SERVER'] + self.perms: int = self.config['PERMS'] + + # Toggles + self.maintenance: bool = self.config['MAINTENANCE'] + self.case_insensitive: bool = self.config['CASE_INSENSITIVE'] + self.custom_help: bool = self.config['CUSTOM_HELP'] + self.mention_assist: bool = self.config['MENTION_ASSIST'] + self.prefixless_dms: bool = self.config['PREFIXLESS_DMS'] + + # RethinkDB + if self.config['RETHINK']['DB']: + self.re = rethinkdb.RethinkDB() + self.re.set_loop_type('asyncio') + self.rdb: str = self.config['RETHINK']['DB'] + self.conn = None + self.rtables: List[str] = [] def _init_extensions(self): """Initializes extensions.""" # Utils - # Avoids race conditions with online utils_dir = os.listdir('extensions/utils') if 'online.py' in utils_dir: @@ -80,16 +96,50 @@ class Bot(commands.Bot): f'extensions.{ext[:-3]}') except Exception as e: print(e) - + + async def _init_rethinkdb(self): + """Initializes RethinkDB.""" + + # Prerequisites + dbc = self.config['RETHINK'] + + # Error handling the initialization + try: + # Create connection + self.conn = await self.re.connect( + host=dbc['HOST'], + port=dbc['PORT'], + db=dbc['DB'], + user=dbc['USERNAME'], + password=dbc['PASSWORD'] + ) + + # Create or get database + dbs = await self.re.db_list().run(self.conn) + if self.rdb not in dbs: + print('Database not present. Creating...') + await self.re.db_create(self.rdb).run(self.conn) + + # Append any existing tables to rtables + tables = await self.re.db(self.rdb).table_list().run(self.conn) + self.rtables.extend(tables) + + # Exit if fails bc bot can't run without db + except Exception as e: + print('RethinkDB init error!\n{}: {}'.format(type(e).__name__, e)) + sys.exit(1) + + print('RethinkDB initialisation successful.') async def _get_prefix_new(self, bot, msg): """More flexible check for prefix.""" # Adds empty prefix if in DMs - if isinstance(msg.channel, discord.DMChannel) and self.config['PREFIXLESS_DMS']: + if isinstance(msg.channel, discord.DMChannel) and self.prefixless_dms: plus_empty = self.prefix.copy() plus_empty.append('') return commands.when_mentioned_or(*plus_empty)(bot, msg) + # Keeps regular if not else: return commands.when_mentioned_or(*self.prefix)(bot, msg) @@ -125,9 +175,14 @@ class Bot(commands.Bot): status=discord.Status.online ) + # NOTE Rethink Entry Point + # Initializes all rethink stuff + if hasattr(self, 'rdb') and not self.rtables: + await self._init_rethinkdb() + # NOTE Extension Entry Point # Loads core, which loads all other extensions - if self.extensions_list == []: + if not self.extensions_list: self._init_extensions() print('Initialized.\n') @@ -167,7 +222,7 @@ class Bot(commands.Bot): return # Empty ping for assistance - elif message.content in mentions and self.config.get('MENTION_ASSIST'): + elif message.content in mentions and self.mention_assist: assist_msg = ( "**Hi there! How can I help?**\n\n" # Two New Lines Here @@ -197,12 +252,12 @@ async def on_command_error(ctx, error): elif isinstance(error, commands.CommandInvokeError): # Prerequisites - embed_fallback = f"**An error occured: {type(error).__name__}. Please contact {bot.appinfo.owner}.**" + embed_fallback = f"**An error occured: {type(error).__name__}. Please contact {bot.appinfo.owner}.**" error_embed = await bot.logging.error( - error, ctx, + error, ctx, ctx.command.cog.qualified_name if ctx.command.cog.qualified_name else "DMs" - ) + ) # Sending await ctx.send(embed_fallback, embed=error_embed) @@ -210,7 +265,7 @@ async def on_command_error(ctx, error): # If anything else goes wrong, just go ahead and send it in chat. else: await bot.logging.error( - error, ctx, + error, ctx, ctx.command.cog.qualified_name if ctx.command.cog.qualified_name else "DMs" ) @@ -218,4 +273,4 @@ async def on_command_error(ctx, error): # NOTE Bot Entry Point # Starts the bot print("Connecting...\n") -bot.run(bot.config['TOKEN']) +bot.run(bot.config['TOKEN']) \ No newline at end of file