Files
ShaiBot/utils/profileSystem.js
T
2026-08-22 22:10:41 +02:00

2406 lines
43 KiB
JavaScript

const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
mClient
} = require("../index");
const {
getCookieBalance,
getUnixTime
} = require("./cookieEconomy");
/*
* ============================================================
* CONFIG
* ============================================================
*/
const DATABASE_NAME =
process.env.M_DB;
const COLLECTIONS = {
stats:
"stats_profiles",
achievements:
"items_achievements",
badges:
"guild_badges",
achievementConfig:
"config_achievements"
};
/*
* ============================================================
* COLLECTION ACCESS
* ============================================================
*/
function getProfileCollections() {
if (!DATABASE_NAME) {
throw new Error(
"M_DB is not configured in .env"
);
}
const db =
mClient.db(
DATABASE_NAME
);
return {
db,
stats:
db.collection(
COLLECTIONS.stats
),
achievements:
db.collection(
COLLECTIONS.achievements
),
badges:
db.collection(
COLLECTIONS.badges
),
achievementConfig:
db.collection(
COLLECTIONS.achievementConfig
)
};
}
/*
* ============================================================
* INITIALIZE
* ============================================================
*/
async function ensureProfileSystem() {
const {
stats,
achievements,
badges,
achievementConfig
} =
getProfileCollections();
await stats.createIndex(
{
userId:
1,
guildId:
1
},
{
unique:
true
}
);
await achievements.createIndex(
{
userId:
1,
guildId:
1
},
{
unique:
true
}
);
await badges.createIndex(
{
guildId:
1,
badgeId:
1
},
{
unique:
true
}
);
await achievementConfig.createIndex(
{
achievementId:
1
},
{
unique:
true
}
);
}
/*
* ============================================================
* DEFAULT STATS
* ============================================================
*/
function getDefaultStats(
userId,
guildId
) {
return {
userId,
guildId,
economy: {
cookiesObtainedTotal:
0,
cookiesLostTotal:
0,
cookiesGivenTotal:
0,
stolenTotal:
0,
stolenFromMeTotal:
0,
vaultStolenTotal:
0
},
unique: {
playersStolenFrom:
[],
playersStolenBy:
[]
},
games: {
total: {
played:
0,
won:
0,
lost:
0,
pushed:
0
},
slots: {
played:
0,
won:
0,
lost:
0,
pushed:
0,
cookiesWon:
0,
cookiesLost:
0
},
blackjack: {
played:
0,
won:
0,
lost:
0,
pushed:
0,
cookiesWon:
0,
cookiesLost:
0
},
roulette: {
played:
0,
won:
0,
lost:
0,
pushed:
0,
cookiesWon:
0,
cookiesLost:
0
},
heist: {
played:
0,
won:
0,
lost:
0,
pushed:
0,
cookiesWon:
0,
cookiesLost:
0
}
},
streaks: {
currentWin:
0,
bestWin:
0,
currentLoss:
0,
bestLoss:
0
}
};
}
/*
* ============================================================
* ENSURE STATS DOCUMENT
* ============================================================
*/
async function ensureStatsDocument(
userId,
guildId
) {
const {
stats
} =
getProfileCollections();
await stats.updateOne(
{
userId,
guildId
},
{
$setOnInsert:
getDefaultStats(
userId,
guildId
)
},
{
upsert:
true
}
);
}
/*
* ============================================================
* ENSURE ACHIEVEMENT DOCUMENT
* ============================================================
*/
async function ensureAchievementDocument(
userId,
guildId
) {
const {
achievements
} =
getProfileCollections();
await achievements.updateOne(
{
userId,
guildId
},
{
$setOnInsert: {
userId,
guildId,
unlocked: {}
}
},
{
upsert:
true
}
);
}
/*
* ============================================================
* GET PROFILE STATS
* ============================================================
*/
async function getProfileStats(
userId,
guildId
) {
const {
stats
} =
getProfileCollections();
const existing =
await stats.findOne({
userId,
guildId
});
return existing
??
getDefaultStats(
userId,
guildId
);
}
/*
* ============================================================
* TRACK PROFILE EVENT
* ============================================================
*/
async function trackProfileEvent(
interaction,
userId,
event
) {
const guildId =
interaction.guildId;
if (
!guildId
||
!userId
||
!event?.type
) {
return;
}
await ensureStatsDocument(
userId,
guildId
);
const {
stats
} =
getProfileCollections();
switch (
event.type
) {
/*
* Lifetime gain.
*/
case "cookie_gain":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.cookiesObtainedTotal":
event.amount
}
}
);
break;
/*
* Lifetime loss.
*/
case "cookie_loss":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.cookiesLostTotal":
event.amount
}
}
);
break;
/*
* Voluntary giving.
*/
case "cookies_given":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.cookiesGivenTotal":
event.amount
}
}
);
break;
/*
* Successful player theft.
*/
case "steal_success":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.stolenTotal":
event.amount
},
$addToSet: {
"unique.playersStolenFrom":
event.targetUserId
}
}
);
break;
/*
* Victim side.
*/
case "stolen_from_me":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.stolenFromMeTotal":
event.amount
},
$addToSet: {
"unique.playersStolenBy":
event.thiefUserId
}
}
);
break;
/*
* Successful Bot Vault Heist.
*/
case "vault_steal":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"economy.vaultStolenTotal":
event.amount
}
}
);
break;
/*
* Game result.
*/
case "game_result":
await trackGameResult(
userId,
guildId,
event
);
break;
default:
return;
}
/*
* Always evaluate from fresh DB state.
*/
const updatedStats =
await getProfileStats(
userId,
guildId
);
await evaluateAchievements(
interaction,
userId,
updatedStats
);
await evaluateBadges(
interaction,
userId,
updatedStats,
event
);
return updatedStats;
}
/*
* ============================================================
* GAME RESULT TRACKING
* ============================================================
*/
async function trackGameResult(
userId,
guildId,
event
) {
const {
stats
} =
getProfileCollections();
const game =
String(
event.game
??
""
).toLowerCase();
if (
![
"slots",
"blackjack",
"roulette",
"heist"
].includes(
game
)
) {
return;
}
const result =
String(
event.result
??
""
).toLowerCase();
const profit =
Number(
event.profit
??
0
);
const increment = {
"games.total.played":
1,
[`games.${game}.played`]:
1
};
const set = {};
/*
* ========================================================
* WIN
* ========================================================
*/
if (
result ===
"win"
) {
increment[
"games.total.won"
] =
1;
increment[
`games.${game}.won`
] =
1;
increment[
"streaks.currentWin"
] =
1;
set[
"streaks.currentLoss"
] =
0;
}
/*
* ========================================================
* LOSS
* ========================================================
*/
else if (
result ===
"loss"
) {
increment[
"games.total.lost"
] =
1;
increment[
`games.${game}.lost`
] =
1;
increment[
"streaks.currentLoss"
] =
1;
set[
"streaks.currentWin"
] =
0;
}
/*
* ========================================================
* PUSH
* ========================================================
*/
else if (
result ===
"push"
) {
increment[
"games.total.pushed"
] =
1;
increment[
`games.${game}.pushed`
] =
1;
/*
* Push leaves streaks unchanged.
*/
}
else {
return;
}
/*
* ========================================================
* REALIZED NET COOKIE RESULT
* ========================================================
*
* Positive:
*
* cookiesWon += profit
*
* Negative:
*
* cookiesLost += abs(profit)
*
* Push:
*
* normally profit = 0
*/
if (
Number.isFinite(
profit
)
) {
if (
profit >
0
) {
increment[
`games.${game}.cookiesWon`
] =
profit;
}
else if (
profit <
0
) {
increment[
`games.${game}.cookiesLost`
] =
Math.abs(
profit
);
}
}
await stats.updateOne(
{
userId,
guildId
},
{
$inc:
increment,
...(Object.keys(set).length
? {
$set:
set
}
: {})
}
);
/*
* Update best streaks.
*/
const current =
await stats.findOne({
userId,
guildId
});
await stats.updateOne(
{
userId,
guildId
},
{
$max: {
"streaks.bestWin":
current?.streaks
?.currentWin
??
0,
"streaks.bestLoss":
current?.streaks
?.currentLoss
??
0
}
}
);
}
/*
* ============================================================
* ACHIEVEMENT EVALUATION
* ============================================================
*/
async function evaluateAchievements(
interaction,
userId,
statsDocument
) {
const guildId =
interaction.guildId;
const {
achievements,
achievementConfig
} =
getProfileCollections();
await ensureAchievementDocument(
userId,
guildId
);
/*
* Only active achievements count toward
* normal evaluation and collector progress.
*/
const definitions =
await achievementConfig
.find({
active: {
$ne:
false
}
})
.toArray();
let unlockedDoc =
await achievements.findOne({
userId,
guildId
});
let unlocked =
unlockedDoc?.unlocked
??
{};
/*
* ========================================================
* NORMAL ACHIEVEMENTS FIRST
* ========================================================
*
* Collector achievements are evaluated afterwards,
* because unlocking one normal achievement can itself
* push the user over 50% or 100%.
*/
const normalDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
for (
const definition
of normalDefinitions
) {
if (
unlocked[
definition.achievementId
]
) {
continue;
}
const currentValue =
readStatValue(
statsDocument,
definition
);
if (
currentValue <
Number(
definition.target
)
) {
continue;
}
const unlockedNow =
await unlockAchievement(
interaction,
userId,
guildId,
definition
);
if (
unlockedNow
) {
unlocked[
definition.achievementId
] =
getUnixTime();
}
}
/*
* Refresh after normal unlocks.
*/
unlockedDoc =
await achievements.findOne({
userId,
guildId
});
unlocked =
unlockedDoc?.unlocked
??
{};
/*
* ========================================================
* COLLECTOR ACHIEVEMENTS
* ========================================================
*/
const collectorDefinitions =
definitions.filter(
definition =>
definition.measure ===
"achievementPercentage"
);
/*
* Collector achievements should NOT count themselves
* toward the denominator.
*
* Otherwise adding "50%" and "100%" achievements changes
* the percentage they are measuring.
*/
const collectibleDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
const totalCollectible =
collectibleDefinitions.length;
const unlockedCollectible =
collectibleDefinitions.filter(
definition =>
Boolean(
unlocked[
definition.achievementId
]
)
).length;
const achievementPercentage =
totalCollectible >
0
? unlockedCollectible /
totalCollectible
: 0;
for (
const definition
of collectorDefinitions
) {
if (
unlocked[
definition.achievementId
]
) {
continue;
}
if (
achievementPercentage <
Number(
definition.target
)
) {
continue;
}
const unlockedNow =
await unlockAchievement(
interaction,
userId,
guildId,
definition
);
if (
unlockedNow
) {
unlocked[
definition.achievementId
] =
getUnixTime();
}
}
}
/*
* ============================================================
* UNLOCK ACHIEVEMENT
* ============================================================
*/
async function unlockAchievement(
interaction,
userId,
guildId,
definition
) {
const {
achievements
} =
getProfileCollections();
const field =
`unlocked.${definition.achievementId}`;
const result =
await achievements.updateOne(
{
userId,
guildId,
[field]: {
$exists:
false
}
},
{
$set: {
[field]:
getUnixTime()
}
}
);
if (
result.modifiedCount !==
1
) {
return false;
}
await sendAchievementNotification(
interaction,
definition
);
return true;
}
/*
* ============================================================
* READ STAT VALUE
* ============================================================
*/
function readStatValue(
document,
definition
) {
if (
!definition.stat
) {
return 0;
}
const value =
definition.stat
.split(".")
.reduce(
(
current,
key
) =>
current?.[key],
document
);
if (
definition.measure ===
"arrayLength"
) {
return Array.isArray(
value
)
? value.length
: 0;
}
return Number(
value
??
0
);
}
/*
* ============================================================
* ACHIEVEMENT NOTIFICATION
* ============================================================
*/
async function sendAchievementNotification(
interaction,
achievement
) {
try {
await interaction.followUp({
embeds: [
new EmbedBuilder()
.setTitle(
"🏆 Achievement Unlocked!"
)
.setDescription(
[
`${achievement.emoji} **${achievement.name}**`,
"",
achievement.description
].join("\n")
)
],
flags:
MessageFlags.Ephemeral
});
}
catch {
/*
* Unlock stays persisted even if the Discord
* interaction can no longer accept follow-ups.
*/
}
}
/*
* ============================================================
* BADGE EVALUATION
* ============================================================
*
* Existing badges remain supported here.
*
* We can expand this next with:
*
* - first Slots jackpot
* - first natural Blackjack
* - first Roulette Zero
* - first 5 / 10 / 15 unique steals
* - Achievement Hunter
* - Completionist
*/
async function evaluateBadges(
interaction,
userId,
statsDocument,
event =
{}
) {
const guildId =
interaction.guildId;
if (!guildId) {
return;
}
/*
* ========================================================
* ECONOMY BADGES
* ========================================================
*/
const balance =
await getCookieBalance(
userId
);
if (
balance >=
100000
) {
await tryClaimBadge(
interaction,
userId,
"first_100k_cookies"
);
}
if (
balance >=
1000000
) {
await tryClaimBadge(
interaction,
userId,
"first_1m_cookies"
);
}
/*
* ========================================================
* VAULT BADGE
* ========================================================
*/
if (
(
statsDocument
?.economy
?.vaultStolenTotal
??
0
)
>=
100000
) {
await tryClaimBadge(
interaction,
userId,
"first_100k_vault_stolen"
);
}
/*
* ========================================================
* UNIQUE PLAYER THEFT BADGES
* ========================================================
*/
const uniquePlayersStolenFrom =
statsDocument
?.unique
?.playersStolenFrom
??
[];
const uniqueStealCount =
uniquePlayersStolenFrom.length;
if (
uniqueStealCount >=
5
) {
await tryClaimBadge(
interaction,
userId,
"first_unique_steals_5"
);
}
if (
uniqueStealCount >=
10
) {
await tryClaimBadge(
interaction,
userId,
"first_unique_steals_10"
);
}
if (
uniqueStealCount >=
15
) {
await tryClaimBadge(
interaction,
userId,
"first_unique_steals_15"
);
}
/*
* ========================================================
* SLOT JACKPOT
* ========================================================
*/
if (
event.type ===
"game_result"
&&
event.game ===
"slots"
&&
event.jackpot ===
true
) {
await tryClaimBadge(
interaction,
userId,
"first_slots_jackpot"
);
}
/*
* ========================================================
* NATURAL BLACKJACK
* ========================================================
*/
if (
event.type ===
"game_result"
&&
event.game ===
"blackjack"
&&
event.natural ===
true
&&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_blackjack_natural"
);
}
/*
* ========================================================
* BLACKJACK ALL IN
* ========================================================
*/
if (
event.type ===
"game_result"
&&
event.game ===
"blackjack"
&&
event.allIn ===
true
&&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_blackjack_allin_win"
);
}
/*
* ========================================================
* ROULETTE ZERO
* ========================================================
*/
if (
event.type ===
"game_result"
&&
event.game ===
"roulette"
&&
event.zero ===
true
&&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_roulette_zero"
);
}
/*
* ========================================================
* ROULETTE ALL IN
* ========================================================
*/
if (
event.type ===
"game_result"
&&
event.game ===
"roulette"
&&
event.allIn ===
true
&&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_roulette_allin_win"
);
}
/*
* ========================================================
* ACHIEVEMENT COLLECTOR BADGES
* ========================================================
*/
await evaluateCollectorBadges(
interaction,
userId
);
}
async function evaluateCollectorBadges(
interaction,
userId
) {
const guildId =
interaction.guildId;
const {
achievements,
achievementConfig
} =
getProfileCollections();
/*
* Don't count Achievement Hunter / Completionist
* themselves toward the percentage.
*/
const activeAchievements =
await achievementConfig
.find({
active: {
$ne:
false
},
measure: {
$ne:
"achievementPercentage"
}
})
.toArray();
if (
activeAchievements.length ===
0
) {
return;
}
const achievementDocument =
await achievements.findOne({
userId,
guildId
});
const unlocked =
achievementDocument
?.unlocked
??
{};
const unlockedCount =
activeAchievements.filter(
achievement =>
Boolean(
unlocked[
achievement.achievementId
]
)
).length;
const percentage =
unlockedCount /
activeAchievements.length;
/*
* First player to 50%.
*/
if (
percentage >=
0.5
) {
await tryClaimBadge(
interaction,
userId,
"first_achievement_half",
{
unlockedAchievements:
unlockedCount,
availableAchievements:
activeAchievements.length
}
);
}
/*
* First player to 100%.
*/
if (
percentage >=
1
) {
await tryClaimBadge(
interaction,
userId,
"first_achievement_all",
{
unlockedAchievements:
unlockedCount,
availableAchievements:
activeAchievements.length
}
);
}
}
/*
* ============================================================
* CLAIM BADGE
* ============================================================
*/
async function tryClaimBadge(
interaction,
userId,
badgeId,
metadata =
null
) {
const {
badges
} =
getProfileCollections();
const guildId =
interaction.guildId;
if (
!guildId
) {
return false;
}
/*
* Build the fields that should be written
* only when this badge is actually claimed.
*/
const update = {
ownerId:
userId,
unlockedAt:
getUnixTime()
};
/*
* Optional historical context.
*
* Example for Completionist:
*
* {
* unlockedAchievements: 64,
* availableAchievements: 64
* }
*/
if (
metadata
&&
typeof metadata ===
"object"
) {
update.metadata =
metadata;
}
/*
* Atomic claim.
*
* Only an unclaimed badge can be updated.
*
* If two users qualify at essentially the
* same time, MongoDB lets only one update
* ownerId from null.
*/
const badge =
await badges.findOneAndUpdate(
{
guildId,
badgeId,
ownerId:
null
},
{
$set:
update
},
{
returnDocument:
"after"
}
);
/*
* Badge does not exist, was already claimed,
* or this user did not win the atomic claim.
*/
if (
!badge
||
badge.ownerId !==
userId
) {
return false;
}
/*
* ========================================================
* PUBLIC ANNOUNCEMENT
* ========================================================
*/
const description = [
`${badge.emoji ?? "🏅"} **${badge.name ?? badge.badgeId}**`,
"",
`<@${userId}> claimed a guild-unique badge!`,
"",
badge.description
??
"A unique badge has been claimed.",
"",
"*This badge can no longer be earned by another player.*"
];
/*
* Add collector history if applicable.
*/
if (
badge.metadata
?.unlockedAchievements !==
undefined
&&
badge.metadata
?.availableAchievements !==
undefined
) {
description.splice(
description.length -
1,
0,
`Claimed at **${badge.metadata.unlockedAchievements}/${badge.metadata.availableAchievements} achievements**.`,
""
);
}
try {
await interaction.channel.send({
embeds: [
new EmbedBuilder()
.setTitle(
"🏅 Unique Badge Claimed!"
)
.setDescription(
description.join(
"\n"
)
)
.setFooter({
text:
`badge:${badge.badgeId}`
})
.setTimestamp()
]
});
}
catch (error) {
/*
* The badge claim itself is already persisted.
*
* A Discord send failure should never undo it.
*/
console.error(
`[Badge] Failed to announce ${badgeId} for ${userId}:`,
error
);
}
return true;
}
/*
* ============================================================
* PROFILE OVERVIEW
* ============================================================
*/
async function buildProfileOverview(
guildId,
user
) {
const stats =
await getProfileStats(
user.id,
guildId
);
const balance =
await getCookieBalance(
user.id
);
const {
achievements,
badges,
achievementConfig
} =
getProfileCollections();
const [
achievementDoc,
achievementCount,
ownedBadges
] =
await Promise.all([
achievements.findOne({
userId:
user.id,
guildId
}),
achievementConfig.countDocuments({
active: {
$ne:
false
}
}),
badges.find({
guildId,
ownerId:
user.id
}).toArray()
]);
const unlockedCount =
Object.keys(
achievementDoc?.unlocked
??
{}
).length;
return new EmbedBuilder()
.setTitle(
`👤 ${user.globalName ?? user.username}'s Profile`
)
.setThumbnail(
user.displayAvatarURL({
size:
256
})
)
.addFields(
{
name:
"🍪 Cookies",
value:
balance.toLocaleString(),
inline:
true
},
{
name:
"🏆 Achievements",
value:
`${unlockedCount} / ${achievementCount}`,
inline:
true
},
{
name:
"🏅 Unique Badges",
value:
`${ownedBadges.length}`,
inline:
true
},
{
name:
"🎮 Games",
value:
[
`Played: **${stats.games.total.played}**`,
`Won: **${stats.games.total.won}**`,
`Lost: **${stats.games.total.lost}**`,
`Pushes: **${stats.games.total.pushed}**`
].join("\n"),
inline:
true
},
{
name:
"🔥 Best Streaks",
value:
[
`Wins: **${stats.streaks.bestWin}**`,
`Losses: **${stats.streaks.bestLoss}**`
].join("\n"),
inline:
true
}
)
.setFooter({
text:
`profile:${user.id}`
});
}
/*
* ============================================================
* BADGES PAGE
* ============================================================
*/
async function buildBadgesEmbed(
guildId,
user
) {
const {
badges
} =
getProfileCollections();
const documents =
await badges.find({
guildId
}).toArray();
const owned =
documents.filter(
badge =>
badge.ownerId ===
user.id
);
const claimedByOthers =
documents.filter(
badge =>
badge.ownerId
&&
badge.ownerId !==
user.id
);
const unclaimed =
documents.filter(
badge =>
!badge.ownerId
);
return new EmbedBuilder()
.setTitle(
`🏅 ${user.globalName ?? user.username}'s Badges`
)
.setDescription(
[
"### Owned",
owned.length
? owned
.map(
badge =>
`${badge.emoji} **${badge.name}**\n${badge.description}`
)
.join("\n\n")
: "*No unique badges yet.*",
"",
"### Claimed by Others",
claimedByOthers.length
? claimedByOthers
.map(
badge =>
`${badge.emoji} **${badge.name}** — <@${badge.ownerId}>`
)
.join("\n")
: "*None yet.*",
"",
"### Still Available",
unclaimed.length
? unclaimed
.map(
badge =>
`${badge.emoji} **${badge.name}**\n${badge.description}`
)
.join("\n\n")
: "*All unique badges have been claimed.*"
].join("\n")
)
.setFooter({
text:
`profile:${user.id}`
});
}
/*
* ============================================================
* ACHIEVEMENTS PAGE
* ============================================================
*/
async function buildAchievementsEmbed(
guildId,
user
) {
const {
achievements,
achievementConfig
} =
getProfileCollections();
const [
unlockedDoc,
definitions
] =
await Promise.all([
achievements.findOne({
userId:
user.id,
guildId
}),
achievementConfig
.find({
active: {
$ne:
false
}
})
.sort({
category:
1,
target:
1
})
.toArray()
]);
const unlocked =
unlockedDoc?.unlocked
??
{};
const lines =
definitions.map(
achievement => {
const complete =
Boolean(
unlocked[
achievement.achievementId
]
);
return [
complete
? "✅"
: "⬜",
" ",
achievement.emoji,
" **",
achievement.name,
"** — ",
achievement.requirement
].join("");
}
);
return new EmbedBuilder()
.setTitle(
`🏆 ${user.globalName ?? user.username}'s Achievements`
)
.setDescription(
lines.join("\n")
||
"No achievements configured."
)
.setFooter({
text:
`profile:${user.id}`
});
}
/*
* ============================================================
* PROFILE BUTTONS
* ============================================================
*/
function buildProfileButtons() {
return [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"profileOverview"
)
.setLabel(
"Overview"
)
.setEmoji(
"👤"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"profileBadges"
)
.setLabel(
"Badges"
)
.setEmoji(
"🏅"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"profileAchievements"
)
.setLabel(
"Achievements"
)
.setEmoji(
"🏆"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"abort"
)
.setLabel(
"Close"
)
.setEmoji(
"✖️"
)
.setStyle(
ButtonStyle.Secondary
)
)
];
}
/*
* ============================================================
* PROFILE TARGET
* ============================================================
*/
function getProfileTargetId(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const [
type,
userId
] =
footer?.split(":")
??
[];
if (
type !==
"profile"
) {
return null;
}
return userId;
}
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = {
ensureProfileSystem,
trackProfileEvent,
getProfileStats,
buildProfileOverview,
buildBadgesEmbed,
buildAchievementsEmbed,
buildProfileButtons,
getProfileTargetId
};