diff --git a/TODO.md b/TODO.md index c6afc5f..6ce745b 100644 --- a/TODO.md +++ b/TODO.md @@ -53,7 +53,7 @@ - [ ] add [tts](https://developer.puter.com/tutorials/free-unlimited-text-to-speech-api/) command -- [ ] update botinfo command +- [x] update botinfo command - [ ] Clean tag system from [tags branch](https://github.com/neoarz/Syntrel/tree/tags) (make sure db is persistent) diff --git a/bot.py b/bot.py index de37a47..bc29256 100644 --- a/bot.py +++ b/bot.py @@ -19,25 +19,6 @@ from utils import ascii, setup_logger, get_uptime, setup_signal_handlers load_dotenv() -""" -Default Intents: -intents.emojis_and_stickers = True -intents.guild_messages = True -intents.guild_reactions = True -intents.guild_scheduled_events = True -intents.guild_typing = True -intents.guilds = True -intents.integrations = True -intents.invites = True -intents.reactions = True -intents.typing = True -intents.voice_states = True -intents.webhooks = True - -intents.members = True -intents.presences = True -""" - intents = discord.Intents.default() intents.message_content = True intents.bans = True @@ -46,7 +27,20 @@ intents.dm_reactions = True intents.dm_typing = True intents.emojis = True intents.messages = True - +intents.reactions = True +intents.typing = True +intents.voice_states = True +intents.webhooks = True +intents.emojis_and_stickers = True +intents.guild_messages = True +intents.guild_reactions = True +intents.guild_scheduled_events = True +intents.guild_typing = True +intents.guilds = True +intents.integrations = True +intents.invites = True +intents.presences = True +intents.members = True diff --git a/cogs/general/__init__.py b/cogs/general/__init__.py index c82a93a..56826ff 100644 --- a/cogs/general/__init__.py +++ b/cogs/general/__init__.py @@ -1,4 +1,5 @@ import discord +from discord import app_commands from discord.ext import commands from discord.ext.commands import Context @@ -6,6 +7,7 @@ from .ping import ping_command from .uptime import uptime_command from .serverinfo import serverinfo_command from .feedback import feedback_command +from .userinfo import userinfo_command def _require_group_prefix(context: Context) -> bool: @@ -31,13 +33,13 @@ class General(commands.GroupCog, name="general"): color=0x7289DA ) embed.set_author(name="General", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") - embed.add_field(name="Available", value="ping, uptime, serverinfo, feedback", inline=False) + embed.add_field(name="Available", value="ping, uptime, serverinfo, userinfo, feedback", inline=False) await context.send(embed=embed) - async def _invoke_hybrid(self, context: Context, name: str): + async def _invoke_hybrid(self, context: Context, name: str, **kwargs): command = self.bot.get_command(name) if command is not None: - await context.invoke(command) + await context.invoke(command, **kwargs) else: await context.send(f"Unknown general command: {name}") @@ -53,6 +55,10 @@ class General(commands.GroupCog, name="general"): async def general_group_serverinfo(self, context: Context): await self._invoke_hybrid(context, "serverinfo") + @general_group.command(name="userinfo") + async def general_group_userinfo(self, context: Context, user: discord.User = None, user_id: str = None): + await self._invoke_hybrid(context, "userinfo", user=user, user_id=user_id) + @general_group.command(name="feedback") async def general_group_feedback(self, context: Context): await self._invoke_hybrid(context, "feedback") @@ -81,6 +87,18 @@ class General(commands.GroupCog, name="general"): async def serverinfo(self, context): return await serverinfo_command()(self, context) + @commands.check(_require_group_prefix) + @commands.hybrid_command( + name="userinfo", + description="Get information on a user.", + ) + @app_commands.describe( + user="User to get info for", + user_id="User ID to get info for" + ) + async def userinfo(self, context, user: discord.User = None, user_id: str = None): + return await userinfo_command()(self, context, user=user, user_id=user_id) + @commands.check(_require_group_prefix) @commands.hybrid_command( name="feedback", @@ -96,4 +114,5 @@ async def setup(bot) -> None: bot.logger.info("Loaded extension 'general.ping'") bot.logger.info("Loaded extension 'general.uptime'") bot.logger.info("Loaded extension 'general.serverinfo'") + bot.logger.info("Loaded extension 'general.userinfo'") bot.logger.info("Loaded extension 'general.feedback'") diff --git a/cogs/general/userinfo.py b/cogs/general/userinfo.py new file mode 100644 index 0000000..b4bfe73 --- /dev/null +++ b/cogs/general/userinfo.py @@ -0,0 +1,543 @@ +import discord +from discord.ext import commands +from discord import app_commands +import aiohttp +import asyncio +from io import BytesIO +from PIL import Image +import re +from datetime import datetime, timezone + +ONE_MONTH = 2628000 + +quests_fetch = 0 +quest_data = [] + +ACTIVITY_TYPE_NAMES = [ + "Playing", + "Streaming", + "Listening to", + "Watching", + "Custom Status", + "Competing in", + "Hang Status" +] + +USER_FLAGS = { + 'STAFF': 1 << 0, + 'PARTNER': 1 << 1, + 'HYPESQUAD': 1 << 2, + 'BUG_HUNTER_LEVEL_1': 1 << 3, + 'HYPESQUAD_ONLINE_HOUSE_1': 1 << 6, + 'HYPESQUAD_ONLINE_HOUSE_2': 1 << 7, + 'HYPESQUAD_ONLINE_HOUSE_3': 1 << 8, + 'PREMIUM_EARLY_SUPPORTER': 1 << 9, + 'BUG_HUNTER_LEVEL_2': 1 << 14, + 'VERIFIED_DEVELOPER': 1 << 17, + 'CERTIFIED_MODERATOR': 1 << 18, + 'ACTIVE_DEVELOPER': 1 << 22 +} + +APPLICATION_FLAGS = { + 'APPLICATION_COMMAND_BADGE': 1 << 23, + 'AUTO_MODERATION_RULE_CREATE_BADGE': 1 << 6 +} + +BADGE_URLS = { + 'staff': 'https://discord.com/company', + 'partner': 'https://discord.com/partners', + 'certified_moderator': 'https://discord.com/safety', + 'hypesquad': 'https://discord.com/hypesquad', + 'hypesquad_house_1': 'https://discord.com/settings/hypesquad-online', + 'hypesquad_house_2': 'https://discord.com/settings/hypesquad-online', + 'hypesquad_house_3': 'https://discord.com/settings/hypesquad-online', + 'bug_hunter_level_1': 'https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs', + 'bug_hunter_level_2': 'https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs', + 'active_developer': 'https://support-dev.discord.com/hc/en-us/articles/10113997751447?ref=badge', + 'early_supporter': 'https://discord.com/settings/premium', + 'premium': 'https://discord.com/settings/premium', + 'bot_commands': 'https://discord.com/blog/welcome-to-the-new-era-of-discord-apps?ref=badge', + 'quest_completed': 'https://discord.com/settings/inventory' +} + +BADGE_ICONS = { + 'staff': '<:discordstaff:1426051878155845702>', + 'partner': '<:discordpartner:1426051933608873986>', + 'certified_moderator': '<:discordmod:1426051921826943050>', + 'hypesquad': '<:hypesquadevents:1426051833536970852>', + 'hypesquad_house_1': '<:hypesquadbravery:1426051916739383297>', + 'hypesquad_house_2': '<:hypesquadbrilliance:1426051849433387068>', + 'hypesquad_house_3': '<:hypesquadbalance:1426051905179750495>', + 'bug_hunter_level_1': '<:discordbughunter1:1426052002193997895>', + 'bug_hunter_level_2': '<:discordbughunter2:1426052028257406987>', + 'active_developer': '<:activedeveloper:1426051981658685552>', + 'verified_developer': '<:discordbotdev:1426051827077480570>', + 'early_supporter': '<:discordearlysupporter:1426052023165517924>', + 'premium': '<:discordnitro:1426051911123206296>', + 'guild_booster_lvl1': '<:discordboost1:1426052007294144605>', + 'guild_booster_lvl2': '<:discordboost2:1426051986985582692>', + 'guild_booster_lvl3': '<:discordboost3:1426051991812964434>', + 'guild_booster_lvl4': '<:discordboost4:1426051955473645671>', + 'guild_booster_lvl5': '<:discordboost5:1426051960456609824>', + 'guild_booster_lvl6': '<:discordboost6:1426051976583712918>', + 'guild_booster_lvl7': '<:discordboost7:1426051965808410634>', + 'guild_booster_lvl8': '<:discordboost8:1426051844014342225>', + 'guild_booster_lvl9': '<:discordboost9:1426051855015743558>', + 'bot_commands': '<:supportscommands:1426051872476889171>', + 'automod': '<:automod:1426051939103146115>', + 'quest_completed': '<:quest:1426051817946611784>', + 'username': '<:username:1426051894371160115>', + 'premium_bot': '<:premiumbot:1426051888272638025>', + 'orb': '<:orb:1426051861605126289>', + 'bronze': '<:bronze:1426051866969772034>', + 'silver': '<:silver:1426051928575709286>', + 'gold': '<:gold:1426052012352737333>', + 'platinum': '<:platinum:1426052018040082545>', + 'diamond': '<:diamond:1426051944685895771>', + 'emerald': '<:emerald:1426051812313792537>', + 'ruby': '<:ruby:1426051838645637150>', + 'opal': '<:opal:1426051883247603762>' +} + +ACTIVITY_TYPE_ICONS = { + 0: '<:gaming:1426409065701048451>', + 2: '<:music:1426409047132737586>', + 3: '<:watching:1426409475450863778>' +} + +def format_username(user): + if user.discriminator and user.discriminator != '0': + return f'{user.name}#{user.discriminator}' + return f'@{user.name}' + +def get_default_avatar(user_id, discriminator=None): + if discriminator and int(discriminator) > 0: + index = int(discriminator) % 5 + else: + index = (int(user_id) >> 22) % 6 + return f'https://cdn.discordapp.com/embed/avatars/{index}.png' + +def snowflake_to_timestamp(snowflake): + return ((int(snowflake) >> 22) + 1420070400000) / 1000 + +async def fetch_quest_data(): + global quests_fetch, quest_data + + async with aiohttp.ClientSession() as session: + async with session.get( + 'https://raw.githubusercontent.com/aamiaa/discord-api-diff/refs/heads/main/quests.json' + ) as resp: + quest_data = await resp.json() + + quests_fetch = int(datetime.now().timestamp() * 1000) + 3600000 + +async def get_user_data(bot, user_id): + headers = {'Authorization': f'Bot {bot.http.token}'} + + async with aiohttp.ClientSession() as session: + async with session.get( + f'https://discord.com/api/v10/users/{user_id}', + headers=headers + ) as resp: + if resp.status == 404: + return None + return await resp.json() + +async def get_application_data(bot, app_id): + headers = {'Authorization': f'Bot {bot.http.token}'} + + async with aiohttp.ClientSession() as session: + async with session.get( + f'https://discord.com/api/v10/applications/{app_id}/rpc', + headers=headers + ) as resp: + if resp.status in [404, 403, 10002]: + return None + return await resp.json() + +async def get_published_listing(bot, sku_id): + headers = {'Authorization': f'Bot {bot.http.token}'} + + async with aiohttp.ClientSession() as session: + async with session.get( + f'https://discord.com/api/v10/store/published-listings/skus/{sku_id}', + headers=headers + ) as resp: + if resp.status != 200: + return None + return await resp.json() + +def userinfo_command(): + @commands.hybrid_command( + name="userinfo", + description="Get information on a user.", + ) + @app_commands.describe( + user="User to get info for", + user_id="User ID to get info for" + ) + async def userinfo(self, context, user: discord.User = None, user_id: str = None): + await context.defer() + + bot = self.bot + target_user = user if user else context.author + + if user_id: + try: + target_user = await bot.fetch_user(int(user_id)) + except: + await context.send('User not found.') + return + + user_data = await get_user_data(bot, target_user.id) + if not user_data: + await context.send('Failed to fetch user data.') + return + + guild = context.guild + member = guild.get_member(target_user.id) if guild else None + + if int(datetime.now().timestamp() * 1000) > quests_fetch: + try: + await fetch_quest_data() + except: + pass + + badges = [] + flags = user_data.get('public_flags', 0) + + if str(target_user.id) == '1015372540937502851': + badges.append(f"[{BADGE_ICONS['staff']}]({BADGE_URLS['staff']})") + badges.append(f"[{BADGE_ICONS['partner']}]({BADGE_URLS['partner']})") + badges.append(f"[{BADGE_ICONS['certified_moderator']}]({BADGE_URLS['certified_moderator']})") + badges.append(f"[{BADGE_ICONS['hypesquad_house_1']}]({BADGE_URLS['hypesquad_house_1']})") + badges.append(f"[{BADGE_ICONS['bug_hunter_level_2']}]({BADGE_URLS['bug_hunter_level_2']})") + badges.append(BADGE_ICONS['verified_developer']) + badges.append(f"[{BADGE_ICONS['early_supporter']}]({BADGE_URLS['early_supporter']})") + badges.append(f"[{BADGE_ICONS['guild_booster_lvl9']}]({BADGE_URLS['premium']})") + badges.append(f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})") + badges.append(BADGE_ICONS['username']) + elif str(target_user.id) == '1376728824108286034': + badges.append(BADGE_ICONS['automod']) + badges.append(BADGE_ICONS['verified_developer']) + badges.append(BADGE_ICONS['premium_bot']) + elif flags & USER_FLAGS['STAFF']: + badges.append(f"[{BADGE_ICONS['staff']}]({BADGE_URLS['staff']})") + if flags & USER_FLAGS['PARTNER']: + badges.append(f"[{BADGE_ICONS['partner']}]({BADGE_URLS['partner']})") + if flags & USER_FLAGS['CERTIFIED_MODERATOR']: + badges.append(f"[{BADGE_ICONS['certified_moderator']}]({BADGE_URLS['certified_moderator']})") + if flags & USER_FLAGS['HYPESQUAD']: + badges.append(f"[{BADGE_ICONS['hypesquad']}]({BADGE_URLS['hypesquad']})") + if flags & USER_FLAGS['HYPESQUAD_ONLINE_HOUSE_1']: + badges.append(f"[{BADGE_ICONS['hypesquad_house_1']}]({BADGE_URLS['hypesquad_house_1']})") + if flags & USER_FLAGS['HYPESQUAD_ONLINE_HOUSE_2']: + badges.append(f"[{BADGE_ICONS['hypesquad_house_2']}]({BADGE_URLS['hypesquad_house_2']})") + if flags & USER_FLAGS['HYPESQUAD_ONLINE_HOUSE_3']: + badges.append(f"[{BADGE_ICONS['hypesquad_house_3']}]({BADGE_URLS['hypesquad_house_3']})") + if flags & USER_FLAGS['BUG_HUNTER_LEVEL_1']: + badges.append(f"[{BADGE_ICONS['bug_hunter_level_1']}]({BADGE_URLS['bug_hunter_level_1']})") + if flags & USER_FLAGS['BUG_HUNTER_LEVEL_2']: + badges.append(f"[{BADGE_ICONS['bug_hunter_level_2']}]({BADGE_URLS['bug_hunter_level_2']})") + if flags & USER_FLAGS['ACTIVE_DEVELOPER']: + badges.append(f"[{BADGE_ICONS['active_developer']}]({BADGE_URLS['active_developer']})") + if flags & USER_FLAGS['VERIFIED_DEVELOPER']: + badges.append(BADGE_ICONS['verified_developer']) + if flags & USER_FLAGS['PREMIUM_EARLY_SUPPORTER']: + badges.append(f"[{BADGE_ICONS['early_supporter']}]({BADGE_URLS['early_supporter']})") + + avatar_hash = user_data.get('avatar', '') + banner_hash = user_data.get('banner') + if (banner_hash or (avatar_hash and avatar_hash.startswith('a_'))) and not user_data.get('bot'): + badges.append(f"[{BADGE_ICONS['premium']}]({BADGE_URLS['premium']})") + + if member and member.premium_since: + boosting_since = member.premium_since + delta = (datetime.now(timezone.utc) - boosting_since).total_seconds() + icon = BADGE_ICONS['guild_booster_lvl1'] + + if delta >= ONE_MONTH * 24: + icon = BADGE_ICONS['guild_booster_lvl9'] + elif delta >= ONE_MONTH * 18: + icon = BADGE_ICONS['guild_booster_lvl8'] + elif delta >= ONE_MONTH * 15: + icon = BADGE_ICONS['guild_booster_lvl7'] + elif delta >= ONE_MONTH * 12: + icon = BADGE_ICONS['guild_booster_lvl6'] + elif delta >= ONE_MONTH * 9: + icon = BADGE_ICONS['guild_booster_lvl5'] + elif delta >= ONE_MONTH * 6: + icon = BADGE_ICONS['guild_booster_lvl4'] + elif delta >= ONE_MONTH * 3: + icon = BADGE_ICONS['guild_booster_lvl3'] + elif delta >= ONE_MONTH * 2: + icon = BADGE_ICONS['guild_booster_lvl2'] + + badges.append(f"[{icon}]({BADGE_URLS['premium']})") + + bot_deleted = False + if user_data.get('bot'): + app_data = await get_application_data(bot, target_user.id) + if app_data: + app_flags = app_data.get('flags', 0) + if app_flags & APPLICATION_FLAGS['APPLICATION_COMMAND_BADGE']: + badges.append(f"[{BADGE_ICONS['bot_commands']}]({BADGE_URLS['bot_commands']})") + if app_flags & APPLICATION_FLAGS['AUTO_MODERATION_RULE_CREATE_BADGE']: + badges.append(BADGE_ICONS['automod']) + else: + bot_deleted = True + + if user_data.get('system'): + bot_deleted = False + + quest_decoration_name = None + avatar_decoration = user_data.get('avatar_decoration_data') + if avatar_decoration and avatar_decoration.get('sku_id'): + for quest in quest_data: + config = quest.get('config', {}) + rewards = config.get('rewards_config', {}).get('rewards', []) or config.get('rewards', []) + + for reward in rewards: + if reward.get('type') == 3 and reward.get('sku_id') == avatar_decoration['sku_id']: + quest_decoration_name = (reward.get('name') or reward.get('messages', {}).get('name') or '*Unknown*').replace('Avatar Decoration', 'Avatar Deco') + badges.append(f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})") + break + if quest_decoration_name: + break + elif avatar_decoration and (avatar_decoration.get('expires_at') or avatar_decoration.get('sku_id') == '1226939756617793606'): + badges.append(f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})") + + if user_data.get('legacy_username'): + badges.append(BADGE_ICONS['username']) + + if user_data.get('bot') and user_data.get('approximated_guild_count'): + badges.append(BADGE_ICONS['premium_bot']) + + profile_effect = user_data.get('profile_effect') + if profile_effect: + effect_id = profile_effect.get('id') + if effect_id: + orb_tier = None + + if '1139323098643333240' in effect_id: + orb_tier = 'opal' + elif '1139323095841308733' in effect_id: + orb_tier = 'ruby' + elif '1139323090842013756' in effect_id: + orb_tier = 'emerald' + elif '1139323087608832090' in effect_id: + orb_tier = 'diamond' + elif '1144286544523669516' in effect_id: + orb_tier = 'platinum' + elif '1139323084127289374' in effect_id: + orb_tier = 'gold' + elif '1139323078435717220' in effect_id: + orb_tier = 'silver' + elif '1139323075214307448' in effect_id: + orb_tier = 'bronze' + else: + orb_tier = 'orb' + + if orb_tier: + badges.append(BADGE_ICONS[orb_tier]) + + default_avatar = get_default_avatar(target_user.id, user_data.get('discriminator', '0')) + avatar_url = target_user.avatar.url if target_user.avatar else default_avatar + + banner_url = None + banner_file = None + original_banner_link = None + if banner_hash: + is_animated = banner_hash.startswith('a_') + ext = 'gif' if is_animated else 'png' + original_banner_url = f'https://cdn.discordapp.com/banners/{target_user.id}/{banner_hash}.{ext}?size=4096' + original_banner_link = original_banner_url + + try: + async with aiohttp.ClientSession() as session: + async with session.get(original_banner_url) as resp: + if resp.status == 200: + banner_data = await resp.read() + img = Image.open(BytesIO(banner_data)) + + if img.width < 1100: + target_width = 1100 + target_height = 440 + + img_aspect = img.width / img.height + target_aspect = target_width / target_height + + if img_aspect > target_aspect: + scale_height = target_height + scale_width = int(scale_height * img_aspect) + else: + scale_width = target_width + scale_height = int(scale_width / img_aspect) + + left = (scale_width - target_width) // 2 + top = (scale_height - target_height) // 2 + right = left + target_width + bottom = top + target_height + + if is_animated: + frames = [] + durations = [] + + try: + while True: + frame = img.copy().convert('RGBA') + frame = frame.resize((scale_width, scale_height), Image.Resampling.LANCZOS) + frame = frame.crop((left, top, right, bottom)) + frames.append(frame) + durations.append(img.info.get('duration', 100)) + img.seek(img.tell() + 1) + except EOFError: + pass + + output = BytesIO() + frames[0].save( + output, + format='GIF', + save_all=True, + append_images=frames[1:], + duration=durations, + loop=0, + optimize=False + ) + output.seek(0) + banner_file = discord.File(output, filename='banner.gif') + banner_url = 'attachment://banner.gif' + else: + img = img.resize((scale_width, scale_height), Image.Resampling.LANCZOS) + img = img.crop((left, top, right, bottom)) + + output = BytesIO() + img.save(output, format='PNG') + output.seek(0) + banner_file = discord.File(output, filename='banner.png') + banner_url = 'attachment://banner.png' + else: + banner_url = original_banner_url + except: + banner_url = original_banner_url + + images = [f'[Avatar]({avatar_url})'] + + if banner_url: + images.append(f'[Banner]({original_banner_link})') + + if avatar_decoration: + await get_published_listing(bot, avatar_decoration['sku_id']) + decoration_url = f"https://cdn.discordapp.com/avatar-decoration-presets/{avatar_decoration['asset']}.png?size=4096&passthrough=true" + images.append(f'[Avatar Deco]({decoration_url})') + + collectibles = user_data.get('collectibles') + if collectibles and collectibles.get('nameplate'): + nameplate = collectibles['nameplate'] + nameplate_asset = nameplate['asset'] + images.append(f'[Nameplate](https://cdn.discordapp.com/assets/collectibles/{nameplate_asset}static.png)') + + mutual_guilds = [g for g in bot.guilds if g.get_member(target_user.id)] + display_name = user_data.get('global_name') or user_data.get('username') + + desc_lines = [ + f"# {member.nick if member and member.nick else display_name}", + f"{format_username(target_user).replace('@', '')} • <@{target_user.id}>" + ] + + subline = "" + if badges: + subline += ''.join(badges) + + activity_lines = [] + if member and member.activities: + for activity in member.activities: + if activity.type == discord.ActivityType.custom: + activity_lines.append(ACTIVITY_TYPE_NAMES[4]) + elif activity.type in [discord.ActivityType.playing, discord.ActivityType.listening, discord.ActivityType.watching]: + name = activity.name + activity_lines.append( + f"{ACTIVITY_TYPE_ICONS.get(activity.type.value, '')} {ACTIVITY_TYPE_NAMES[activity.type.value]} **{name}**".strip() + ) + + if subline: + desc_lines.append(subline) + + if mutual_guilds: + desc_lines.append(f"-# {len(mutual_guilds)} Bot Mutual Server{'s' if len(mutual_guilds) > 1 else ''}") + else: + desc_lines.append('') + + is_system = user_data.get('system') or user_data.get('discriminator') == '0000' + + if bot_deleted and not is_system: + desc_lines.append("*This bot's application has been deleted*\n-# (or app ID and user ID desync)") + if is_system: + desc_lines.append('**System account**') + desc_lines.append('') + + if activity_lines: + desc_lines.extend(activity_lines) + + embed = discord.Embed( + color=0x7289DA, + description='\n'.join(desc_lines) + ) + + primary_guild = user_data.get('primary_guild') + if primary_guild and primary_guild.get('identity_guild_id'): + clan_badge_url = f"https://cdn.discordapp.com/clan-badges/{primary_guild['identity_guild_id']}/{primary_guild['badge']}.png?size=4096" + embed.set_author(name=primary_guild.get('tag', ''), icon_url=clan_badge_url) + else: + embed.set_author(name="User Information", icon_url="https://yes.nighty.works/raw/gSxqzV.png") + + if member and member.nick and member.nick != display_name: + embed.title = display_name + + embed.set_thumbnail(url=avatar_url) + + if banner_url: + embed.set_image(url=banner_url) + + created_timestamp = int(snowflake_to_timestamp(target_user.id)) + created_date = f"" + + embed.add_field(name='Created Date', value=created_date, inline=False) + + if member and hasattr(member, 'joined_at') and member.joined_at: + joined_timestamp = int(member.joined_at.timestamp()) + join_date = f"" + embed.add_field(name='Join Date', value=join_date, inline=False) + + is_bot = user_data.get('bot', False) + embed.add_field(name='Is Bot', value='True' if is_bot else 'False', inline=True) + + if member and member.roles[1:]: + roles = sorted(member.roles[1:], key=lambda r: r.position, reverse=True) + role_mentions = [f'<@&{r.id}>' for r in roles] + role_list = ' '.join(role_mentions) + + if len(role_list) > 1024: + truncated_roles = [] + current_length = 0 + for mention in role_mentions: + if current_length + len(mention) + 1 > 1020: + break + truncated_roles.append(mention) + current_length += len(mention) + 1 + role_list = ' '.join(truncated_roles) + ' ...' + + embed.add_field(name=f'Roles ({len(member.roles) - 1})', value=role_list, inline=False) + + if images: + embed.add_field(name='\u200b', value='\u3000'.join(images), inline=False) + + embed.set_footer(text=f'ID: {target_user.id}') + + if banner_file: + await context.send(embed=embed, file=banner_file) + else: + await context.send(embed=embed) + + return userinfo +