From a824c068e7363ba519dec39577e12fe7639f5476 Mon Sep 17 00:00:00 2001 From: Adriene Hutchins Date: Mon, 2 Mar 2020 23:08:20 -0500 Subject: [PATCH] New get_webhook and logging features --- extensions/search.py | 89 +++++++++++------- extensions/utils/logging.py | 181 ++++++++++++++++++++++++++++++++++++ extensions/utils/online.py | 18 +++- main.py | 66 +------------ 4 files changed, 254 insertions(+), 100 deletions(-) create mode 100644 extensions/utils/logging.py diff --git a/extensions/search.py b/extensions/search.py index bb36283..dfa2bf3 100644 --- a/extensions/search.py +++ b/extensions/search.py @@ -19,6 +19,8 @@ class Search(commands.Cog): # Main Stuff self.bot = bot + self.info = bot.logging.info + self.warn = bot.logging.warn self.request = bot.request self.instances = bot.instances self.emoji = "\U0001F50D" @@ -57,7 +59,6 @@ class Search(commands.Cog): with open('searxes.txt') as f: self.instances = f.read().split('\n') instance = random.sample(self.instances, k=1)[0] - print(f"Attempting to use {instance}") # Error Template error_msg = ( @@ -81,9 +82,6 @@ class Search(commands.Cog): # Figure out engines for different categories to get decent results. if category == 'videos': call += '&engines=bing+videos,google+videos' - - print(call) - # Make said API call try: async with self.request.get(call) as resp: @@ -121,17 +119,21 @@ class Search(commands.Cog): # Instance Info msg += f"\n\n_Results retrieved from instance `{instance}`._" + return msg + # Reached if error with returned results except (KeyError, IndexError) as e: # Logging - print(f"{e} with instance {instance}, trying again.") + self.warn( + f"A user encountered a(n) `{e}` with <{instance}> when searching for `{query}`. " + "Consider removing it or looking into it.", + name="Failed Instance" + ) self.instances.remove(instance) # Weed the instance out # Recurse until good response return await self._search_logic(query, is_nsfw) - return msg - async def _instance_check(self, instance, info): """Checks the quality of an instance.""" @@ -172,84 +174,94 @@ class Search(commands.Cog): async def search(self, ctx, *, query: str): """Search online for general results.""" - # Logging - print(f"\n\nNEW CALL: {ctx.author} from {ctx.guild}.\n") - - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw()) + await self.info( + content=f"**{ctx.author}** searched for `{query}` in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command(aliases=['video']) async def videos(self, ctx, *, query: str): """Search online for videos.""" - # Logging - print(f"\n\nNEW VIDEO CALL: {ctx.author} from {ctx.guild}.\n") - - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'videos') + await self.info( + content=f"**{ctx.author}** searched for `{query}` videos in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command() async def music(self, ctx, *, query: str): """Search online for music.""" - # Logging - print(f"\n\nNEW MUSIC CALL: {ctx.author} from {ctx.guild}.\n") - - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'music') + await self.info( + content=f"**{ctx.author}** searched for `{query}` music in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command(aliases=['file']) async def files(self, ctx, *, query: str): """Search online for files.""" - # Logging - print(f"\n\nNEW FILES CALL: {ctx.author} from {ctx.guild}.\n") - - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'files') + await self.info( + content=f"**{ctx.author}** searched for `{query}` files in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command(aliases=['image']) async def images(self, ctx, *, query: str): """Search online for images.""" - # Logging - print(f"\n\nNEW IMAGES CALL: {ctx.author} from {ctx.guild}.\n") - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'images') + await self.info( + content=f"**{ctx.author}** searched for `{query}` images in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command() async def it(self, ctx, *, query: str): """Search online for IT-related information.""" - # Logging - print(f"\n\nNEW IT CALL: {ctx.author} from {ctx.guild}.\n") - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'it') + await self.info( + content=f"**{ctx.author}** searched for `{query}` IT in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command(aliases=['map']) async def maps(self, ctx, *, query: str): """Search online for map information.""" - # Logging - print(f"\n\nNEW MAP CALL: {ctx.author} from {ctx.guild}.\n") - # Handling async with ctx.typing(): msg = await self._search_logic(query, ctx.channel.is_nsfw(), 'map') + await self.info( + content=f"**{ctx.author}** searched for `{query}` maps in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) await ctx.send(msg) @commands.command() @@ -287,9 +299,7 @@ class Search(commands.Cog): if isinstance(error, commands.CommandNotFound) or \ isinstance(error, commands.CheckFailure): - # Logging - print(f"\n\nNEW CALL: {ctx.author} from {ctx.guild}.\n") - + # Handling async with ctx.typing(): # Prepares term @@ -297,6 +307,15 @@ class Search(commands.Cog): term = term.lstrip(' ') # Does search msg = await self._search_logic(term, ctx.channel.is_nsfw()) + + # Logging + await self.info( + content=f"**{ctx.author}** searched for `{term}` in \"{ctx.guild}\" and got this:" + f"\n\n{msg}", + name="Search Results" + ) + + # Sends result await ctx.send(msg) diff --git a/extensions/utils/logging.py b/extensions/utils/logging.py new file mode 100644 index 0000000..b918ce6 --- /dev/null +++ b/extensions/utils/logging.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +# tacibot logging util +# Provides utils for logging via webhooks. + +'''Online File''' + +import discord +from discord.ext.commands import Context +import traceback +from typing import Optional + + +class Logging(): + """Provides logging utilities for bots.""" + + def __init__(self, bot): + self.bot = bot + self.request = bot.request + self.online = bot.online + self.maintenance = bot.maintenance + + # Sets info hook first + self.info_hook = self.online.get_webhook( + bot.config['INFO_HOOK'] if bot.config['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'] + else self.info_hook + ) + self.error_hook = self.online.get_webhook( + bot.config['ERROR_HOOK'] if bot.config['ERROR_HOOK'] + else self.info_hook + ) + self.debug_hook = self.online.get_webhook( + bot.config['DEBUG_HOOK'] if bot.config['DEBUG_HOOK'] + else self.info_hook + ) + + # If no hooks, nothing is there + else: + self.warn_hook = self.error_hook = self.debug_hook = None + + async def _create_error_embed(self, error: Exception, ctx: Context): + """Creates a new basic error embed.""" + + # Prerequisites + formatted_tb = traceback.format_tb(error.__traceback__) + formatted_tb = ''.join(formatted_tb) + original_exc = traceback.format_exception( + type(error), error, error.__traceback__) + print(original_exc) + + # Hastebins Traceback + try: + url = await self.online.hastebin( + ''.join(original_exc)) + except Exception as e: + url = None + print(e) + + # Embed Building + error_embed = discord.Embed( + title=( + f"{type(error).__name__} " + f"{'(Click for Hastebin)' if url else ''}" + ), + url=url if url else None, + color=0xFF0000 + ) + + # Formats Traceback + trace_content = ( + "```py\n\nTraceback (most recent call last):" + "\n{}{}: {}```").format( + formatted_tb, + type(error).__name__, + error) + + # Adds Traceback + error_embed.add_field( + name=( + f"`{type(error).__name__}` in " + f"command `{ctx.command.qualified_name}`" + ), + value=(trace_content[:1018] + '...```') + if len(trace_content) > 1024 + else trace_content + ) + + # Provides completed embed + return error_embed + + async def info(self, content: str, + embed: Optional[discord.Embed] = None, + name: Optional[str] = None): + """Logs info and sends it to the appropriate places.""" + + if self.info_hook: + return await self.info_hook.send( + content=content, + username=f"{self.bot.user.name} - {name if name else 'unknown'}", + avatar_url=str(self.bot.user.avatar_url), + embed=embed + ) + else: + return + + async def warn(self, content: str, + embed: Optional[discord.Embed] = None, + name: Optional[str] = None): + """Logs warnings and sends them to the appropriate places.""" + + if self.warn_hook: + return await self.warn_hook.send( + content=content, + username=f"{self.bot.user.name} - {name if name else 'unknown'}", + avatar_url=str(self.bot.user.avatar_url), + embed=embed + ) + else: + return + + async def error(self, error: Exception, ctx: Context, name: Optional[str]): + """Logs errors and sends them to the appropriate places.""" + + # Prerequisites + error_embed = await self._create_error_embed(error, ctx) + + # Log and say so + if self.error_hook: + + # Log + fallback = ( + f"**{ctx.author}** encountered a(n) " + f"`{type(error).__name__}` when attempting to use " + f"`{ctx.command}`." + ) + await self.error_hook.send( + content=fallback, + username=f"{self.bot.user.name} - {name if name else 'unknown'}", + avatar_url=str(self.bot.user.avatar_url), + embed=error_embed + ) + + # Say so + is_reported = "has been automatically reported, but" + + # Say hasn't been logged + else: + is_reported = "has not been automatically reported, so" + + # Added reported info and return + error_embed.description = ( + f"This is (probably) a bug. This {is_reported} " + f"please give **{self.bot.appinfo.owner}** a heads-up in DMs." + ) + return error_embed + + async def debug(self, content: str, + embed: Optional[discord.Embed] = None, + name: Optional[str] = None): + """Logs warnings and sends them to the appropriate places.""" + + if self.debug_hook and self.maintenance: + return await self.debug_hook.send( + content=content, + username=f"{self.bot.user.name} - {name if name else 'unknown'}", + avatar_url=str(self.bot.user.avatar_url), + embed=embed + ) + else: + return + + +def setup(bot): + bot.logging = Logging(bot) diff --git a/extensions/utils/online.py b/extensions/utils/online.py index a5edb2e..11362f2 100644 --- a/extensions/utils/online.py +++ b/extensions/utils/online.py @@ -5,7 +5,12 @@ '''Online File''' +import discord + + class Online(): + """Provides various online utilities for your bot.""" + def __init__(self, bot): self.bot = bot self.request = bot.request @@ -18,11 +23,16 @@ class Online(): async with self.request.post(url=url, data=data) as haste_response: haste_key = (await haste_response.json())['key'] haste_url = f"http://hastebin.com/{haste_key}" - # data = {'sprunge': ''} - # data['sprunge'] = string - # haste_url = await self.aioclient.post(url='http://sprunge.us', - # data=data) return haste_url + def get_webhook(self, url: str): + """Easily gets a webhook from a url.""" + + return discord.Webhook.from_url( + url, + adapter=discord.AsyncWebhookAdapter(self.request) + ) + + def setup(bot): bot.online = Online(bot) diff --git a/main.py b/main.py index b95c796..df08346 100644 --- a/main.py +++ b/main.py @@ -14,18 +14,15 @@ import json import os import asyncio import aiohttp +import logging import random - class Bot(commands.Bot): """Custom Bot Class that subclasses the commands.ext one""" def __init__(self, **options): """Initializes the main parts of the bot.""" - # Logging - print('Initializing...\n') - # Initializes parent class super().__init__(self._get_prefix_new, **options) @@ -43,8 +40,6 @@ class Bot(commands.Bot): with open('searxes.txt') as f: self.instances = f.read().split('\n') - # Logging - def _init_extensions(self): """Initializes extensions.""" @@ -163,73 +158,22 @@ async def on_command_error(ctx, error): # Lets other cogs handle CommandNotFound. # Change this if you want command not found handling - if isinstance(error, commands.CommandNotFound): + if isinstance(error, commands.CommandNotFound)or isinstance(error, commands.CheckFailure): return # Provides a very pretty embed if something's actually a dev's fault. elif isinstance(error, commands.CommandInvokeError): # Prerequisites - original_error = error.original - traceback_orig = traceback.format_tb(original_error.__traceback__) - traceback_orig = ''.join(traceback_orig) - appinfo = await bot.application_info() - original_exc = traceback.format_exception( - type(original_error), original_error, original_error.__traceback__) - print(original_exc) - - # Main Message - embed_fallback = f"**An error occured: {type(original_error).__name__}. Please contact {appinfo.owner}.**" - - # Hastebins Traceback - try: - url = await bot.online.hastebin( - ''.join(original_exc)) - except Exception as e: - url = None - print(e) - - # Embed Building - error_embed = discord.Embed( - title=( - f"{type(original_error).__name__} " - f"{'(Click for Hastebin)' if url else ''}" - ), - url=url if url else None, - color=0xFF0000, - description=( # TODO Change if has logging - "This is (probably) a bug. This has not been automatically " - f"reported, so please give **{appinfo.owner}** a heads-up in DMs.") - ) - - # Formats Traceback - trace_content = ( - "```py\n\nTraceback (most recent call last):" - "\n{}{}: {}```").format( - traceback_orig, - type(original_error).__name__, - original_error) - - # Adds Traceback - error_embed.add_field( - name=( - f"`{type(original_error).__name__}` in " - f"command `{ctx.command.qualified_name}`" - ), - value=(trace_content[:1018] + '...```') - if len(trace_content) > 1024 - else trace_content - ) + embed_fallback = f"**An error occured: {type(error).__name__}. Please contact {bot.appinfo.owner}.**" + error_embed = await bot.logging.error(error, ctx, ctx.command.cog.__name__) # Sending await ctx.send(embed_fallback, embed=error_embed) # If anything else goes wrong, just go ahead and send it in chat. else: - original_error = error - original_exc = traceback.format_exception( - type(original_error), original_error, original_error.__traceback__) - print(''.join(original_exc)) + await bot.logging.error(error, ctx, ctx.command.cog.__name__) await ctx.send(error) # NOTE Bot Entry Point # Starts the bot