import { Client, Partials, GatewayIntentBits, ActivityType } from "discord.js"; import { DISCORD_TOKEN, DISCORD_USER_ID, DISCORD_GUILD_ID } from "$env/static/private"; class Bot { private client: Client; // Normalize a discord.js Activity object into a simple POJO private async mapActivity(a: any) { const type = typeof a.type === 'number' ? a.type : (ActivityType[a.type] ?? a.type); const application_id = a.applicationId ?? a.application_id ?? null; const rawLarge = a.assets?.largeImage ?? a.assets?.large_image ?? null; const rawSmall = a.assets?.smallImage ?? a.assets?.small_image ?? null; const formatAsset = (raw: string | null) => { if (!raw) return null; if (raw.startsWith('mp:external/')) { return `https://media.discordapp.net/${raw.replace(/^mp:/, '')}`; } if (/^https?:\/\//.test(raw)) return raw; if (application_id) return `https://cdn.discordapp.com/app-assets/${application_id}/${raw}.webp`; return raw; }; let app_icon = null; if (type === 0 && application_id) { try { const appUser = await this.client.users.fetch(application_id); app_icon = appUser.displayAvatarURL({ extension: 'webp', size: 128 }); } catch (e) { // Placeholder for apps that don't have a bot user app_icon = "https://cdn.discordapp.com/embed/avatars/0.png"; } } return { name: a.name ?? null, type, details: a.details ?? a.state ?? null, url: a.url ?? null, application_id, assets: { large_image: formatAsset(rawLarge), small_image: formatAsset(rawSmall) }, app_icon, emoji: a.emoji ? { id: a.emoji.id ?? null, animated: !!a.emoji.animated } : null }; } constructor() { // Need presence and members intents to read user presence/activity this.client = new Client({ partials: [Partials.User], intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildPresences ] }); this.client.login(DISCORD_TOKEN).catch((error) => { console.error("Failed to login to Discord:", error); }); this.client.once("clientReady", async () => { console.log(`Logged in as ${this.client.user?.tag}`); }); } getClient(): Client { return this.client; } /** * Look up the user's presence specifically in the given guild. * Returns null if guild/member/presence not available. */ async getUserActivityInGuild(guildId: string = DISCORD_GUILD_ID, userId: string = DISCORD_USER_ID) { try { // Fetch guild from cache or API let guild = this.client.guilds.cache.get(guildId) || await this.client.guilds.fetch(guildId).catch(() => null); if (!guild) return null; // Fetch member (will populate presence if available) const member = await guild.members.fetch(userId).catch(() => null); if (!member) return null; const presence = member.presence; const user = member.user; // Build avatar URL const avatarHash = user.avatar; const avatarUrl = avatarHash ? `https://cdn.discordapp.com/avatars/${user.id}/${avatarHash}.${avatarHash.startsWith('a_') ? 'gif' : 'webp'}?size=128` : `https://cdn.discordapp.com/embed/avatars/${(BigInt(user.id) >> BigInt(22)) % BigInt(6)}.png`; // Get guild/clan tag if available const guildTag = (user as any).primaryGuild?.tag ?? null; const guildTagBadgeImage = `https://cdn.discordapp.com/clan-badges/${(user as any).primaryGuild?.identityGuildId}/${(user as any).primaryGuild?.badge}.png?size=32`; if (!presence) return { guildId, status: 'offline', activities: [], displayName: member.displayName, avatarUrl, guildTag, guildTagBadgeImage }; const status = presence.status; const activities = await Promise.all((presence.activities || []).map(async (a: any) => await this.mapActivity(a))); return { guildId, status, activities, displayName: member.displayName, avatarUrl, guildTag, guildTagBadgeImage }; } catch (err) { return null; } } } export const bot = new Bot(); // Re-export image helper for convenience export { fetchUserImages } from "./discord/fetchUserImages";