Skip to content

Commit 4a71616

Browse files
authored
Logging Start (#19)
* feat: Error And Guild Join/Leave Logging * chore: Run Black
1 parent 2e9de98 commit 4a71616

File tree

3 files changed

+151
-4
lines changed

3 files changed

+151
-4
lines changed

bot/cogs/developer.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import discord
2+
from discord import ApplicationContext
3+
4+
from utils.bot import SurveyWolf
5+
from main import bot as survey_wolf_bot
6+
7+
8+
class Developer(discord.Cog, guild_ids=survey_wolf_bot.config["dev_guilds"]):
9+
def __init__(self, bot):
10+
self.bot: SurveyWolf = bot
11+
12+
log_text = {
13+
"error_logging_webhook": "Errors",
14+
"server_join_leave_webhook": "Guild Join/Leave",
15+
}
16+
logs = [discord.OptionChoice(x[1], x[0]) for x in log_text.items()]
17+
logging = discord.SlashCommandGroup("logging", "Actions For The Discord Facing Logging")
18+
19+
async def cog_before_invoke(self, ctx: ApplicationContext) -> None:
20+
if ctx.guild_id not in self.bot.config["dev_guilds"]:
21+
raise Exception("Guild Not Authorized To Run Developer Command")
22+
23+
if not await self.bot.is_owner(ctx.user):
24+
raise Exception("User Not Authorized To Run Developer Command")
25+
26+
@discord.Cog.listener()
27+
async def on_guild_join(self, guild):
28+
await self.bot.config["server_join_leave_webhook"].send(
29+
f"Joined A New Server: {guild.id} Total: {len(self.bot.guilds)}"
30+
)
31+
32+
@discord.Cog.listener()
33+
async def on_guild_remove(self, guild):
34+
await self.bot.config["server_join_leave_webhook"].send(
35+
f"Left A Server: {guild.id} Total: {len(self.bot.guilds)}"
36+
)
37+
38+
async def _remove_webhook(self, w: discord.Webhook | None):
39+
if w is not None:
40+
try:
41+
if w.is_partial():
42+
w = await self.bot.fetch_webhook(w.id)
43+
await w.delete(reason="Logging Moved Or Disabled")
44+
except discord.NotFound:
45+
pass
46+
47+
@logging.command(description="Creates A Webhook In The Current Channel For The Specified Log")
48+
async def set(
49+
self, ctx: discord.ApplicationContext, log: discord.Option(str, description="The Log To Set", choices=logs)
50+
):
51+
w = await ctx.channel.create_webhook(
52+
name=f"{self.bot.user.name} {self.log_text[log]} Log",
53+
avatar=await self.bot.user.avatar.read() if self.bot.user.avatar else None,
54+
reason="Logging Enabled",
55+
)
56+
await self._remove_webhook(self.bot.config[log])
57+
self.bot.update_config(log, w, w.url)
58+
await ctx.respond("Logging Set", ephemeral=True)
59+
60+
@logging.command(description="Removes The Webhook For The Specified Log")
61+
async def unset(
62+
self, ctx: discord.ApplicationContext, log: discord.Option(str, description="The Log To Remove", choices=logs)
63+
):
64+
await self._remove_webhook(self.bot.config[log])
65+
self.bot.update_config(log, None, "None")
66+
await ctx.respond("Logging Unset", ephemeral=True)
67+
68+
69+
def setup(bot):
70+
bot.add_cog(Developer(bot))

bot/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
load_dotenv()
77

8-
COGS = ["utility", "survey.creation", "survey.active", "survey.results"]
8+
COGS = ["utility", "survey.creation", "survey.active", "survey.results", "developer"]
99

1010
intents = discord.Intents.default()
1111

bot/utils/bot.py

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import traceback
12
from abc import ABC
3+
from typing import Any
4+
25
import yaml
36

47
import discord
58
from utils.database import database
6-
from discord import Interaction
9+
from . import embed_factory as ef
10+
from discord import Interaction, ApplicationContext, DiscordException
711

812

913
class AdvContext(discord.ApplicationContext):
@@ -29,12 +33,85 @@ class AdvContext(discord.ApplicationContext):
2933
class SurveyWolf(discord.Bot, ABC):
3034
def __init__(self, description=None, *args, **options):
3135
super().__init__(description=description, *args, **options)
36+
self._did_on_ready = False
3237

3338
with open("config.yaml") as stream:
3439
config = yaml.safe_load(stream)
3540

36-
self.ERROR_LOGGING_WEBHOOK = config["error_logging_webhook"]
37-
self.SERVER_JOIN_LEAVE_WEBHOOK = config["server_join_leave_webhook"]
41+
self._raw_config = config
42+
self.config = self._raw_config.copy()
43+
44+
async def on_ready(self):
45+
if self._did_on_ready:
46+
return
47+
self._did_on_ready = True
48+
# Do Some Additional Processing On Some Config Items
49+
self.config.update(
50+
{
51+
"error_logging_webhook": await self._create_webhook(self.config["error_logging_webhook"]),
52+
"server_join_leave_webhook": await self._create_webhook(self.config["server_join_leave_webhook"]),
53+
}
54+
)
55+
56+
async def _create_webhook(self, url: str) -> discord.Webhook | None:
57+
if url == "None":
58+
return None
59+
60+
try:
61+
return await self.fetch_webhook(discord.Webhook.from_url(url, session=self.http._HTTPClient__session).id)
62+
except discord.NotFound:
63+
return None
64+
65+
def update_config(self, key: str, value, raw=None) -> None:
66+
"""
67+
Updates the config with the key and value. Updates the config file with the raw value
68+
:param key: The Config Key
69+
:param value: The Value The Bot Should Retrieve
70+
:param raw: The Value That Should Be Stored In The Config File. Defaults To `value`
71+
"""
72+
if raw is None:
73+
raw = value
74+
75+
self._raw_config[key] = raw
76+
with open("config.yaml", "w") as stream:
77+
yaml.safe_dump(self._raw_config, stream)
78+
self.config[key] = value
3879

3980
async def get_application_context(self, interaction: Interaction, cls=None) -> discord.ApplicationContext:
4081
return await super().get_application_context(interaction, cls=cls or AdvContext)
82+
83+
@staticmethod
84+
def _split_text(text: str, max_length: int, newline: bool = False) -> list[str]:
85+
texts = []
86+
while len(text) > max_length:
87+
if newline:
88+
try:
89+
ind = text[:max_length].rindex("\n")
90+
except ValueError:
91+
# No newline was found so fall back to character count
92+
ind = max_length
93+
else:
94+
ind = max_length
95+
96+
texts.append(text[:ind])
97+
# The +1 is to remove the newline character
98+
text = text[ind + 1:]
99+
texts.append(text)
100+
return texts
101+
102+
async def on_application_command_error(self, ctx: ApplicationContext, exception: DiscordException) -> None:
103+
if (w := self.config["error_logging_webhook"]) is not None:
104+
text = f"Error In {ctx.command.qualified_name} Guild ID: {ctx.guild_id} Channel ID: {ctx.channel_id}\n"
105+
text += "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
106+
for i in self._split_text(text, 1990, newline=True):
107+
await w.send(f"```py\n{i}\n```")
108+
await ctx.respond(embed=await ef.error("An Error Occurred"))
109+
raise exception
110+
111+
async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None:
112+
if (w := self.config["error_logging_webhook"]) is not None:
113+
text = "Error In " + event_method + "\n"
114+
text += traceback.format_exc()
115+
for i in self._split_text(text, 1990, newline=True):
116+
await w.send(f"```py\n{i}\n```")
117+
traceback.print_exc()

0 commit comments

Comments
 (0)