first commit

This commit is contained in:
yhqe committed 2026-06-28 11:30:57 -05:00
commit d94c5c6cc0
134 files changed
+23720

No files matched your search

+249
View File
@@ -0,0 +1,249 @@
// =============================================
// DISCORD ROLE EMOJI ADDER + ROLE ICON SETTER
// =============================================
// !addicons — fetches skin icons and uploads as server emojis
// !setroleicons — applies uploaded emojis as role icons
// !removeoldemojis — removes all skin emojis so you can re-add fresh
//
// Run with: node "add-icons.js"
// =============================================
require('dotenv').config();
const { Client, GatewayIntentBits, PermissionsBitField } = require('discord.js');
const sharp = require('sharp');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Direct cosmetic IDs — all verified to return valid icons from fortnite-api.com
// overrideUrl = use a specific variant image instead of the default icon
const roleIconMap = [
{ name: 'Fully Golden Peely', id: 'CID_701_Athena_Commando_M_BananaAgent', emoji: 'FullyGoldenPeely',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_701_athena_commando_m_bananaagent/variants/progressive/stage4.png' },
{ name: 'OG Aerial Assault Trooper', id: 'CID_017_Athena_Commando_M', emoji: 'AerialAssaultTrooper' },
{ name: 'OG Ghoul Trooper', id: 'CID_029_Athena_Commando_F_Halloween', emoji: 'GhoulTrooper',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_029_athena_commando_f_halloween/variants/material/mat3.png' },
{ name: 'OG Renegade Raider', id: 'CID_028_Athena_Commando_F', emoji: 'RenegadeRaider' },
{ name: 'OG Skull Trooper', id: 'CID_030_Athena_Commando_M_Halloween', emoji: 'SkullTrooper',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_030_athena_commando_m_halloween/variants/clothingcolor/mat1.png' },
{ name: 'Barricade', id: 'Character_RankedPhantom', emoji: 'Barricade' },
{ name: 'Double Helix', id: 'CID_183_Athena_Commando_M_ModernMilitaryRed', emoji: 'DoubleHelix' },
{ name: 'Honor Guard', id: 'CID_342_Athena_Commando_M_StreetRacerMetallic', emoji: 'HonorGuard' },
{ name: 'Wonder', id: 'CID_434_Athena_Commando_F_StealthHonor', emoji: 'Wonder' },
{ name: 'Surf Strider', id: 'CID_783_Athena_Commando_M_AquaJacket', emoji: 'SurfStrider' },
{ name: 'Stealth Reflex', id: 'CID_386_Athena_Commando_M_StreetOpsStealth', emoji: 'StealthReflex' },
{ name: 'Dark Vertex', id: 'CID_371_Athena_Commando_M_SpeedyMidnight', emoji: 'DarkVertex' },
{ name: 'Royale Bomber', id: 'CID_113_Athena_Commando_M_BlueAce', emoji: 'RoyaleBomber' },
{ name: 'Wildcat', id: 'CID_757_Athena_Commando_F_WildCat', emoji: 'Wildcat' },
{ name: 'Clyde', id: 'Character_UnbrokenTrash', emoji: 'Clyde' },
{ name: 'Eon', id: 'CID_174_Athena_Commando_F_CarbideWhite', emoji: 'Eon' },
{ name: 'Huntmaster Saber', id: 'Character_MasterKeyOrder', emoji: 'HuntmasterSaber' },
{ name: 'Dark Skully', id: 'CID_850_Athena_Commando_F_SkullBriteCube', emoji: 'DarkSkully' },
{ name: 'Galaxy', id: 'CID_175_Athena_Commando_M_Celestial', emoji: 'Galaxy' },
{ name: 'Ikonik', id: 'CID_313_Athena_Commando_M_KpopFashion', emoji: 'Ikonik' },
{ name: 'Black Knight', id: 'CID_035_Athena_Commando_M_Medieval', emoji: 'BlackKnight' },
{ name: 'Glow', id: 'CID_479_Athena_Commando_F_Davinci', emoji: 'Glow' },
{ name: 'Rogue Spider Knight', id: 'CID_516_Athena_Commando_M_BlackWidowRogue', emoji: 'RogueSpiderKnight' },
{ name: 'Thrilldiver', id: 'Character_SummerUnsuitable', emoji: 'Thrilldiver' },
{ name: 'Freediver', id: 'Character_CoolSuitable', emoji: 'Freediver' },
{ name: 'Neo Versa', id: 'CID_441_Athena_Commando_F_CyberScavengerBlue', emoji: 'NeoVersa' },
{ name: 'Cobalt Snowfoot', id: 'Character_SnowNinjaDark', emoji: 'CobaltSnowfoot' },
{ name: 'Florin', id: 'Character_AsterOrder', emoji: 'Florin' },
{ name: "What's Wrong, Toona Fish?", id: 'CID_A_201_Athena_Commando_M_TeriyakiFishToon', emoji: 'ToonaFish' },
{ name: 'Cameo Needs Loot', id: 'Emoji_S20_NeedLoot', emoji: 'Cameo',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s20_needloot/smallicon.png' },
];
// ── fetch with timeout so nothing ever hangs ──────────────────────────────────
async function fetchWithTimeout(url, ms = 12000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try {
return await fetch(url, { signal: ctrl.signal });
} finally {
clearTimeout(timer);
}
}
// ── get icon URL from fortnite-api.com by direct ID ───────────────────────────
async function fetchIconUrl(cosmeticId) {
const url = `https://fortnite-api.com/v2/cosmetics/br/${cosmeticId}`;
const res = await fetchWithTimeout(url);
if (!res.ok) throw new Error(`API returned ${res.status} for ${cosmeticId}`);
const json = await res.json();
if (!json.data?.images?.icon) throw new Error(`No icon in response for ${cosmeticId}`);
return json.data.images.icon;
}
// ── download image and upscale to 256x256 for crisp Discord emoji ─────────────
async function downloadAndProcess(imageUrl) {
const res = await fetchWithTimeout(imageUrl, 15000);
if (!res.ok) throw new Error(`Image download failed: ${res.status}`);
const raw = Buffer.from(await res.arrayBuffer());
// Upscale to 256x256 with Lanczos resampling + sharpening
return await sharp(raw)
.resize(256, 256, { fit: 'cover', position: 'centre', kernel: 'lanczos3' })
.sharpen({ sigma: 0.8, m1: 1.0, m2: 2.0 })
.png({ compressionLevel: 6 })
.toBuffer();
}
// ── create emoji with automatic rate-limit retry ──────────────────────────────
async function createEmojiSafe(guild, buffer, name, reason) {
for (let attempt = 1; attempt <= 6; attempt++) {
try {
return await guild.emojis.create({ attachment: buffer, name, reason });
} catch (err) {
const isRateLimit = err.status === 429 || err.httpStatus === 429 ||
(err.message || '').toLowerCase().includes('rate limit');
if (isRateLimit) {
const wait = ((err.retryAfter || 5) * 1000) + 1000;
console.warn(` ⏳ Rate limited on "${name}", waiting ${wait}ms (attempt ${attempt}/6)...`);
await new Promise(r => setTimeout(r, wait));
} else {
throw err;
}
}
}
throw new Error(`Gave up creating emoji "${name}" after 6 attempts`);
}
// ─────────────────────────────────────────────────────────────────────────────
client.once('ready', () => {
console.log(`✅ Logged in as ${client.user.tag}`);
console.log('Commands: !addicons | !setroleicons | !removeoldemojis');
});
// ── !removeoldemojis ──────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!removeoldemojis') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
await message.reply('Removing old skin emojis...');
const names = new Set(roleIconMap.map(e => e.emoji.toLowerCase()));
let removed = 0;
// Fetch fresh emoji list
await message.guild.emojis.fetch();
for (const emoji of message.guild.emojis.cache.values()) {
if (!names.has(emoji.name.toLowerCase())) continue;
try {
await emoji.delete('Replacing with corrected icons');
console.log(` 🗑 Deleted: ${emoji.name}`);
removed++;
await new Promise(r => setTimeout(r, 400));
} catch (err) {
console.error(` ❌ Failed to delete ${emoji.name}: ${err.message}`);
}
}
message.channel.send(`Removed **${removed}** emojis. Now run \`!addicons\`.`);
});
// ── !addicons ─────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!addicons') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
await message.reply(`Starting — uploading ${roleIconMap.length} skin icons. This takes a few minutes, watch the terminal for progress.`);
// Always fetch a fresh copy of existing emojis
await message.guild.emojis.fetch();
const existingNames = new Set(
message.guild.emojis.cache.map(e => e.name.toLowerCase())
);
let added = 0, skipped = 0, failed = 0;
for (const entry of roleIconMap) {
if (existingNames.has(entry.emoji.toLowerCase())) {
console.log(` ⏭ Already exists: ${entry.emoji}`);
skipped++;
continue;
}
process.stdout.write(` 📥 ${entry.name} — fetching... `);
try {
const iconUrl = entry.overrideUrl || await fetchIconUrl(entry.id);
process.stdout.write(`downloading... `);
const buffer = await downloadAndProcess(iconUrl);
process.stdout.write(`uploading... `);
await createEmojiSafe(message.guild, buffer, entry.emoji, `Skin icon: ${entry.name}`);
console.log(`✅`);
added++;
} catch (err) {
console.log(`❌ ${err.message}`);
failed++;
}
// 2s gap between uploads to stay under Discord rate limits
await new Promise(r => setTimeout(r, 2000));
}
message.channel.send(
`Done! ✅ Added **${added}** emojis. ` +
`⏭ Skipped **${skipped}** (already existed). ` +
`❌ **${failed}** failed.`
);
});
// ── !setroleicons ─────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!setroleicons') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
await message.reply('Applying emojis to roles...');
// Refresh both caches
await message.guild.emojis.fetch();
await message.guild.roles.fetch();
let updated = 0, skipped = 0, failed = 0;
for (const entry of roleIconMap) {
const role = message.guild.roles.cache.find(
r => r.name.toLowerCase() === entry.name.toLowerCase()
);
if (!role) {
console.log(` ⏭ Role not found: ${entry.name}`);
skipped++;
continue;
}
const emoji = message.guild.emojis.cache.find(
e => e.name.toLowerCase() === entry.emoji.toLowerCase()
);
if (!emoji) {
console.log(` ⚠ Emoji not found for: ${entry.name} — run !addicons first`);
skipped++;
continue;
}
try {
await role.setIcon(emoji);
console.log(` ✅ ${entry.name} → :${entry.emoji}:`);
updated++;
} catch (err) {
console.error(` ❌ Failed "${entry.name}": ${err.message}`);
failed++;
}
await new Promise(r => setTimeout(r, 500));
}
message.channel.send(
`Done! ✅ Set icons on **${updated}** roles. ` +
`⏭ Skipped **${skipped}**. ❌ **${failed}** failed.`
);
});
client.login(process.env.TOKEN);
+2029
View File
File diff suppressed because it is too large. Load diff
+402
View File
@@ -0,0 +1,402 @@
// =============================================
// FORTNITE SKIN VERIFY BOT
// =============================================
// Verifies a user's Fortnite locker via Epic Games
// OAuth and auto-assigns Discord roles based on
// the skins and styles they own.
//
// Run with: node verify.js
// Users type: !verify in your server
// =============================================
require('dotenv').config();
const { Client, GatewayIntentBits, PermissionsBitField, EmbedBuilder } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: ['CHANNEL'],
});
// ─── Epic Games API tokens (from RiftCheckerBot source) ───────────────────────
const SWITCH_TOKEN = 'OThmN2U0MmMyZTNhNGY4NmE3NGViNDNmYmI0MWVkMzk6MGEyNDQ5YTItMDAxYS00NTFlLWFmZWMtM2U4MTI5MDFjNGQ3';
const IOS_TOKEN = 'M2Y2OWU1NmM3NjQ5NDkyYzhjYzI5ZjFhZjA4YThhMTI6YjUxZWU5Y2IxMjIzNGY1MGE2OWVmYTY3ZWY1MzgxMmU=';
const EPIC_OAUTH = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token';
const EPIC_OAUTH_PROD03 = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token';
const EPIC_EXCHANGE = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange';
const EPIC_DEVICE_AUTH = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/deviceAuthorization';
// ─── Role → skin/style mapping ────────────────────────────────────────────────
// Each entry defines what cosmetic ID (and optionally style tag) a user must
// own to receive the role. styleTag means they need that specific variant unlocked.
const ROLE_REQUIREMENTS = [
// OG Season 1 skins — style required for the OG variant
{
role: 'OG Renegade Raider',
id: 'cid_028_athena_commando_f',
styleTag: 'Mat3', // Black & Gold style = OG
anyStyle: false,
},
{
role: 'OG Aerial Assault Trooper',
id: 'cid_017_athena_commando_m',
styleTag: 'Stage2', // upgraded style = OG unlock
anyStyle: false,
},
{
role: 'OG Ghoul Trooper',
id: 'cid_029_athena_commando_f_halloween',
styleTag: 'Mat3', // PINK Ghoul = OG
anyStyle: false,
},
{
role: 'OG Skull Trooper',
id: 'cid_030_athena_commando_m_halloween',
styleTag: 'Mat1', // PURPLE GLOW = OG
anyStyle: false,
},
// Exclusive skins — just owning them is enough
{ role: 'Black Knight', id: 'cid_035_athena_commando_m_medieval' },
{ role: 'Double Helix', id: 'cid_183_athena_commando_m_modernmilitaryred' },
{ role: 'Honor Guard', id: 'cid_342_athena_commando_m_streetracermetallic' },
{ role: 'Wonder', id: 'cid_434_athena_commando_f_stealthhonor' },
{ role: 'Surf Strider', id: 'cid_783_athena_commando_m_aquajacket' },
{ role: 'Stealth Reflex', id: 'cid_386_athena_commando_m_streetopsstealth' },
{ role: 'Dark Vertex', id: 'cid_371_athena_commando_m_speedymidnight' },
{ role: 'Royale Bomber', id: 'cid_113_athena_commando_m_blueace' },
{ role: 'Wildcat', id: 'cid_757_athena_commando_f_wildcat' },
{ role: 'Clyde', id: 'character_unbrokentrash' },
{ role: 'Eon', id: 'cid_174_athena_commando_f_carbidewhite' },
{ role: 'Huntmaster Saber', id: 'character_masterkeyorder' },
{ role: 'Dark Skully', id: 'cid_850_athena_commando_f_skullbritecube' },
{ role: 'Galaxy', id: 'cid_175_athena_commando_m_celestial' },
{ role: 'Ikonik', id: 'cid_313_athena_commando_m_kpopfashion' },
{ role: 'Glow', id: 'cid_479_athena_commando_f_davinci' },
{ role: 'Rogue Spider Knight', id: 'cid_516_athena_commando_m_blackwidowrogue' },
{ role: 'Thrilldiver', id: 'character_summerunsuitable' },
{ role: 'Freediver', id: 'character_coolsuitable' },
{ role: 'Neo Versa', id: 'cid_441_athena_commando_f_cyberscavengerblue' },
{ role: 'Cobalt Snowfoot', id: 'character_snowninjadark' },
{ role: 'Florin', id: 'character_asterorder' },
{ role: "What's Wrong, Toona Fish?", id: 'cid_a_201_athena_commando_m_teriyakifishtoon' },
{ role: 'Cameo Needs Loot', id: 'cid_571_athena_commando_f_sheath' },
{ role: 'Barricade', id: 'character_rankedphantom' },
// Fully Golden Peely = base Peely skin (golden style is a level-up variant)
{ role: 'Fully Golden Peely', id: 'cid_349_athena_commando_m_banana' },
];
// ─── STW Founders check (from common_core profile) ────────────────────────────
// founder packs give specific items in common_core
const STW_FOUNDER_IDS = [
'cid_095_athena_commando_m_founder', // Standard Founder
'cid_096_athena_commando_f_founder', // Standard Founder female
];
const STW_ULTIMATE_IDS = [
'tiertoken_collectionbook_ultimate', // Ultimate founder token
];
// ─── pending verifications: Map<discordUserId, { messageChannelId, guildId }> ──
const pending = new Map();
// ─── Helper: get switch token access token ────────────────────────────────────
async function getSwitchAccessToken() {
const res = await fetch(EPIC_OAUTH, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `basic ${SWITCH_TOKEN}`,
},
body: 'grant_type=client_credentials',
});
const data = await res.json();
return data.access_token;
}
// ─── Helper: create device code ───────────────────────────────────────────────
async function createDeviceCode(accessToken) {
const res = await fetch(EPIC_DEVICE_AUTH, {
method: 'POST',
headers: {
'Authorization': `bearer ${accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return res.json();
}
// ─── Helper: poll until user completes login ──────────────────────────────────
async function waitForLogin(deviceCode, maxWaitMs = 300000) {
const start = Date.now();
while (Date.now() - start < maxWaitMs) {
await new Promise(r => setTimeout(r, 10000)); // poll every 10s
const res = await fetch(EPIC_OAUTH_PROD03, {
method: 'POST',
headers: {
'Authorization': `basic ${SWITCH_TOKEN}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `grant_type=device_code&device_code=${deviceCode}`,
});
const data = await res.json();
if (res.status === 200 && data.access_token) {
return data;
}
const err = data.errorCode || '';
if (err.includes('not_found') || err.includes('expired')) {
return null; // link expired
}
// authorization_pending = still waiting, keep polling
}
return null;
}
// ─── Helper: exchange token for iOS client token ──────────────────────────────
async function exchangeForIosToken(accessToken) {
// Get exchange code
const exRes = await fetch(EPIC_EXCHANGE, {
headers: { 'Authorization': `bearer ${accessToken}` },
});
const exData = await exRes.json();
// Use exchange code with iOS token
const res = await fetch(EPIC_OAUTH_PROD03, {
method: 'POST',
headers: {
'Authorization': `basic ${IOS_TOKEN}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `grant_type=exchange_code&exchange_code=${exData.code}`,
});
return res.json();
}
// ─── Helper: get athena locker (owned skins + styles) ─────────────────────────
async function getLockerData(accountId, accessToken) {
const url = `https://fortnite-public-service-prod11.ol.epicgames.com/fortnite/api/game/v2/profile/${accountId}/client/QueryProfile?profileId=athena&rvn=-1`;
const res = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: '{}',
});
return res.json();
}
// ─── Helper: get common_core (STW founders check) ────────────────────────────
async function getCommonCoreData(accountId, accessToken) {
const url = `https://fortnite-public-service-prod11.ol.epicgames.com/fortnite/api/game/v2/profile/${accountId}/client/QueryProfile?profileId=common_core&rvn=-1`;
const res = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: '{}',
});
return res.json();
}
// ─── Helper: build owned items map from athena profile ────────────────────────
function buildOwnedMap(athenaData) {
const owned = {}; // id (lowercase) -> Set of owned style tags
const items = athenaData?.profileChanges?.[0]?.profile?.items || {};
for (const item of Object.values(items)) {
const tpl = item.templateId || '';
if (!tpl.toLowerCase().startsWith('athena') && !tpl.toLowerCase().startsWith('character')) continue;
const rawId = tpl.split(':')[1] || '';
const id = rawId.toLowerCase();
if (!owned[id]) owned[id] = new Set();
const variants = item.attributes?.variants || [];
for (const v of variants) {
for (const tag of (v.owned || [])) {
owned[id].add(tag);
}
}
}
return owned;
}
// ─── Helper: check which roles the user qualifies for ────────────────────────
function getRolesForLocker(ownedMap, commonCoreItems) {
const earned = [];
for (const req of ROLE_REQUIREMENTS) {
const id = req.id.toLowerCase();
if (!ownedMap[id]) continue; // doesn't own this skin
if (req.styleTag) {
// Must own the specific style
if (ownedMap[id].has(req.styleTag)) {
earned.push(req.role);
}
} else {
// Just owning the skin is enough
earned.push(req.role);
}
}
// STW Founders check via common_core
const ccItems = commonCoreItems || {};
const ccIds = Object.values(ccItems).map(i => (i.templateId || '').toLowerCase());
const hasFounder = STW_FOUNDER_IDS.some(id => ccIds.some(cc => cc.includes(id.toLowerCase())));
const hasUltimate = STW_ULTIMATE_IDS.some(id => ccIds.some(cc => cc.includes(id.toLowerCase())));
// Also check for founder purchase via mtx receipts
const hasMtxFounder = ccIds.some(cc => cc.includes('founder'));
if (hasUltimate) {
earned.push('Ultimate STW Founders');
earned.push('STW Founders'); // ultimate implies standard too
} else if (hasFounder || hasMtxFounder) {
earned.push('STW Founders');
}
return [...new Set(earned)]; // deduplicate
}
// ─── Bot event handlers ───────────────────────────────────────────────────────
client.once('ready', () => {
console.log(`Logged in as ${client.user.tag}`);
console.log('Users can type !verify to verify their Fortnite account.');
});
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
if (message.content !== '!verify') return;
const userId = message.author.id;
const guild = message.guild;
if (!guild) return;
// Prevent duplicate verifications
if (pending.has(userId)) {
return message.reply('You already have a verification in progress. Check your DMs for the login link.');
}
await message.reply('Check your DMs for a login link to verify your Fortnite account!');
try {
// Step 1: get switch token
const switchToken = await getSwitchAccessToken();
// Step 2: create device code
const deviceData = await createDeviceCode(switchToken);
if (!deviceData.user_code || !deviceData.device_code) {
return message.author.send('❌ Failed to generate a login link. Please try again later.');
}
const loginUrl = `https://www.epicgames.com/activate?userCode=${deviceData.user_code}`;
// Step 3: DM the user the link
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle('Fortnite Account Verification')
.setDescription(
`Click the link below to log into your Epic Games account.\n\n` +
`**[Click here to verify →](${loginUrl})**\n\n` +
`Your code: \`${deviceData.user_code}\`\n\n` +
`This link expires in **5 minutes**. Once you log in, the bot will automatically check your locker and assign your roles.`
)
.setFooter({ text: 'Your login is read-only. The bot only checks your cosmetics.' });
await message.author.send({ embeds: [embed] });
pending.set(userId, { guildId: guild.id, channelId: message.channel.id });
// Step 4: poll for completion
const tokenData = await waitForLogin(deviceData.device_code);
pending.delete(userId);
if (!tokenData) {
return message.author.send('❌ Login link expired or was not completed. Run `!verify` again to try again.');
}
// Step 5: exchange for iOS token (gives us full API access)
const iosData = await exchangeForIosToken(tokenData.access_token);
if (!iosData.access_token) {
return message.author.send('❌ Authentication failed. Please try again.');
}
const accountId = iosData.account_id;
const displayName = iosData.displayName || 'Unknown';
const accessToken = iosData.access_token;
await message.author.send(`✅ Logged in as **${displayName}**! Checking your locker...`);
// Step 6: fetch locker data
const [athenaData, commonCoreData] = await Promise.all([
getLockerData(accountId, accessToken),
getCommonCoreData(accountId, accessToken),
]);
if (!athenaData?.profileChanges) {
return message.author.send('❌ Failed to read your locker. Please try again.');
}
// Step 7: figure out which roles they earn
const ownedMap = buildOwnedMap(athenaData);
const ccItems = commonCoreData?.profileChanges?.[0]?.profile?.items || {};
const earnedRoleNames = getRolesForLocker(ownedMap, ccItems);
// Step 8: assign roles in the guild
const member = await guild.members.fetch(userId).catch(() => null);
if (!member) {
return message.author.send('❌ Could not find you in the server. Make sure you are still in the server.');
}
const assigned = [];
const notFound = [];
for (const roleName of earnedRoleNames) {
const role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase());
if (!role) {
notFound.push(roleName);
continue;
}
if (!member.roles.cache.has(role.id)) {
await member.roles.add(role, 'Fortnite locker verification').catch(() => null);
}
assigned.push(roleName);
}
// Step 9: send result
if (assigned.length === 0) {
await message.author.send(
`✅ Verification complete for **${displayName}**!\n\n` +
`No matching exclusive roles were found for your account. ` +
`If you think this is wrong, contact a server admin.`
);
} else {
await message.author.send(
`✅ Verification complete for **${displayName}**!\n\n` +
`**Roles assigned (${assigned.length}):**\n` +
assigned.map(r => `• ${r}`).join('\n') +
(notFound.length ? `\n\n⚠️ These roles were earned but not found in the server: ${notFound.join(', ')}` : '')
);
}
// Also send a confirmation in the original channel
const ch = guild.channels.cache.get(message.channel.id);
if (ch) {
ch.send(`✅ <@${userId}> has been verified and received **${assigned.length}** role(s).`).catch(() => null);
}
} catch (err) {
pending.delete(userId);
console.error('Verify error:', err);
message.author.send('❌ An unexpected error occurred during verification. Please try again.').catch(() => null);
}
});
client.login(process.env.TOKEN);