Files
void-sentinel/web/lib/discord.ts
2026-01-04 02:38:34 +05:30

490 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: JSON.stringify({ guild_ids: guildIds }),
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);
// Use JSON.stringify which matches the backend expectation of strings
const body = JSON.stringify({ guild_ids: guildIds });
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" };
}
}