Skip to content

Commit 7b66d2b

Browse files
authored
Merge pull request #347 from rHomelab/refactor/guild_profiles_model
2 parents 152b11c + 775b2e1 commit 7b66d2b

File tree

1 file changed

+129
-74
lines changed

1 file changed

+129
-74
lines changed

guild_profiles/guild_profiles.py

Lines changed: 129 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import re
33
from dataclasses import asdict, dataclass
4+
from datetime import datetime, timezone
45
from pathlib import Path
56
from typing import Type
67

@@ -37,6 +38,35 @@ def to_dict(self) -> dict:
3738
return {k: str(v) if isinstance(v, Path) else v for k, v in asdict(self).items()}
3839

3940

41+
@dataclass
42+
class GuildProfile:
43+
name: str
44+
creator: int
45+
created: datetime
46+
icon_id: int
47+
banner_id: int
48+
49+
@classmethod
50+
def from_dict(cls: Type["GuildProfile"], name: str, data: dict) -> "GuildProfile":
51+
"""Create a GuildProfile from a dictionary."""
52+
return cls(
53+
name=name,
54+
creator=int(data["creator"]),
55+
created=datetime.fromtimestamp(int(data["created"]), tz=timezone.utc),
56+
icon_id=int(data["icon_id"]),
57+
banner_id=int(data["banner_id"]),
58+
)
59+
60+
def to_dict(self) -> dict:
61+
"""Convert a GuildProfile to a dictionary for storage (excluding name)."""
62+
profile_dict = asdict(self)
63+
# Remove name from the dict since it's used as the key in storage
64+
profile_dict.pop("name", None)
65+
# Convert datetime to timestamp for storage
66+
profile_dict["created"] = int(self.created.timestamp())
67+
return profile_dict
68+
69+
4070
class GuildProfilesCog(commands.Cog):
4171
"""Cog for managing guild profiles (icon and banner)"""
4272

@@ -171,6 +201,31 @@ async def _delete_asset(self, guild: discord.Guild, id: int, asset: GuildAsset):
171201
raise ValueError(f"Asset ID {asset_id} does not exist in the guild's assets.")
172202
del assets[asset_id]
173203

204+
async def _get_profile(self, guild: discord.Guild, name: str) -> GuildProfile:
205+
"""Get a GuildProfile object for a given profile name.
206+
207+
Args:
208+
guild: The guild to which the profile belongs.
209+
name: The name of the profile to retrieve.
210+
211+
Returns:
212+
A GuildProfile object containing the profile's details.
213+
214+
Raises:
215+
ValueError: If the profile name does not exist in the guild's profiles.
216+
"""
217+
profile_name = name.lower()
218+
219+
profiles = await self.config.guild(guild).profiles()
220+
if profile_name not in profiles:
221+
log.error(f"Profile '{profile_name}' does not exist in guild {guild.id}.")
222+
raise ValueError(f"Profile '{profile_name}' does not exist in the guild's profiles.")
223+
224+
profile_data = profiles[profile_name]
225+
log.debug(f"Retrieved profile: {profile_name} for guild {guild.id}")
226+
227+
return GuildProfile.from_dict(profile_name, profile_data)
228+
174229
async def _check_asset_assigned_profiles(self, guild: discord.Guild, id: int) -> list[str]:
175230
"""Check if an asset ID is currently in use by any guild profile.
176231
@@ -184,8 +239,9 @@ async def _check_asset_assigned_profiles(self, guild: discord.Guild, id: int) ->
184239
asset_assigned_profiles = []
185240
async with self.config.guild(guild).profiles() as profiles:
186241
for profile_name, profile_data in profiles.items():
187-
if int(profile_data.get("icon_id")) == id or int(profile_data.get("banner_id")) == id:
188-
asset_assigned_profiles.append(profile_name)
242+
profile = GuildProfile.from_dict(profile_name, profile_data)
243+
if id in (profile.icon_id, profile.banner_id):
244+
asset_assigned_profiles.append(profile.name)
189245
return asset_assigned_profiles
190246

191247
@commands.group(name="guildprofile") # type: ignore
@@ -248,12 +304,14 @@ async def create_profile_cmd(self, ctx: commands.GuildContext, name: str, icon_i
248304
return await ctx.send(f"A profile named '{name}' already exists. Please use a different name.")
249305

250306
# Create the profile
251-
profiles[profile_name] = {
252-
"creator": ctx.author.id,
253-
"created": int(discord.utils.utcnow().timestamp()),
254-
"icon_id": icon_id,
255-
"banner_id": banner_id,
256-
}
307+
new_profile = GuildProfile(
308+
name=profile_name,
309+
creator=ctx.author.id,
310+
created=discord.utils.utcnow(),
311+
icon_id=icon_id,
312+
banner_id=banner_id,
313+
)
314+
profiles[profile_name] = new_profile.to_dict()
257315

258316
log.info(
259317
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
@@ -268,14 +326,16 @@ async def list_profiles_cmd(self, ctx: commands.GuildContext):
268326
profiles = await self.config.guild(ctx.guild).profiles()
269327

270328
if not profiles:
271-
return await ctx.send("No guild profiles have been created yet.")
329+
await ctx.send("No guild profiles have been created yet.")
330+
return
272331

273332
profile_list = []
274333
for name, data in profiles.items():
275-
creator = ctx.guild.get_member(data["creator"])
334+
profile = GuildProfile.from_dict(name, data)
335+
creator = ctx.guild.get_member(profile.creator)
276336
creator_name = creator.display_name if creator else "Unknown User"
277-
created_time = f"<t:{data['created']}:R>"
278-
profile_list.append(f"- **{name}** - Created by {creator_name} {created_time}")
337+
created_time = f"<t:{int(profile.created.timestamp())}:R>"
338+
profile_list.append(f"- **{profile.name}** - Created by {creator_name} {created_time}")
279339

280340
profile_chunks = [
281341
profile_list[i : i + self.EMBED_PAGE_LENGTH] for i in range(0, len(profile_list), self.EMBED_PAGE_LENGTH)
@@ -295,10 +355,9 @@ async def list_profiles_cmd(self, ctx: commands.GuildContext):
295355
@profile_cmd.command(name="info")
296356
async def profile_info_cmd(self, ctx: commands.GuildContext, name: str):
297357
"""View information about a specific guild profile."""
298-
profiles = await self.config.guild(ctx.guild).profiles()
299-
name = name.lower()
300-
301-
if name not in profiles:
358+
try:
359+
profile = await self._get_profile(ctx.guild, name)
360+
except ValueError:
302361
log.debug(
303362
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
304363
+ f"attempted to view a non-existent profile: {name}"
@@ -308,18 +367,17 @@ async def profile_info_cmd(self, ctx: commands.GuildContext, name: str):
308367
# Show typing indicator while processing
309368
await ctx.typing()
310369

311-
profile = profiles[name]
312-
creator = ctx.guild.get_member(profile["creator"])
370+
creator = ctx.guild.get_member(profile.creator)
313371
creator_name = creator.mention if creator else "Unknown User"
314372

315-
embed = discord.Embed(title=f"Guild Profile: {name}", color=await ctx.embed_colour())
373+
embed = discord.Embed(title=f"Guild Profile: {profile.name}", color=await ctx.embed_colour())
316374
embed.add_field(name="Creator", value=creator_name, inline=True)
317-
embed.add_field(name="Created", value=f"<t:{profile['created']}:F>", inline=True)
375+
embed.add_field(name="Created", value=f"<t:{int(profile.created.timestamp())}:F>", inline=True)
318376

319377
# Get file paths for icon and banner
320378
try:
321-
icon_asset = await self._get_asset(ctx.guild, profile["icon_id"])
322-
banner_asset = await self._get_asset(ctx.guild, profile["banner_id"])
379+
icon_asset = await self._get_asset(ctx.guild, profile.icon_id)
380+
banner_asset = await self._get_asset(ctx.guild, profile.banner_id)
323381
except (ValueError, FileNotFoundError) as e:
324382
return await ctx.send(f"Failed to retrieve asset: {e!s}")
325383

@@ -345,10 +403,9 @@ async def update_profile_cmd(self, ctx: commands.GuildContext, name: str, icon_i
345403
- An icon asset ID to update the profile's icon.
346404
- A banner asset ID to update the profile's banner.
347405
"""
348-
profiles = await self.config.guild(ctx.guild).profiles()
349-
name = name.lower()
350-
351-
if name not in profiles:
406+
try:
407+
profile = await self._get_profile(ctx.guild, name)
408+
except ValueError:
352409
log.debug(
353410
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
354411
+ f"attempted to update a non-existent profile: {name}"
@@ -362,12 +419,10 @@ async def update_profile_cmd(self, ctx: commands.GuildContext, name: str, icon_i
362419
except (ValueError, FileNotFoundError) as e:
363420
return await ctx.send(f"Failed to retrieve asset: {e!s}")
364421

365-
async with self.config.guild(ctx.guild).profiles() as profiles:
366-
profiles[name]["icon_id"] = icon_id
367-
422+
profile.icon_id = icon_id
368423
log.info(
369424
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
370-
+ f"updated the icon for profile {name} to asset ID {icon_id}"
425+
+ f"updated the icon for profile {profile.name} to asset ID {icon_id}"
371426
)
372427

373428
# Update banner if provided
@@ -377,50 +432,50 @@ async def update_profile_cmd(self, ctx: commands.GuildContext, name: str, icon_i
377432
except (ValueError, FileNotFoundError) as e:
378433
return await ctx.send(f"Failed to retrieve asset: {e!s}")
379434

380-
async with self.config.guild(ctx.guild).profiles() as profiles:
381-
profiles[name]["banner_id"] = banner_id
382-
435+
profile.banner_id = banner_id
383436
log.info(
384437
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
385-
+ f"updated the banner for profile {name} to asset ID {banner_id}"
438+
+ f"updated the banner for profile {profile.name} to asset ID {banner_id}"
386439
)
387440

388-
await ctx.send(f"Guild profile '{name}' updated successfully.")
441+
# Save the updated profile back to config
442+
async with self.config.guild(ctx.guild).profiles() as profiles:
443+
profiles[profile.name] = profile.to_dict()
444+
445+
await ctx.send(f"Guild profile '{profile.name}' updated successfully.")
389446

390447
@profile_cmd.command(name="delete")
391448
async def delete_profile_cmd(self, ctx: commands.GuildContext, name: str):
392449
"""Delete a guild profile."""
393-
name = name.lower()
394-
395-
async with self.config.guild(ctx.guild).profiles() as profiles:
396-
if name not in profiles:
397-
log.debug(
398-
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
399-
+ f"attempted to delete a non-existent profile: {name}"
400-
)
401-
return await ctx.send(f"No profile named '{name}' exists.")
402-
403-
profile = profiles[name]
450+
try:
451+
profile = await self._get_profile(ctx.guild, name)
452+
except ValueError:
453+
log.debug(
454+
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
455+
+ f"attempted to delete a non-existent profile: {name}"
456+
)
457+
return await ctx.send(f"No profile named '{name}' exists.")
404458

405-
# Check if user is creator or has admin privileges
406-
if profile["creator"] != ctx.author.id and not await is_admin_or_superior(self.bot, ctx.author):
407-
log.warning(
408-
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
409-
+ f"attempted to delete profile {name} without the necessary permissions."
410-
)
411-
return await ctx.send(
412-
"You don't have permission to delete this profile. " + "Only the creator or admins can delete it."
413-
)
459+
# Check if user is creator or has admin privileges
460+
if profile.creator != ctx.author.id and not await is_admin_or_superior(self.bot, ctx.author):
461+
log.warning(
462+
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
463+
+ f"attempted to delete profile {profile.name} without the necessary permissions."
464+
)
465+
return await ctx.send(
466+
"You don't have permission to delete this profile. " + "Only the creator or admins can delete it."
467+
)
414468

415-
# Remove the profile from config
416-
del profiles[name]
469+
# Remove the profile from config
470+
async with self.config.guild(ctx.guild).profiles() as profiles:
471+
del profiles[profile.name]
417472

418473
log.info(
419474
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
420-
+ f"deleted the profile {name}"
475+
+ f"deleted the profile {profile.name}"
421476
)
422477

423-
await ctx.send(f"Guild profile '{name}' deleted successfully.")
478+
await ctx.send(f"Guild profile '{profile.name}' deleted successfully.")
424479

425480
@profile_cmd.command(name="apply")
426481
async def apply_profile_cmd(self, ctx: commands.GuildContext, name: str):
@@ -429,10 +484,9 @@ async def apply_profile_cmd(self, ctx: commands.GuildContext, name: str):
429484
430485
This will update the guild's icon and banner to match the profile.
431486
"""
432-
profiles = await self.config.guild(ctx.guild).profiles()
433-
name = name.lower()
434-
435-
if name not in profiles:
487+
try:
488+
profile = await self._get_profile(ctx.guild, name)
489+
except ValueError:
436490
log.debug(
437491
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
438492
+ f"attempted to apply a non-existent profile: {name}"
@@ -442,39 +496,40 @@ async def apply_profile_cmd(self, ctx: commands.GuildContext, name: str):
442496
# Show typing indicator while processing
443497
await ctx.typing()
444498

445-
profile = profiles[name]
446-
447499
# Get file paths
448500
try:
449-
icon_asset = await self._get_asset(ctx.guild, profile["icon_id"])
450-
banner_asset = await self._get_asset(ctx.guild, profile["banner_id"])
501+
icon_asset = await self._get_asset(ctx.guild, profile.icon_id)
502+
banner_asset = await self._get_asset(ctx.guild, profile.banner_id)
451503
except (ValueError, FileNotFoundError) as e:
452504
return await ctx.send(f"Failed to retrieve asset: {e!s}")
453505

454506
# Apply changes
455507
try:
456508
async with aio_open(icon_asset.path, "rb") as icon_file:
457-
icon_data = await icon_file.read()
509+
icon_bytes = await icon_file.read()
458510

459511
async with aio_open(banner_asset.path, "rb") as banner_file:
460-
banner_data = await banner_file.read()
512+
banner_bytes = await banner_file.read()
461513

462-
await ctx.guild.edit(icon=icon_data, banner=banner_data, reason=f"Guild profile '{name}' applied by {ctx.author}")
514+
await ctx.guild.edit(
515+
icon=icon_bytes, banner=banner_bytes, reason=f"Guild profile '{profile.name}' applied by {ctx.author}"
516+
)
463517
log.info(
464518
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
465-
+ f"applied profile '{name}' to the guild."
519+
+ f"applied profile '{profile.name}' to the guild."
466520
)
467-
await ctx.send(f"Guild profile '{name}' has been applied to the guild.")
521+
await ctx.send(f"Guild profile '{profile.name}' has been applied to the guild.")
468522
except discord.Forbidden:
469523
log.error(
470524
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
471-
+ f"attempted to apply profile '{name}' but I don't have permission to change the guild's icon and banner."
525+
+ f"attempted to apply profile '{profile.name}' but I don't have permission to change the guild's "
526+
+ "icon and banner."
472527
)
473528
await ctx.send("I don't have permission to change the guild's icon and banner.")
474529
except discord.HTTPException as e:
475530
log.error(
476531
f"User {ctx.author.global_name} ({ctx.author.id}) in guild {ctx.guild.name} ({ctx.guild.id}) "
477-
+ f"attempted to apply profile '{name}' but an HTTP error occurred: {e!s}"
532+
+ f"attempted to apply profile '{profile.name}' but an HTTP error occurred: {e!s}"
478533
)
479534
await ctx.send(f"An error occurred while updating the guild: {e!s}")
480535

0 commit comments

Comments
 (0)