492 lines
16 KiB
TypeScript
492 lines
16 KiB
TypeScript
export interface Guild {
|
|
id: string;
|
|
name: string;
|
|
icon: string | null;
|
|
owner: boolean;
|
|
permissions: string;
|
|
features: string[];
|
|
botInGuild?: boolean;
|
|
isBetaServer?: boolean;
|
|
}
|
|
|
|
|
|
export interface LeaderboardMember {
|
|
user_id: string;
|
|
username: string;
|
|
avatar: string;
|
|
level: number;
|
|
xp: number;
|
|
rank: number;
|
|
}
|
|
|
|
export interface Role {
|
|
role_id: string;
|
|
role_name: string;
|
|
color: number;
|
|
position: number;
|
|
}
|
|
|
|
export interface TrackRole {
|
|
role_name: string;
|
|
role_id: string; // Changed from id to role_id to match backend
|
|
level: number;
|
|
}
|
|
|
|
export interface LevelTrack {
|
|
[trackName: string]: TrackRole[];
|
|
}
|
|
|
|
// ... (previous interfaces)
|
|
|
|
export interface LevelBridge {
|
|
in_role_id: string;
|
|
out_role_id: string;
|
|
}
|
|
|
|
export interface LevelTracksResponse {
|
|
tracks: LevelTrack[];
|
|
}
|
|
|
|
export async function getGuildLeaderboard(guildId: string): Promise<LeaderboardMember[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return [];
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/leaderboard`, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string
|
|
},
|
|
next: { revalidate: 60 }
|
|
});
|
|
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
console.error(`Fetch Leaderboard failed: ${response.status} ${response.statusText}`);
|
|
return [];
|
|
} catch (e) {
|
|
console.error("Failed to fetch leaderboard", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function checkBotMembership(guildIds: string[]): Promise<boolean[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return guildIds.map(() => false);
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/is_bot_there`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string
|
|
},
|
|
body: `{"guild_ids": [${guildIds.join(",")}]}`,
|
|
next: { revalidate: 0 } // Don't cache this as it might change
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.results;
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error(`CheckBotMembership failed: ${response.status} ${response.statusText}`, errorText);
|
|
}
|
|
return guildIds.map(() => false);
|
|
} catch (e) {
|
|
console.error("Failed to check bot membership", e);
|
|
return guildIds.map(() => false);
|
|
}
|
|
}
|
|
|
|
export async function checkBetaServer(guildIds: string[]): Promise<boolean[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return guildIds.map(() => false);
|
|
|
|
// Helper to ensure IDs are numeric strings to prevent injection if we are doing manual JSON
|
|
const safeGuildIds = guildIds.filter(id => /^\d+$/.test(id));
|
|
|
|
if (safeGuildIds.length === 0) return guildIds.map(() => false);
|
|
|
|
// Manually construct JSON to preserve u64 precision (native JS numbers lose precision > 2^53)
|
|
// We want: { "guild_ids": [123, 456] }
|
|
// checkBotMembership also does this manual construction for the same reason.
|
|
const body = `{"guild_ids": [${safeGuildIds.join(",")}]}`;
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/is_beta_server`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string
|
|
},
|
|
body: body,
|
|
next: { revalidate: 60 }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.results;
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error(`CheckBetaServer failed: ${response.status} ${response.statusText}`, errorText);
|
|
}
|
|
return guildIds.map(() => false);
|
|
} catch (e) {
|
|
console.error("Failed to check beta server", e);
|
|
return guildIds.map(() => false);
|
|
}
|
|
}
|
|
|
|
export async function getUserGuilds(accessToken: string): Promise<Guild[]> {
|
|
try {
|
|
const response = await fetch("https://discord.com/api/users/@me/guilds", {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
next: { revalidate: 60 } // Cache for 60 seconds
|
|
});
|
|
|
|
if (response.ok) {
|
|
const guilds: Guild[] = await response.json();
|
|
// Filter: Manage Guild (0x20) or Administrator (0x8)
|
|
const managedGuilds = guilds.filter((guild: any) => {
|
|
const perms = BigInt(guild.permissions);
|
|
const MANAGE_GUILD = BigInt(0x20);
|
|
const ADMINISTRATOR = BigInt(0x8);
|
|
return (perms & MANAGE_GUILD) === MANAGE_GUILD || (perms & ADMINISTRATOR) === ADMINISTRATOR;
|
|
});
|
|
|
|
// Check if bot is in these guilds and if they are beta servers
|
|
const guildIds = managedGuilds.map(g => g.id);
|
|
if (guildIds.length > 0) {
|
|
const [memberships, betaStatuses] = await Promise.all([
|
|
checkBotMembership(guildIds),
|
|
checkBetaServer(guildIds)
|
|
]);
|
|
|
|
return managedGuilds.map((guild, index) => ({
|
|
...guild,
|
|
botInGuild: memberships[index] || false,
|
|
isBetaServer: betaStatuses[index] || false
|
|
}));
|
|
}
|
|
|
|
return managedGuilds;
|
|
}
|
|
return [];
|
|
} catch (e) {
|
|
console.error("Failed to fetch guilds", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getGuildRoles(guildId: string, userToken?: string): Promise<Role[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return [];
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string
|
|
};
|
|
if (userToken) {
|
|
headers["X-User-Token"] = userToken;
|
|
}
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/roles`, {
|
|
method: "GET",
|
|
headers,
|
|
next: { revalidate: 60 }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// Transform API response: { "0": { role_id, role_name, color, position }, ... }
|
|
// to array of Role: { role_id, role_name, color, position }
|
|
if (data && typeof data === 'object') {
|
|
return Object.values(data).map((role: any) => ({
|
|
role_id: role.role_id || role.id,
|
|
role_name: role.role_name || role.name,
|
|
color: role.color ?? 0,
|
|
position: role.position ?? 0
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
console.error(`Fetch Guild Roles failed: ${response.status} ${response.statusText}`);
|
|
return [];
|
|
} catch (e) {
|
|
console.error("Failed to fetch guild roles", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getLevelTracks(guildId: string, userToken?: string): Promise<LevelTrack[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return [];
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string
|
|
};
|
|
if (userToken) {
|
|
headers["X-User-Token"] = userToken;
|
|
}
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
|
method: "GET",
|
|
headers,
|
|
next: { revalidate: 0 } // Don't cache as this is frequently edited
|
|
});
|
|
|
|
if (response.ok) {
|
|
// WORKAROUND: Handle u64 precision loss (JS Numbers are f64)
|
|
// Backend sends role_id as u64 (might be > 2^53).
|
|
// We intercept the raw text and wrap role_id numeric values in quotes before parsing.
|
|
const text = await response.text();
|
|
const safeText = text.replace(/"role_id":\s*(\d+)/g, '"role_id": "$1"');
|
|
return JSON.parse(safeText);
|
|
}
|
|
console.error(`Fetch Level Tracks failed: ${response.status} ${response.statusText}`);
|
|
return [];
|
|
} catch (e) {
|
|
console.error("Failed to fetch level tracks", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Type for role in track API
|
|
export interface TrackRoleUpdate {
|
|
role_id: string; // Changed to string to handle u64 precision on frontend
|
|
level: number;
|
|
}
|
|
|
|
// Create a new track (POST)
|
|
export async function createLevelTrack(
|
|
guildId: string,
|
|
data: { track_name: string; roles: TrackRoleUpdate[] },
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
|
const bodyJson = JSON.stringify(data);
|
|
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: bodyJson
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to create level track", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|
|
|
|
// Update existing track (PATCH)
|
|
export async function updateLevelTrack(
|
|
guildId: string,
|
|
data: { track_name: string; roles: TrackRoleUpdate[] },
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
|
const bodyJson = JSON.stringify(data);
|
|
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: bodyJson
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to update level track", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|
|
|
|
// Delete a track (DELETE)
|
|
export async function deleteLevelTrack(
|
|
guildId: string,
|
|
trackName: string,
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: JSON.stringify({ track_name: trackName })
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to delete level track", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|
|
// ... (previous code)
|
|
|
|
// Level Bridger API Helpers
|
|
|
|
export async function getLevelBridges(guildId: string, userToken: string): Promise<LevelBridge[]> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return [];
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
};
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
|
method: "GET",
|
|
headers,
|
|
next: { revalidate: 0 }
|
|
});
|
|
|
|
if (response.ok) {
|
|
// WORKAROUND: Handle u64 precision loss if needed, though usually these simple objects survive better
|
|
// But let's be safe like we were with tracks if the backend sends huge numbers.
|
|
const text = await response.text();
|
|
// Regex to safely wrap role_id's if they come as raw numbers
|
|
const safeText = text.replace(/("in_role_id"|"out_role_id"):\s*(\d+)/g, '$1: "$2"');
|
|
return JSON.parse(safeText);
|
|
}
|
|
console.error(`Fetch Level Bridges failed: ${response.status} ${response.statusText}`);
|
|
return [];
|
|
} catch (e) {
|
|
console.error("Failed to fetch level bridges", e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function createLevelBridge(
|
|
guildId: string,
|
|
bridge: LevelBridge,
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
|
const bodyJson = JSON.stringify(bridge);
|
|
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: bodyJson
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to create level bridge", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|
|
|
|
export async function updateLevelBridge(
|
|
guildId: string,
|
|
bridge: LevelBridge,
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
const bodyJson = JSON.stringify(bridge);
|
|
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: bodyJson
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to update level bridge", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|
|
|
|
export async function deleteLevelBridge(
|
|
guildId: string,
|
|
inRoleId: string,
|
|
userToken: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
|
|
|
const bodyJson = JSON.stringify({ in_role_id: inRoleId });
|
|
|
|
|
|
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-API-Key": process.env.BOT_API_KEY as string,
|
|
"X-User-Token": userToken
|
|
},
|
|
body: bodyJson
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
const errorText = await response.text();
|
|
return { success: false, error: errorText || `Failed with status ${response.status}` };
|
|
} catch (e) {
|
|
console.error("Failed to delete level bridge", e);
|
|
return { success: false, error: "Internal error" };
|
|
}
|
|
}
|