fix: Refactor userinfo command for improved member data handling and timezone awareness
This commit is contained in:
parent
0713e025ef
commit
d4b263d4c9
@ -1,7 +1,7 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import AllowedMentions, ui
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
class UserInfoCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
@ -14,14 +14,13 @@ class UserInfoCog(commands.Cog):
|
||||
member = ctx.author
|
||||
|
||||
# Fetch the member to ensure up-to-date data, especially for guild members
|
||||
# --- Information Gathering ---
|
||||
cached = ctx.guild.get_member(member.id) if ctx.guild else None
|
||||
member = cached or member # keep presence if we have it
|
||||
member = cached or member # keep presence if we have it
|
||||
|
||||
# only hit the API if we still need guild-specific data that might be stale
|
||||
if ctx.guild and member is not None and member.joined_at is None:
|
||||
try:
|
||||
member = await ctx.guild.fetch_member(member.id) # roles/nick/etc.
|
||||
member = await ctx.guild.fetch_member(member.id) # roles/nick/etc.
|
||||
except discord.NotFound:
|
||||
await ctx.send("Could not find the specified member in this server.", ephemeral=True)
|
||||
return
|
||||
@ -29,64 +28,98 @@ class UserInfoCog(commands.Cog):
|
||||
await ctx.send(f"An error occurred while fetching member data: `{e}`", ephemeral=True)
|
||||
return
|
||||
|
||||
username_discriminator = f"{member.name}#{member.discriminator}" if member.discriminator != "0" else member.name
|
||||
username_discriminator = (
|
||||
f"{member.name}#{member.discriminator}"
|
||||
if member.discriminator != "0"
|
||||
else member.name
|
||||
)
|
||||
created_at_str = member.created_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
joined_at_str = member.joined_at.strftime("%Y-%m-%d %H:%M:%S UTC") if member.joined_at else "N/A"
|
||||
|
||||
joined_at_str = (
|
||||
member.joined_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
if member.joined_at
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
roles = [role.mention for role in reversed(member.roles) if role.name != "@everyone"]
|
||||
roles_str = ", ".join(roles) if roles else "None"
|
||||
if len(roles_str) > 1000: # Discord limits field values
|
||||
if len(roles_str) > 1000: # Discord limits field values
|
||||
roles_str = roles_str[:997] + "..."
|
||||
|
||||
status_str = str(member.status).title()
|
||||
activity_str = (
|
||||
f"Playing {member.activity.name}" if member.activity and member.activity.type is discord.ActivityType.playing else
|
||||
f"Streaming {member.activity.name}" if member.activity and member.activity.type is discord.ActivityType.streaming else
|
||||
f"Listening to {member.activity.title}…" if member.activity and member.activity.type is discord.ActivityType.listening else
|
||||
f"Watching {member.activity.name}" if member.activity and member.activity.type is discord.ActivityType.watching else
|
||||
f"{member.activity.emoji} {member.activity.state}".strip() # Use .state for custom status text
|
||||
if member.activity and member.activity.type is discord.ActivityType.custom else
|
||||
"None"
|
||||
f"Playing {member.activity.name}"
|
||||
if member.activity
|
||||
and member.activity.type is discord.ActivityType.playing
|
||||
else f"Streaming {member.activity.name}"
|
||||
if member.activity
|
||||
and member.activity.type is discord.ActivityType.streaming
|
||||
else f"Listening to {member.activity.title}…"
|
||||
if member.activity
|
||||
and member.activity.type is discord.ActivityType.listening
|
||||
else f"Watching {member.activity.name}"
|
||||
if member.activity
|
||||
and member.activity.type is discord.ActivityType.watching
|
||||
else f"{member.activity.emoji} {member.activity.state}".strip()
|
||||
if member.activity
|
||||
and member.activity.type is discord.ActivityType.custom
|
||||
else "None"
|
||||
)
|
||||
|
||||
# Badges / Flags
|
||||
flags = member.public_flags # this is a PublicUserFlags instance
|
||||
badges = [
|
||||
name.replace('_', ' ').title()
|
||||
name.replace("_", " ").title()
|
||||
for name, enabled in flags
|
||||
if enabled
|
||||
]
|
||||
badges_str = ', '.join(badges) or "None"
|
||||
badges_str = ", ".join(badges) or "None"
|
||||
|
||||
# Pronouns
|
||||
pronouns_str = getattr(member, 'pronouns', 'N/A') # API v10-beta
|
||||
pronouns_str = getattr(member, "pronouns", "N/A") # API v10-beta
|
||||
|
||||
# Avatar Type
|
||||
avatar_type = "GIF" if member.avatar and member.avatar.is_animated() else "Static"
|
||||
|
||||
# --- FIXED: use aware UTC datetime for “now” ---
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
# Account Age
|
||||
account_age = datetime.utcnow() - member.created_at
|
||||
account_age = now_utc - member.created_at
|
||||
account_age_str = f"{account_age.days // 365} years, {(account_age.days % 365) // 30} months"
|
||||
|
||||
# Join Position
|
||||
join_position_str = "N/A"
|
||||
if ctx.guild and member.joined_at:
|
||||
sorted_members = sorted(ctx.guild.members, key=lambda m: m.joined_at if m.joined_at else datetime.min)
|
||||
sorted_members = sorted(
|
||||
ctx.guild.members,
|
||||
key=lambda m: m.joined_at
|
||||
if m.joined_at
|
||||
else datetime.min.replace(tzinfo=timezone.utc),
|
||||
)
|
||||
try:
|
||||
join_position_str = f"{sorted_members.index(member) + 1} of {len(sorted_members)}"
|
||||
except ValueError:
|
||||
pass # Member not found in sorted list, should not happen if member is from ctx.guild.members
|
||||
pass # Member not found in sorted list (should not happen)
|
||||
|
||||
# Server Boost Info
|
||||
boost_str = "Not boosting"
|
||||
if member.premium_since:
|
||||
months = (datetime.utcnow() - member.premium_since).days // 30
|
||||
boost_str = f"Boosting for {months} month{'s' if months!=1 else ''}"
|
||||
elif ctx.guild and discord.utils.get(member.roles, id=ctx.guild.premium_subscriber_role.id) in member.roles: # Check if they have the boost role
|
||||
# calculate months of boosting
|
||||
months = (now_utc - member.premium_since).days // 30
|
||||
boost_str = f"Boosting for {months} month{'s' if months != 1 else ''}"
|
||||
elif (
|
||||
ctx.guild
|
||||
and discord.utils.get(member.roles, id=ctx.guild.premium_subscriber_role.id)
|
||||
in member.roles
|
||||
):
|
||||
boost_str = "Boosting (time unknown)"
|
||||
|
||||
# Top / Hoisted Role
|
||||
top_role_str = member.top_role.mention if member.top_role and member.top_role.name != "@everyone" else "None"
|
||||
top_role_str = (
|
||||
member.top_role.mention
|
||||
if member.top_role and member.top_role.name != "@everyone"
|
||||
else "None"
|
||||
)
|
||||
|
||||
# Key Permissions
|
||||
key_permissions = []
|
||||
@ -109,8 +142,8 @@ class UserInfoCog(commands.Cog):
|
||||
# Timeout Status
|
||||
timeout_str = "Not timed out"
|
||||
if member.communication_disabled_until:
|
||||
until_time = member.communication_disabled_until
|
||||
time_left = until_time - datetime.utcnow()
|
||||
until_time = member.communication_disabled_until # this is tz-aware
|
||||
time_left = until_time - now_utc
|
||||
if time_left > timedelta(0):
|
||||
hours, remainder = divmod(int(time_left.total_seconds()), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
@ -122,8 +155,8 @@ class UserInfoCog(commands.Cog):
|
||||
device_map = {
|
||||
discord.Status.online: "🟢",
|
||||
discord.Status.idle: "🌙",
|
||||
discord.Status.dnd: "⛔",
|
||||
discord.Status.offline: "⚫"
|
||||
discord.Status.dnd: "⛔",
|
||||
discord.Status.offline: "⚫",
|
||||
}
|
||||
devices = []
|
||||
if member.desktop_status != discord.Status.offline:
|
||||
@ -134,86 +167,143 @@ class UserInfoCog(commands.Cog):
|
||||
devices.append(f"Web {device_map.get(member.web_status, '⚫')}")
|
||||
device_status_str = ", ".join(devices) or "Offline"
|
||||
|
||||
|
||||
# --- UI Components v2 View ---
|
||||
class UserInfoView(ui.LayoutView):
|
||||
def __init__(self, target_member: discord.Member):
|
||||
super().__init__(timeout=180) # 3 minutes timeout
|
||||
super().__init__(timeout=180) # 3 minutes timeout
|
||||
|
||||
main_container = ui.Container(accent_colour=target_member.accent_color or discord.Color.default())
|
||||
main_container = ui.Container(
|
||||
accent_colour=target_member.accent_color or discord.Color.default()
|
||||
)
|
||||
self.add_item(main_container)
|
||||
|
||||
# Header Section with Avatar and Banner
|
||||
header_section = ui.Section(accessory=ui.Thumbnail(media=target_member.display_avatar.url, description="User Avatar"))
|
||||
header_section = ui.Section(
|
||||
accessory=ui.Thumbnail(
|
||||
media=target_member.display_avatar.url,
|
||||
description="User Avatar",
|
||||
)
|
||||
)
|
||||
main_container.add_item(header_section)
|
||||
header_section.add_item(ui.TextDisplay(f"**{target_member.display_name}**"))
|
||||
header_section.add_item(ui.TextDisplay(f"({username_discriminator}) - ID: {target_member.id}"))
|
||||
|
||||
header_section.add_item(
|
||||
ui.TextDisplay(f"({username_discriminator}) - ID: {target_member.id}")
|
||||
)
|
||||
|
||||
if target_member.banner:
|
||||
main_container.add_item(ui.Thumbnail(media=target_member.banner.url, description="User Banner"))
|
||||
main_container.add_item(
|
||||
ui.Thumbnail(
|
||||
media=target_member.banner.url, description="User Banner"
|
||||
)
|
||||
)
|
||||
|
||||
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
|
||||
# Account & Profile
|
||||
main_container.add_item(ui.TextDisplay(f"**Account Created:** {created_at_str} ({account_age_str} ago)"))
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Account Created:** {created_at_str} ({account_age_str} ago)")
|
||||
)
|
||||
main_container.add_item(ui.TextDisplay(f"**Avatar Type:** {avatar_type}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Badges:** {badges_str}"))
|
||||
if pronouns_str != 'N/A':
|
||||
main_container.add_item(ui.TextDisplay(f"**Pronouns:** {pronouns_str}"))
|
||||
if pronouns_str != "N/A":
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Pronouns:** {pronouns_str}")
|
||||
)
|
||||
|
||||
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
|
||||
# Guild-specific
|
||||
if ctx.guild:
|
||||
main_container.add_item(ui.TextDisplay(f"**Joined Server:** {joined_at_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Join Position:** {join_position_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Server Boost:** {boost_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Top Role:** {top_role_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Key Permissions:** {permissions_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Timeout Status:** {timeout_str}"))
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Joined Server:** {joined_at_str}")
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Join Position:** {join_position_str}")
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Server Boost:** {boost_str}")
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Top Role:** {top_role_str}")
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Key Permissions:** {permissions_str}")
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Timeout Status:** {timeout_str}")
|
||||
)
|
||||
if target_member.nick:
|
||||
main_container.add_item(ui.TextDisplay(f"**Nickname:** {target_member.nick}"))
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Nickname:** {target_member.nick}")
|
||||
)
|
||||
|
||||
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
main_container.add_item(
|
||||
ui.Separator(spacing=discord.SeparatorSpacing.small)
|
||||
)
|
||||
|
||||
# Status & Activity
|
||||
main_container.add_item(ui.TextDisplay(f"**Status:** {status_str}"))
|
||||
main_container.add_item(ui.TextDisplay(f"**Device Status:** {device_status_str}"))
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Device Status:** {device_status_str}")
|
||||
)
|
||||
main_container.add_item(ui.TextDisplay(f"**Activity:** {activity_str}"))
|
||||
|
||||
|
||||
# Roles
|
||||
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
main_container.add_item(
|
||||
ui.Separator(spacing=discord.SeparatorSpacing.small)
|
||||
)
|
||||
main_container.add_item(ui.TextDisplay(f"**Roles ({len(roles)}):**"))
|
||||
if roles:
|
||||
main_container.add_item(ui.TextDisplay(roles_str))
|
||||
else:
|
||||
main_container.add_item(ui.TextDisplay("None"))
|
||||
|
||||
|
||||
# Voice State
|
||||
if target_member.voice:
|
||||
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
main_container.add_item(ui.TextDisplay(f"**Voice Channel:** {target_member.voice.channel.mention if target_member.voice.channel else 'Not in a channel'}"))
|
||||
main_container.add_item(
|
||||
ui.Separator(spacing=discord.SeparatorSpacing.small)
|
||||
)
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(
|
||||
f"**Voice Channel:** {target_member.voice.channel.mention if target_member.voice.channel else 'Not in a channel'}"
|
||||
)
|
||||
)
|
||||
voice_state_details = []
|
||||
if target_member.voice.self_mute: voice_state_details.append("Muted (Self)")
|
||||
if target_member.voice.self_deaf: voice_state_details.append("Deafened (Self)")
|
||||
if target_member.voice.mute: voice_state_details.append("Muted (Server)")
|
||||
if target_member.voice.deaf: voice_state_details.append("Deafened (Server)")
|
||||
if target_member.voice.self_stream: voice_state_details.append("Streaming")
|
||||
if target_member.voice.self_video: voice_state_details.append("Video On")
|
||||
if target_member.voice.self_mute:
|
||||
voice_state_details.append("Muted (Self)")
|
||||
if target_member.voice.self_deaf:
|
||||
voice_state_details.append("Deafened (Self)")
|
||||
if target_member.voice.mute:
|
||||
voice_state_details.append("Muted (Server)")
|
||||
if target_member.voice.deaf:
|
||||
voice_state_details.append("Deafened (Server)")
|
||||
if target_member.voice.self_stream:
|
||||
voice_state_details.append("Streaming")
|
||||
if target_member.voice.self_video:
|
||||
voice_state_details.append("Video On")
|
||||
if voice_state_details:
|
||||
main_container.add_item(ui.TextDisplay(f"**Voice State:** {', '.join(voice_state_details)}"))
|
||||
main_container.add_item(
|
||||
ui.TextDisplay(f"**Voice State:** {', '.join(voice_state_details)}")
|
||||
)
|
||||
|
||||
try:
|
||||
view = UserInfoView(member)
|
||||
await ctx.send(view=view, ephemeral=False, allowed_mentions=AllowedMentions(roles=False, users=False, everyone=False)) # Send publicly by default
|
||||
await ctx.send(
|
||||
view=view,
|
||||
ephemeral=False,
|
||||
allowed_mentions=AllowedMentions(roles=False, users=False, everyone=False),
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc() # Print full traceback to console
|
||||
await ctx.send(f"An error occurred while creating the user info display: `{e}`", ephemeral=True)
|
||||
|
||||
traceback.print_exc()
|
||||
await ctx.send(
|
||||
f"An error occurred while creating the user info display: `{e}`", ephemeral=True
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
print(f'{self.__class__.__name__} cog has been loaded.')
|
||||
print(f"{self.__class__.__name__} cog has been loaded.")
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(UserInfoCog(bot))
|
||||
|
Loading…
x
Reference in New Issue
Block a user