Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging improvements #15

Merged
merged 23 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.4

FROM --platform=$TARGETPLATFORM python:3.11.6
FROM --platform=$TARGETPLATFORM python:3.12.3

WORKDIR /usr/algalon

Expand Down
118 changes: 24 additions & 94 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import os
import sys
import cogs
import logging
import yaml
import atexit
import discord
import logging
import platform
import logging.config

from discord.ext import bridge, commands
from logging.handlers import TimedRotatingFileHandler
from discord.ext import bridge

try:
if platform.machine() != "armv7l":
from dotenv import load_dotenv

load_dotenv()
except ImportError:
pass

if not os.getenv("DEBUG"):
sys.exit(0)

OWNER_ID = int(os.getenv("OWNER_ID"))

Expand All @@ -24,23 +28,17 @@
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)

logger = logging.getLogger("discord")
logger.setLevel(logging.DEBUG)
log_format = logging.Formatter("[%(asctime)s]:[%(levelname)s:%(name)s]: %(message)s")
log_cfg_path = os.path.join(DIR, "log_config.yaml")
with open(log_cfg_path) as f:
log_cfg = yaml.safe_load(f)
logging.config.dictConfig(log_cfg)

console_handler = logging.StreamHandler()
console_handler.setFormatter(log_format)
console_handler.setLevel(logging.DEBUG)

logger.addHandler(console_handler) # adds console handler to our logger

file_handler = TimedRotatingFileHandler(
filename=LOG_FILE, encoding="utf-8", when="midnight", backupCount=30
)
file_handler.setFormatter(log_format)
file_handler.setLevel(logging.DEBUG)
queue_handler = logging.getHandlerByName("queue_handler")
if queue_handler is not None:
queue_handler.listener.start()
atexit.register(queue_handler.listener.stop)

logger.addHandler(file_handler) # adds filehandler to our logger
logger = logging.getLogger("discord")

logger.info(
f"Using Python version {sys.version}",
Expand All @@ -49,88 +47,24 @@
cogs.log_start()


# This subclasses the default help command to provide our bot with a prettier help command.
class CDNBotHelpCommand(commands.HelpCommand):
def get_command_signature(self, command):
return "%s%s %s" % (
self.context.clean_prefix,
command.qualified_name,
command.signature,
)

async def send_bot_help(self, mapping):
embed = discord.Embed(title="Help", color=discord.Color.blue())

for cog, commands in mapping.items():
filtered = await self.filter_commands(commands, sort=True)

if command_signatures := [self.get_command_signature(c) for c in filtered]:
cog_name = getattr(cog, "qualified_name", "No Category")
embed.add_field(
name=cog_name, value="\n".join(command_signatures), inline=False
)

await self.get_destination().send(embed=embed)

async def send_command_help(self, command):
embed = discord.Embed(
title=self.get_command_signature(command), color=discord.Color.random()
)

if command.help:
embed.description = command.help
if alias := command.aliases:
embed.add_field(name="Aliases", value=", ".join(alias), inline=False)

await self.get_destination().send(embed=embed)

async def send_help_embed(self, title, description, commands):
embed = discord.Embed(
title=title, description=description or "No help found..."
)

if filtered_commands := await self.filter_commands(commands):
for command in filtered_commands:
embed.add_field(
name=self.get_command_signature(command),
value=command.help or "No help found...",
)

await self.get_destination().send(embed=embed)

async def send_group_help(self, group):
title = self.get_command_signature(group)
await self.send_help_embed(title, group.help, group.commands)

async def send_cog_help(self, cog):
title = cog.qualified_name or "No"
await self.send_help_embed(
f"{title} Category", cog.description, cog.get_commands()
)


# The almighty Algalon himself
class CDNBot(bridge.Bot):
"""This is the almighty CDN bot, also known as Algalon. Inherits from `discord.ext.bridge.Bot`."""

COGS_LIST = ["watcher", "nux", "admin"]

def __init__(self, command_prefix, help_command=None, **options):
def __init__(self, command_prefix, **options):
command_prefix = command_prefix or "!"
help_command = help_command or commands.DefaultHelpCommand()

super().__init__(
command_prefix=command_prefix, help_command=help_command, **options # type: ignore
)
super().__init__(command_prefix=command_prefix, **options) # type: ignore

for cog in self.COGS_LIST:
logger.info(f"Loading {cog} cog...")
try:
self.load_extension(f"cogs.{cog}")
logger.info(f"{cog} cog loaded!")
except Exception as exc:
logger.error(f"Error loading cog {cog}")
logger.error(exc)
except Exception:
logger.error(f"Error loading cog '{cog}'", exc_info=True)

async def on_ready(self):
"""This `async` function runs once when the bot is connected to Discord and ready to execute commands."""
Expand All @@ -139,17 +73,14 @@ async def on_ready(self):
async def notify_owner_of_command_exception(
self, ctx: discord.ApplicationContext, exc: discord.DiscordException
):
owner = await self.get_or_fetch_user(self.owner_id) # type: ignore
dm_channel = await owner.create_dm() # type: ignore

message = f"An error occurred in command `{ctx.command}`:\n```py\n{exc.__class__.__name__}\n"
message += f"Args:\n"
message += "\n".join(arg for arg in exc.args)
message += f"\nCALLER: {ctx.author} ({ctx.author.id})\n"
message += f"GUILD: {ctx.guild} ({ctx.guild.id})\n```" # type: ignore
message += "See logs for traceback."

await dm_channel.send(message)
await self.send_message_to_owner(message)

async def send_message_to_owner(self, message: str):
owner = await self.get_or_fetch_user(self.owner_id) # type: ignore
Expand All @@ -172,7 +103,6 @@ async def send_message_to_owner(self, message: str):

bot = CDNBot(
command_prefix="!",
help_command=CDNBotHelpCommand(),
description="Algalon 2.0",
intents=discord.Intents.default(),
owner_id=OWNER_ID,
Expand Down
40 changes: 22 additions & 18 deletions cogs/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import os
import io
import logging
import discord

from time import time
from discord.ext import bridge, commands, tasks

from .config import LiveConfig
from discord.ext import bridge, commands

logger = logging.getLogger("discord.admin")

Expand Down Expand Up @@ -35,11 +33,11 @@ async def reload_cog(self, ctx: bridge.BridgeApplicationContext, cog_name: str):
return

cog_name_internal = f"cogs.{cog_name}"
logger.debug(f"Reloading {cog_name_internal}")
logger.info(f"Reloading {cog_name_internal}")
try:
self.bot.reload_extension(cog_name_internal)
except Exception as exc:
logger.error(f"Error reloading {cog_name_internal}.")
logger.error(f"Error reloading cog '{cog_name_internal}'", exc_info=True)
await self.bot.notify_owner_of_command_exception(ctx, exc)
await ctx.interaction.response.send_message(
f"busted.\n`{exc}`", ephemeral=True, delete_after=300
Expand All @@ -56,22 +54,19 @@ async def reload_cog(self, ctx: bridge.BridgeApplicationContext, cog_name: str):
@bridge.bridge_command(name="guilds", guild_ids=[HOME_GUILD], guild_only=True)
async def get_all_guilds(self, ctx: bridge.BridgeApplicationContext):
"""Dumps details for all guilds Algalon is a part of."""
message = "```\n"
await ctx.defer()
message = ""
for guild in self.bot.guilds:
guild = await self.bot.fetch_guild(guild.id)
message += f"""
{guild.name}
message += f"""Guild: {guild.name}
ID: {guild.id}
Members (approx): {guild.approximate_member_count}
Description: {guild.description or 'N/A'}
Icon: {guild.icon.url if guild.icon else 'N/A'}
Banner: {guild.banner.url if guild.banner else 'N/A'}
Members (approx): {guild.approximate_member_count}\n
"""

message += "```"
await ctx.interaction.response.send_message(
message, ephemeral=True, delete_after=300
)
message_bytes = io.BytesIO(message.encode())
file = discord.File(message_bytes, filename="guilds.txt")
await ctx.respond(file=file)
message_bytes.close()

@commands.is_owner()
@bridge.bridge_command(name="forceupdate", guild_ids=[HOME_GUILD], guild_only=True)
Expand All @@ -80,7 +75,7 @@ async def force_update_check(self, ctx: bridge.BridgeApplicationContext):
watcher = self.bot.get_cog("CDNCog")
await ctx.defer()
await watcher.cdn_auto_refresh()
await ctx.respond("Updates complete.")
await ctx.respond("Updates complete.", ephemeral=True, delete_after=300)

@bridge.bridge_command(
name="alien",
Expand All @@ -107,6 +102,15 @@ async def Perceive(
await message.add_reaction(emoji)
await ctx.respond("gottem", ephemeral=True, delete_after=5)

@commands.is_owner()
@bridge.bridge_command(name="nuxtest", guild_ids=[HOME_GUILD], guild_only=True)
async def nuxtest(self, ctx: bridge.BridgeApplicationContext):
guild = ctx.guild
nux_cog = self.bot.get_cog("GuildNUX")
message = nux_cog.get_nux_message(guild)

await ctx.respond(message, ephemeral=True, delete_after=300)


def setup(bot):
bot.add_cog(AdminCog(bot))
2 changes: 2 additions & 0 deletions cogs/api/blizzard.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""This file is never loaded and isn't useful, but keeping it for historical purposes"""

import os
import httpx
import asyncio
Expand Down
28 changes: 15 additions & 13 deletions cogs/api/blizzard_tact.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,23 @@ def parse_cdn_response(self, response):
all_data[region] = output

return all_data
except Exception as exc:
self.logger.error(f"Encountered an error parsing API response.")
self.logger.error(exc)
except Exception:
self.logger.error(
f"Encountered an error parsing API response", exc_info=True
)

return False

def construct_url(self, host: str, path: str, hash: str):
return f"http://{host}/{path}/{hash[:2]}/{hash[2:4]}/{hash}"

async def is_encrypted(self, branch: str, product_config_hash: str):
async with httpx.AsyncClient(timeout=2) as client:
async with httpx.AsyncClient(timeout=10) as client:
url = f"{self.__API_URL}{branch}{self.__API_ENDPOINT}"
try:
response = await client.get(url)
except httpx.ConnectTimeout as exc:
self.logger.error(f"TACT CDN info request for {branch} timed out.")
except httpx.ConnectTimeout:
self.logger.warning(f"TACT CDN info request for {branch} timed out")
return None

if response.status_code != 200:
Expand All @@ -71,32 +72,33 @@ async def is_encrypted(self, branch: str, product_config_hash: str):

while not product_config: # loop over all possible hosts until one works
if not data["us"]["hosts"]: # type: ignore
self.logger.error("Blizzard TACT hosts are broken. Help.")
self.logger.warning(
f"No working CDN hosts found for branch '{branch}'"
)
return None

host = data["us"]["hosts"].pop(0) # type: ignore
path = data["us"]["config_path"] # type: ignore
cdn_config_url = self.construct_url(host, path, product_config_hash)

try:
self.logger.info(
self.logger.debug(
f"Attempting to fetch product config for {branch}..."
)
cdn_response = await client.get(cdn_config_url)

if cdn_response.status_code != 200:
self.logger.debug(
self.logger.warning(
"Blizzard TACT host returned non 200 status code. Skipping..."
)
continue
else:
self.logger.info(
self.logger.debug(
f"{branch} product config found, returning encryption status..."
)
product_config = cdn_response.json()

except httpx.ConnectTimeout as exc:
self.logger.error("TACT API request timed out.")
self.logger.error(exc)
except httpx.ConnectTimeout:
self.logger.warning(f"CDN request for branch '{branch}' timed out")

return "decryption_key_name" in product_config["all"]["config"]
13 changes: 5 additions & 8 deletions cogs/api/twitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def send_tweet(self, embed, nonce: str):

logger.info("Sending tweet...")
if nonce in self.sent_tokens:
logger.info("Tweet already sent for this package. Skipping...")
logger.critical("Tweet already sent for this package. Skipping...")
return

is_warcraft = False
Expand Down Expand Up @@ -87,16 +87,13 @@ async def send_tweet(self, embed, nonce: str):
self.sent_tokens.append(nonce)
return
else:
logger.error(
f"Error occurred sending tweet. Please investigate.\n{response.errors or response}" # type: ignore
logger.critical(
f"Error occurred sending tweet.\n{response.errors or response}" # type: ignore
)
return response
except Exception as exc:
logger.error(
"Error occurred sending tweet. Please investigate.", exc_info=exc
)
except Exception:
logger.critical("Error occurred sending tweet.", exc_info=True)
return text
else:
logger.info("Debug mode enabled. Skipping tweet...")
logger.debug(f"Tweet text:\n{text}")
return
Loading
Loading