Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 248 additions & 2 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => {
} catch (err) {
return res.status(500).json({ message: "Internal server error", err });
}
case 'set':
case 'set':
if (!extraData || typeof extraData.channelId === "undefined") {
return res.status(400).json({ message: "Illegal request" });
}
Expand Down Expand Up @@ -308,7 +308,7 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => {
return res.status(400).json({ message: "Illegal request" });
}

if(!extraData || !extraData.user || !extraData.value) {
if (!extraData || !extraData.user || !extraData.value) {
return res.status(400).json({ message: "Illegal request" });
}

Expand Down Expand Up @@ -339,6 +339,61 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => {
return res.status(500).json({ message: "Internal server error" });
}
}
case "sync": {
if (target !== "polaris" && target !== "mee6" && target !== "lurkr") {
return res.status(400).json({ message: "Illegal request" });
}

switch (target) {
case "polaris": {
try {
const [err, success] = await syncFromPolaris(guild);
if (err) {
if (err instanceof Error && err.message === "Server not found in Polaris") {
return res.status(404).json({ message: "Server not found in Polaris" });
}
return res.status(500).json({ message: "Internal server error", err });
} else {
return res.status(200).json(success);
}
} catch (err) {
return res.status(500).json({ message: "Internal server error", err });
}
}
case "mee6": {
try {
const [err, success] = await syncFromMee6(guild);
if (err) {
if (err instanceof Error && err.message === "Server not found in MEE6") {
return res.status(404).json({ message: "Server not found in MEE6" });
}
return res.status(500).json({ message: "Internal server error", err });
} else {
return res.status(200).json(success);
}
} catch (err) {
return res.status(500).json({ message: "Internal server error", err });
}
}
case "lurkr": {
try {
const [err, success] = await syncFromLurkr(guild);
if (err) {
if (err instanceof Error && err.message === "Server not found in Lurkr") {
return res.status(404).json({ message: "Server not found in Lurkr" });
}
return res.status(500).json({ message: "Internal server error", err });
} else {
return res.status(200).json(success);
}
} catch (err) {
return res.status(500).json({ message: "Internal server error", err });
}
}
default:
return res.status(500).json({ message: "Internal server error" });
}
}
default:
return res.status(400).json({ message: "Illegal request" });
}
Expand Down Expand Up @@ -445,3 +500,194 @@ async function adminRolesAdd(guild: string, role: string, level: number) {
});
}
//#endregion

//#region Syncing
async function syncFromPolaris(guild: string) {
const res = await fetch(`https://gdcolon.com/polaris/api/leaderboard/${guild}`);
const data = await res.json();
if (data.apiError && data.code === "invalidServer") {
return [new Error("Server not found in Polaris"), false];
}
const users = data.leaderboard;
for (let i = 1; i < data.pageInfo.pageCount; i++) {
const res = await fetch(`https://gdcolon.com/polaris/api/leaderboard/${guild}?page=${i + 1}`);
const data = await res.json();
users.push(...data.leaderboard);
}

if (users.length === 0) {
return [new Error("No users found"), false];
}

try {
for (const user of users) {
const xpValue = user.xp;
const level = Math.floor(Math.sqrt(xpValue / 100));
const nextLevel = level + 1;
const nextLevelXp = Math.pow(nextLevel, 2) * 100;
const xpNeededForNextLevel = nextLevelXp - xpValue;
const currentLevelXp = Math.pow(level, 2) * 100;
const progressToNextLevel =
((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;

await new Promise((resolve, reject) => {
pool.query(
`INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
user.id,
guild,
xpValue,
user.avatar,
user.username,
user.nickname ?? user.displayName,
level,
xpNeededForNextLevel,
progressToNextLevel.toFixed(2),
],
(err) => {
if (err) {
console.error("Error syncing from Polaris:", err);
reject(err);
} else {
resolve(null);
}
},
);
});
}
return [null, true]
} catch (err) {
return [err, false];
}

}

async function syncFromMee6(guild: string) {
const res = await fetch(`https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=0`);
const data = await res.json();
if (data.status_code === 404) {
return [new Error("Server not found in MEE6"), false];
}
const users = data.players;
let pageNumber = 1;
// this is needed because MEE6 doesn't give us the total amount of pages
// eslint-disable-next-line no-constant-condition
while (true) {
const res = await fetch(`https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=${pageNumber}`);
const data = await res.json();
users.push(...data.players);
if (data.players.length < 1000) break;
pageNumber += 1;
}

if (users.length === 0) {
return [new Error("No users found"), false];
}

try {
for (const user of users) {
const xpValue = user.xp;
const level = Math.floor(Math.sqrt(xpValue / 100));
const nextLevel = level + 1;
const nextLevelXp = Math.pow(nextLevel, 2) * 100;
const xpNeededForNextLevel = nextLevelXp - xpValue;
const currentLevelXp = Math.pow(level, 2) * 100;
const progressToNextLevel =
((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;

await new Promise((resolve, reject) => {
pool.query(
`INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
user.id,
guild,
xpValue,
`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`,
user.username,
user.username,
level,
xpNeededForNextLevel,
progressToNextLevel.toFixed(2),
],
(err) => {
if (err) {
console.error("Error syncing from MEE6:", err);
reject(err);
} else {
resolve(null);
}
},
);
});
}
return [null, true]
} catch (err) {
return [err, false];
}
}

async function syncFromLurkr(guild: string) {
const res = await fetch(`https://api.lurkr.gg/v2/levels/${guild}?page=1`);
const data = await res.json();
if (data.message === "Guild no found") {
return [new Error("Server not found in Lurkr"), false];
}
const users = data.levels;

if (users.length === 0) {
return [new Error("No users found"), false];
}

let pageNumber = 2;
// this is needed because Lurkr doesn't give us the total amount of pages
// eslint-disable-next-line no-constant-condition
while (true) {
const res = await fetch(`https://api.lurkr.gg/v2/levels/${guild}?page=${pageNumber}`);
const data = await res.json();
users.push(...data.levels);
if (data.levels.length < 100) break;
pageNumber += 1;
}

try {
for (const user of users) {
const xpValue = user.xp;
const level = Math.floor(Math.sqrt(user.xp / 100));
const nextLevel = level + 1;
const nextLevelXp = Math.pow(nextLevel, 2) * 100;
const xpNeededForNextLevel = nextLevelXp - user.xp;
const currentLevelXp = Math.pow(level, 2) * 100;
const progressToNextLevel =
((user.xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;

await new Promise((resolve, reject) => {
pool.query(
`INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
user.userId,
guild,
xpValue,
`https://cdn.discordapp.com/avatars/${user.userId}/${user.user.avatar}.webp`,
user.user.username,
user.user.username,
level,
xpNeededForNextLevel,
progressToNextLevel.toFixed(2),
],
(err) => {
if (err) {
console.error("Error syncing from Lurkr:", err);
reject(err);
} else {
resolve(null);
}
},
);
});
}
return [null, true]
} catch (err) {
return [err, false];
}
}
//#endregion
61 changes: 60 additions & 1 deletion bot/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import client from '.';
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type CommandInteraction, ChannelType, type APIApplicationCommandOption, GuildMember, AttachmentBuilder, ComponentType } from 'discord.js';
import { heapStats } from 'bun:jsc';
import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel, setXP, setLevel } from './utils/requestAPI';
import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel, setXP, setLevel, syncFromBot } from './utils/requestAPI';
import convertToLevels from './utils/convertToLevels';
import quickEmbed from './utils/quickEmbed';
import { Font, RankCardBuilder } from 'canvacord';
Expand Down Expand Up @@ -721,6 +721,65 @@ const commands: Record<string, Command> = {
return;
}
}
},
sync: {
data: {
options: [{
name: 'bot',
description: 'Select the bot to sync XP data from',
type: 3,
required: true,
choices: [
{
name: 'Polaris',
value: 'polaris',
},
{
name: 'MEE6',
value: 'mee6',
},
{
name: 'Lurkr',
value: 'lurkr',
},
]
}],
name: 'sync',
description: 'Sync XP data from another bot!',
integration_types: [0],
contexts: [0, 2],
},
execute: async (interaction) => {
if (!interaction.memberPermissions?.has('ManageGuild')) {
const errorEmbed = quickEmbed({
color: 'Red',
title: 'Error!',
description: 'Missing permissions: `Manage Server`'
}, interaction);
await interaction.reply({
ephemeral: true,
embeds: [errorEmbed]
})
.catch(console.error);
return;
}

const bot = interaction.options.get('bot')?.value;
const formattedBotNames = {
'polaris': 'Polaris',
'mee6': 'MEE6',
'lurkr': 'Lurkr'
};

await interaction.reply({ ephemeral: true, content: `Syncing data from ${formattedBotNames[bot as keyof typeof formattedBotNames]}...` });
const apiSuccess = await syncFromBot(interaction.guildId as string, bot as string);
if (!apiSuccess) {
await interaction.editReply({ content: `Error syncing data! This might mean that ${formattedBotNames[bot as keyof typeof formattedBotNames]} is not set up for this server, or the leaderboard for this server is not public.` });
return;
}
await interaction.editReply({ content: 'Data synced!' });
return;
}
}
};

Expand Down
14 changes: 14 additions & 0 deletions bot/utils/requestAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,17 @@ export async function setCooldown(guild: string, cooldown: number) {
return response.status === 200;
}
//#endregion

//#region Sync
export async function syncFromBot(guild: string, bot: string) {
const response = await fetch(`http://localhost:18103/admin/sync/${guild}/${bot}`, {
"headers": {
'Content-Type': 'application/json',
'Authorization': process.env.AUTH as string,
},
"body": JSON.stringify({}),
"method": "POST"
});
return response.status === 200;
}
//#endregion
Binary file modified bun.lockb
100644 → 100755
Binary file not shown.