Skip to content

Commit 471bf0d

Browse files
authored
Merge pull request #19 from ToastedDev/dev
feat: add ability to set xp and level for a user
2 parents b6120b3 + ce93ddd commit 471bf0d

File tree

4 files changed

+237
-64
lines changed

4 files changed

+237
-64
lines changed

api/db/queries/users.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,43 @@ export async function getUser(userId: string, guildId: string): Promise<[QueryEr
3636
});
3737
});
3838
}
39+
40+
export async function setXP(guildId: string, userId: string, xp: number): Promise<[QueryError | null, boolean]> {
41+
const newLevel = Math.floor(Math.sqrt(xp / 100));
42+
const nextLevel = newLevel + 1;
43+
const nextLevelXp = Math.pow(nextLevel, 2) * 100;
44+
const xpNeededForNextLevel = nextLevelXp - xp;
45+
const currentLevelXp = Math.pow(newLevel, 2) * 100;
46+
const progressToNextLevel =
47+
((xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;
48+
49+
return new Promise((resolve, reject) => {
50+
pool.query("UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ? WHERE id = ? AND guild_id = ?", [xp, newLevel, xpNeededForNextLevel.toFixed(2), progressToNextLevel.toFixed(2), userId, guildId], (err) => {
51+
if (err) {
52+
reject([err, false]);
53+
} else {
54+
resolve([null, true]);
55+
}
56+
});
57+
});
58+
}
59+
60+
export async function setLevel(guildId: string, userId: string, level: number): Promise<[QueryError | null, boolean]> {
61+
const newXp = Math.pow(level, 2) * 100;
62+
const nextLevel = level + 1;
63+
const nextLevelXp = Math.pow(nextLevel, 2) * 100;
64+
const xpNeededForNextLevel = nextLevelXp - newXp;
65+
const currentLevelXp = Math.pow(level, 2) * 100;
66+
const progressToNextLevel =
67+
((newXp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;
68+
69+
return new Promise((resolve, reject) => {
70+
pool.query("UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ? WHERE id = ? AND guild_id = ?", [newXp, level, xpNeededForNextLevel.toFixed(2), progressToNextLevel.toFixed(2), userId, guildId], (err) => {
71+
if (err) {
72+
reject([err, false]);
73+
} else {
74+
resolve([null, true]);
75+
}
76+
});
77+
});
78+
}

api/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express, { type NextFunction, type Request, type Response } from "express";
22
import cors from "cors";
33
import path from "path";
4-
import { getBotInfo, getGuild, getUser, getUsers, initTables, pool, updateGuild, enableUpdates, disableUpdates, setCooldown, setUpdatesChannel } from "./db";
4+
import { getBotInfo, getGuild, getUser, getUsers, initTables, pool, updateGuild, enableUpdates, disableUpdates, setCooldown, setUpdatesChannel, setXP, setLevel } from "./db";
55

66
const app = express();
77
const PORT = 18103;
@@ -292,6 +292,42 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => {
292292
default:
293293
return res.status(500).json({ message: "Internal server error" });
294294
}
295+
case "set": {
296+
if (target !== "xp" && target !== "level") {
297+
return res.status(400).json({ message: "Illegal request" });
298+
}
299+
300+
if(!extraData || !extraData.user || !extraData.value) {
301+
return res.status(400).json({ message: "Illegal request" });
302+
}
303+
304+
switch (target) {
305+
case "xp":
306+
try {
307+
const [err, success] = await setXP(guild, extraData.user, extraData.value);
308+
if (err) {
309+
return res.status(500).json({ message: "Internal server error", err });
310+
} else {
311+
return res.status(200).json(success);
312+
}
313+
} catch (err) {
314+
return res.status(500).json({ message: "Internal server error", err });
315+
}
316+
case "level":
317+
try {
318+
const [err, success] = await setLevel(guild, extraData.user, extraData.value);
319+
if (err) {
320+
return res.status(500).json({ message: "Internal server error", err });
321+
} else {
322+
return res.status(200).json(success);
323+
}
324+
} catch (err) {
325+
return res.status(500).json({ message: "Internal server error", err });
326+
}
327+
default:
328+
return res.status(500).json({ message: "Internal server error" });
329+
}
330+
}
295331
default:
296332
return res.status(400).json({ message: "Illegal request" });
297333
}

bot/commands.ts

Lines changed: 136 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import client from '.';
44
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type CommandInteraction, ChannelType, type APIApplicationCommandOption, GuildMember, AttachmentBuilder, ComponentType } from 'discord.js';
55
import { heapStats } from 'bun:jsc';
6-
import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel } from './utils/requestAPI';
6+
import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel, setXP, setLevel } from './utils/requestAPI';
77
import convertToLevels from './utils/convertToLevels';
88
import quickEmbed from './utils/quickEmbed';
99
import { Font, RankCardBuilder } from 'canvacord';
@@ -154,74 +154,74 @@ const commands: Record<string, Command> = {
154154
});
155155
return;
156156
}
157-
157+
158158
const rank = leaderboard.leaderboard.findIndex((entry: ({ id: string; })) => entry.id === user) + 1;
159-
159+
160160
const card = new RankCardBuilder()
161-
.setDisplayName(member.displayName)
162-
.setAvatar(member.displayAvatarURL({ forceStatic: true, size: 4096 })) // user avatar
163-
.setCurrentXP(xp.xp) // current xp
164-
.setRequiredXP(xp.xp + xp.xp_needed_next_level) // required xp
165-
.setLevel(xp.level) // user level
166-
.setRank(rank) // user rank
167-
.setOverlay(member.user.banner ? 95 : 90) // overlay percentage. Overlay is a semi-transparent layer on top of the background
168-
.setBackground(member.user.bannerURL({ forceStatic: true, size: 4096 }) ?? "#23272a")
169-
161+
.setDisplayName(member.displayName)
162+
.setAvatar(member.displayAvatarURL({ forceStatic: true, size: 4096 })) // user avatar
163+
.setCurrentXP(xp.xp) // current xp
164+
.setRequiredXP(xp.xp + xp.xp_needed_next_level) // required xp
165+
.setLevel(xp.level) // user level
166+
.setRank(rank) // user rank
167+
.setOverlay(member.user.banner ? 95 : 90) // overlay percentage. Overlay is a semi-transparent layer on top of the background
168+
.setBackground(member.user.bannerURL({ forceStatic: true, size: 4096 }) ?? "#23272a")
169+
170170
if (interaction.user.discriminator !== "0") {
171-
card.setUsername("#" + member.user.discriminator)
171+
card.setUsername("#" + member.user.discriminator)
172172
} else {
173-
card.setUsername("@" + member.user.username)
173+
card.setUsername("@" + member.user.username)
174174
}
175-
175+
176176
const color = member.roles.highest.hexColor ?? "#ffffff"
177177

178-
card.setStyles({
179-
progressbar: {
180-
thumb: {
181-
style: {
182-
backgroundColor: color
178+
card.setStyles({
179+
progressbar: {
180+
thumb: {
181+
style: {
182+
backgroundColor: color
183183
}
184-
}
185-
},
186-
})
187-
188-
const image = await card.build({
189-
format: "png"
190-
});
191-
const attachment = new AttachmentBuilder(image, { name: `${user}.png` });
184+
}
185+
},
186+
})
187+
188+
const image = await card.build({
189+
format: "png"
190+
});
191+
const attachment = new AttachmentBuilder(image, { name: `${user}.png` });
192192

193193
const msg = await interaction.followUp({
194-
files: [attachment],
194+
files: [attachment],
195195
components: [
196-
new ActionRowBuilder<ButtonBuilder>().setComponents(
196+
new ActionRowBuilder<ButtonBuilder>().setComponents(
197197
new ButtonBuilder()
198-
.setCustomId("text-mode")
199-
.setLabel("Use text mode")
200-
.setStyle(ButtonStyle.Secondary)
198+
.setCustomId("text-mode")
199+
.setLabel("Use text mode")
200+
.setStyle(ButtonStyle.Secondary)
201201
)
202202
],
203203
fetchReply: true
204204
});
205-
206-
const collector = msg.createMessageComponentCollector({
207-
componentType: ComponentType.Button,
208-
time: 60 * 1000
209-
});
210-
211-
collector.on("collect", async (i) => {
212-
if (i.user.id !== user)
213-
return i.reply({
214-
content: "You're not the one who initialized this message! Try running /xp on your own.",
215-
ephemeral: true
216-
});
217-
218-
if (i.customId !== "text-mode") return;
219-
220-
const progress = xp.progress_next_level;
221-
const progressBar = createProgressBar(progress);
222-
223-
await i.update({
224-
embeds: [
205+
206+
const collector = msg.createMessageComponentCollector({
207+
componentType: ComponentType.Button,
208+
time: 60 * 1000
209+
});
210+
211+
collector.on("collect", async (i) => {
212+
if (i.user.id !== user)
213+
return i.reply({
214+
content: "You're not the one who initialized this message! Try running /xp on your own.",
215+
ephemeral: true
216+
});
217+
218+
if (i.customId !== "text-mode") return;
219+
220+
const progress = xp.progress_next_level;
221+
const progressBar = createProgressBar(progress);
222+
223+
await i.update({
224+
embeds: [
225225
quickEmbed(
226226
{
227227
color,
@@ -248,10 +248,10 @@ const commands: Record<string, Command> = {
248248
],
249249
files: [],
250250
components: []
251-
})
252-
})
253-
254-
function createProgressBar(progress: number): string {
251+
})
252+
})
253+
254+
function createProgressBar(progress: number): string {
255255
const filled = Math.floor(progress / 10);
256256
const empty = 10 - filled;
257257
return '▰'.repeat(filled) + '▱'.repeat(empty);
@@ -286,11 +286,11 @@ const commands: Record<string, Command> = {
286286
}, interaction);
287287

288288
// Add a field for each user with a mention
289-
leaderboard.leaderboard.forEach((entry: { user_id: string; xp: number; }, index: number) => {
289+
leaderboard.leaderboard.forEach((entry: { id: string; xp: number; }, index: number) => {
290290
leaderboardEmbed.addFields([
291291
{
292292
name: `${index + 1}.`,
293-
value: `<@${entry.user_id}>: ${entry.xp.toLocaleString("en-US")} XP`,
293+
value: `<@${entry.id}>: ${entry.xp.toLocaleString("en-US")} XP`,
294294
inline: false
295295
}
296296
]);
@@ -475,7 +475,7 @@ const commands: Record<string, Command> = {
475475
value: 'reset',
476476
},
477477
]
478-
},{
478+
}, {
479479
name: 'channel',
480480
description: 'Enter the channel ID. Required for set action.',
481481
type: 7,
@@ -523,8 +523,8 @@ const commands: Record<string, Command> = {
523523
}
524524
await interaction.reply({ ephemeral: true, content: 'Updates are now disabled for this server' }).catch(console.error);
525525
return;
526-
case 'set':
527-
if(!channelId) {
526+
case 'set':
527+
if (!channelId) {
528528
await interaction.reply({ ephemeral: true, content: 'ERROR: Channel was not specified!' });
529529
return;
530530
}
@@ -592,7 +592,7 @@ const commands: Record<string, Command> = {
592592
value: 'set',
593593
}
594594
]
595-
},{
595+
}, {
596596
name: 'cooldown',
597597
description: 'Enter the cooldown in seconds. Required for set action.',
598598
type: 4,
@@ -648,6 +648,79 @@ const commands: Record<string, Command> = {
648648
return;
649649
}
650650
}
651+
},
652+
set: {
653+
data: {
654+
options: [{
655+
name: 'user',
656+
description: 'The user you want to update the XP or level of.',
657+
type: 6,
658+
required: true,
659+
}, {
660+
name: 'type',
661+
description: 'Select the data type to set',
662+
type: 3,
663+
required: true,
664+
choices: [
665+
{
666+
name: 'XP',
667+
value: 'xp',
668+
},
669+
{
670+
name: 'Level',
671+
value: 'level',
672+
}
673+
]
674+
}, {
675+
name: 'value',
676+
description: 'The new value to set',
677+
type: 3,
678+
required: true,
679+
}],
680+
name: 'set',
681+
description: 'Set the XP or level of a user!',
682+
integration_types: [0],
683+
contexts: [0, 2],
684+
},
685+
execute: async (interaction) => {
686+
if (!interaction.memberPermissions?.has('ManageGuild')) {
687+
const errorEmbed = quickEmbed({
688+
color: 'Red',
689+
title: 'Error!',
690+
description: 'Missing permissions: `Manage Server`'
691+
}, interaction);
692+
await interaction.reply({
693+
ephemeral: true,
694+
embeds: [errorEmbed]
695+
})
696+
.catch(console.error);
697+
return;
698+
}
699+
700+
const user = interaction.options.get('user')?.value as string;
701+
const type = interaction.options.get('type')?.value;
702+
const value = interaction.options.get('value')?.value;
703+
704+
let apiSuccess;
705+
switch (type) {
706+
case 'xp':
707+
apiSuccess = await setXP(interaction.guildId as string, user, parseInt(value as string));
708+
if (!apiSuccess) {
709+
await interaction.reply({ ephemeral: true, content: 'Error setting XP!' });
710+
return;
711+
}
712+
await interaction.reply({ ephemeral: true, content: `XP set to ${value} for <@${user}>` });
713+
return;
714+
case 'level':
715+
apiSuccess = await setLevel(interaction.guildId as string, user, parseInt(value as string));
716+
if (!apiSuccess) {
717+
await interaction.reply({ ephemeral: true, content: 'Error setting level!' });
718+
return;
719+
}
720+
await interaction.reply({ ephemeral: true, content: `Level set to ${value} for <@${user}>` });
721+
return;
722+
}
723+
}
651724
}
652725
};
653726

0 commit comments

Comments
 (0)