114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
const MINECRAFT_USERNAME = /^[A-Za-z0-9_]{3,16}$/;
|
|
const MINECRAFT_UUID = /^[0-9a-f]{32}$/i;
|
|
const DISCORD_NICKNAME_LIMIT = 32;
|
|
|
|
export interface JavaProfile {
|
|
uuid: string;
|
|
username: string;
|
|
}
|
|
|
|
export async function lookupJavaProfile(
|
|
username: string,
|
|
request: typeof fetch = fetch,
|
|
): Promise<JavaProfile | null> {
|
|
const candidate = username.trim();
|
|
if (!MINECRAFT_USERNAME.test(candidate)) return null;
|
|
|
|
const response = await request(
|
|
`https://api.mojang.com/users/profiles/minecraft/${encodeURIComponent(candidate)}`,
|
|
{ headers: { accept: "application/json" }, cache: "no-store" },
|
|
);
|
|
|
|
if (response.status === 204 || response.status === 404) return null;
|
|
if (!response.ok) throw new Error(`Mojang profile lookup failed (${response.status})`);
|
|
|
|
const profile: unknown = await response.json();
|
|
if (!profile || typeof profile !== "object") return null;
|
|
const { id, name } = profile as { id?: unknown; name?: unknown };
|
|
if (typeof id !== "string" || !MINECRAFT_UUID.test(id)) return null;
|
|
if (typeof name !== "string" || !MINECRAFT_USERNAME.test(name)) return null;
|
|
|
|
return { uuid: id.toLowerCase(), username: name };
|
|
}
|
|
|
|
export function formatDiscordNickname(firstName: string, minecraftUsername: string) {
|
|
const suffix = ` (${minecraftUsername})`;
|
|
const availableCharacters = DISCORD_NICKNAME_LIMIT - [...suffix].length;
|
|
const shortenedName = [...firstName.trim()].slice(0, Math.max(1, availableCharacters)).join("").trimEnd();
|
|
return `${shortenedName}${suffix}`;
|
|
}
|
|
|
|
export function formatManagedDiscordNickname(
|
|
firstName: string,
|
|
minecraftUsername: string | null,
|
|
) {
|
|
if (minecraftUsername) return formatDiscordNickname(firstName, minecraftUsername);
|
|
return formatDiscordNickname(firstName, "TBD");
|
|
}
|
|
|
|
export interface DiscordGuildIdentity {
|
|
id: string;
|
|
username: string;
|
|
globalName: string | null;
|
|
nickname: string | null;
|
|
}
|
|
|
|
export async function getGuildMemberIdentity(
|
|
input: { guildId: string; discordUserId: string; botToken: string },
|
|
request: typeof fetch = fetch,
|
|
): Promise<DiscordGuildIdentity> {
|
|
const response = await request(
|
|
`https://discord.com/api/v10/guilds/${input.guildId}/members/${input.discordUserId}`,
|
|
{
|
|
headers: {
|
|
authorization: `Bot ${input.botToken}`,
|
|
accept: "application/json",
|
|
},
|
|
cache: "no-store",
|
|
},
|
|
);
|
|
if (!response.ok) throw new Error(`Discord guild member lookup failed (${response.status})`);
|
|
|
|
const payload: unknown = await response.json();
|
|
if (!payload || typeof payload !== "object") throw new Error("Discord guild member lookup returned invalid data");
|
|
const member = payload as { nick?: unknown; user?: unknown };
|
|
if (!member.user || typeof member.user !== "object") throw new Error("Discord guild member lookup omitted user data");
|
|
const user = member.user as { id?: unknown; username?: unknown; global_name?: unknown };
|
|
if (typeof user.id !== "string" || typeof user.username !== "string") {
|
|
throw new Error("Discord guild member lookup returned invalid user data");
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
globalName: typeof user.global_name === "string" ? user.global_name : null,
|
|
nickname: typeof member.nick === "string" ? member.nick : null,
|
|
};
|
|
}
|
|
|
|
export async function updateGuildNickname(
|
|
input: {
|
|
guildId: string;
|
|
discordUserId: string;
|
|
nickname: string;
|
|
botToken: string;
|
|
},
|
|
request: typeof fetch = fetch,
|
|
) {
|
|
const response = await request(
|
|
`https://discord.com/api/v10/guilds/${input.guildId}/members/${input.discordUserId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
authorization: `Bot ${input.botToken}`,
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify({ nick: input.nickname }),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Discord nickname update failed (${response.status})`);
|
|
}
|
|
}
|