joel behöver en python
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.13",
|
"@ai-sdk/openai": "^0.0.13",
|
||||||
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@discordjs/opus": "^0.10.0",
|
"@discordjs/opus": "^0.10.0",
|
||||||
"@discordjs/voice": "^0.18.0",
|
"@discordjs/voice": "^0.18.0",
|
||||||
"@elysiajs/cors": "^1.4.0",
|
"@elysiajs/cors": "^1.4.0",
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { getRandomMention, getRandomMemberMention } from "./mentions";
|
|||||||
export { startSpontaneousMentionsCron, stopSpontaneousMentionsCron } from "./spontaneous-cron";
|
export { startSpontaneousMentionsCron, stopSpontaneousMentionsCron } from "./spontaneous-cron";
|
||||||
export { TypingIndicator } from "./typing";
|
export { TypingIndicator } from "./typing";
|
||||||
export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
|
export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
|
||||||
|
export { logVoiceDependencyHealth, speakVoiceover } from "./voice";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource,
|
createAudioResource,
|
||||||
entersState,
|
entersState,
|
||||||
|
generateDependencyReport,
|
||||||
getVoiceConnection,
|
getVoiceConnection,
|
||||||
joinVoiceChannel,
|
joinVoiceChannel,
|
||||||
StreamType,
|
StreamType,
|
||||||
@@ -25,11 +26,89 @@ const logger = createLogger("Features:Joel:Voice");
|
|||||||
const MAX_VOICE_TEXT_LENGTH = 800;
|
const MAX_VOICE_TEXT_LENGTH = 800;
|
||||||
const PLAYBACK_TIMEOUT_MS = 60_000;
|
const PLAYBACK_TIMEOUT_MS = 60_000;
|
||||||
const READY_TIMEOUT_MS = 15_000;
|
const READY_TIMEOUT_MS = 15_000;
|
||||||
|
const VOICE_DEPENDENCY_REPORT = generateDependencyReport();
|
||||||
|
|
||||||
|
type VoiceDependencyHealth = {
|
||||||
|
hasEncryptionLibrary: boolean;
|
||||||
|
hasFfmpeg: boolean;
|
||||||
|
hasOpusLibrary: boolean;
|
||||||
|
report: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VoiceConnectionResult = {
|
||||||
|
channelId: string | null;
|
||||||
|
connection: VoiceConnection | null;
|
||||||
|
skipReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VoicePlaybackEvent = {
|
||||||
|
authorId: string;
|
||||||
|
audioBytes?: number;
|
||||||
|
channelId: string | null;
|
||||||
|
connectionStatus?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
guildId: string;
|
||||||
|
outcome: "skipped" | "success" | "error";
|
||||||
|
playerStarted: boolean;
|
||||||
|
skipReason?: string;
|
||||||
|
textLength: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractDependencySection(startHeading: string, endHeading?: string): string {
|
||||||
|
const startToken = `${startHeading}\n`;
|
||||||
|
const startIndex = VOICE_DEPENDENCY_REPORT.indexOf(startToken);
|
||||||
|
if (startIndex === -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionStart = startIndex + startToken.length;
|
||||||
|
const endIndex = endHeading
|
||||||
|
? VOICE_DEPENDENCY_REPORT.indexOf(`\n${endHeading}`, sectionStart)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
return VOICE_DEPENDENCY_REPORT
|
||||||
|
.slice(sectionStart, endIndex === -1 ? undefined : endIndex)
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInstalledDependency(section: string): boolean {
|
||||||
|
return section
|
||||||
|
.split("\n")
|
||||||
|
.some((line) => line.trim().startsWith("-") && !line.includes("not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVoiceDependencyHealth(): VoiceDependencyHealth {
|
||||||
|
const opusSection = extractDependencySection("Opus Libraries", "Encryption Libraries");
|
||||||
|
const encryptionSection = extractDependencySection("Encryption Libraries", "FFmpeg");
|
||||||
|
|
||||||
|
const hasOpusLibrary = hasInstalledDependency(opusSection);
|
||||||
|
const hasEncryptionLibrary = hasInstalledDependency(encryptionSection);
|
||||||
|
const hasFfmpeg = /FFmpeg[\s\S]*- version:\s+(?!not found)/.test(VOICE_DEPENDENCY_REPORT)
|
||||||
|
&& VOICE_DEPENDENCY_REPORT.includes("- libopus: yes");
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasEncryptionLibrary,
|
||||||
|
hasFfmpeg,
|
||||||
|
hasOpusLibrary,
|
||||||
|
report: VOICE_DEPENDENCY_REPORT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const voiceDependencyHealth = getVoiceDependencyHealth();
|
||||||
|
|
||||||
function isAbortError(error: unknown): boolean {
|
function isAbortError(error: unknown): boolean {
|
||||||
return error instanceof Error && error.name === "AbortError";
|
return error instanceof Error && error.name === "AbortError";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof error === "string" ? error : "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
function resolveMentions(message: Message<true>, content: string): string {
|
function resolveMentions(message: Message<true>, content: string): string {
|
||||||
let text = content;
|
let text = content;
|
||||||
|
|
||||||
@@ -88,14 +167,18 @@ function attachConnectionLogging(connection: VoiceConnection, guildId: string, c
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrCreateConnection(message: Message<true>) {
|
async function getOrCreateConnection(message: Message<true>): Promise<VoiceConnectionResult> {
|
||||||
const voiceChannel = message.member?.voice.channel;
|
const voiceChannel = message.member?.voice.channel;
|
||||||
if (!voiceChannel) {
|
if (!voiceChannel) {
|
||||||
logger.debug("No voice channel for author", {
|
logger.debug("No voice channel for author", {
|
||||||
userId: message.author.id,
|
userId: message.author.id,
|
||||||
guildId: message.guildId,
|
guildId: message.guildId,
|
||||||
});
|
});
|
||||||
return null;
|
return {
|
||||||
|
channelId: null,
|
||||||
|
connection: null,
|
||||||
|
skipReason: "author_not_in_voice_channel",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const me = message.guild.members.me ?? (await message.guild.members.fetchMe());
|
const me = message.guild.members.me ?? (await message.guild.members.fetchMe());
|
||||||
@@ -105,7 +188,11 @@ async function getOrCreateConnection(message: Message<true>) {
|
|||||||
guildId: message.guildId,
|
guildId: message.guildId,
|
||||||
channelId: voiceChannel.id,
|
channelId: voiceChannel.id,
|
||||||
});
|
});
|
||||||
return null;
|
return {
|
||||||
|
channelId: voiceChannel.id,
|
||||||
|
connection: null,
|
||||||
|
skipReason: "missing_connect_or_speak_permission",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = getVoiceConnection(message.guildId);
|
const existing = getVoiceConnection(message.guildId);
|
||||||
@@ -114,7 +201,10 @@ async function getOrCreateConnection(message: Message<true>) {
|
|||||||
guildId: message.guildId,
|
guildId: message.guildId,
|
||||||
channelId: voiceChannel.id,
|
channelId: voiceChannel.id,
|
||||||
});
|
});
|
||||||
return existing;
|
return {
|
||||||
|
channelId: voiceChannel.id,
|
||||||
|
connection: existing,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -140,7 +230,10 @@ async function getOrCreateConnection(message: Message<true>) {
|
|||||||
guildId: message.guildId,
|
guildId: message.guildId,
|
||||||
channelId: voiceChannel.id,
|
channelId: voiceChannel.id,
|
||||||
});
|
});
|
||||||
return connection;
|
return {
|
||||||
|
channelId: voiceChannel.id,
|
||||||
|
connection,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isAbortError(error)) {
|
if (isAbortError(error)) {
|
||||||
logger.debug("Voice connection ready timeout", {
|
logger.debug("Voice connection ready timeout", {
|
||||||
@@ -152,41 +245,103 @@ async function getOrCreateConnection(message: Message<true>) {
|
|||||||
logger.error("Voice connection failed to become ready", error);
|
logger.error("Voice connection failed to become ready", error);
|
||||||
}
|
}
|
||||||
connection.destroy();
|
connection.destroy();
|
||||||
return null;
|
return {
|
||||||
|
channelId: voiceChannel.id,
|
||||||
|
connection: null,
|
||||||
|
skipReason: isAbortError(error) ? "voice_connection_ready_timeout" : "voice_connection_failed",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logVoiceDependencyHealth(): void {
|
||||||
|
const payload = {
|
||||||
|
hasEncryptionLibrary: voiceDependencyHealth.hasEncryptionLibrary,
|
||||||
|
hasFfmpeg: voiceDependencyHealth.hasFfmpeg,
|
||||||
|
hasOpusLibrary: voiceDependencyHealth.hasOpusLibrary,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (voiceDependencyHealth.hasEncryptionLibrary && voiceDependencyHealth.hasFfmpeg && voiceDependencyHealth.hasOpusLibrary) {
|
||||||
|
logger.info("Discord voice dependency health", payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("Discord voice dependency health degraded", {
|
||||||
|
...payload,
|
||||||
|
report: voiceDependencyHealth.report,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function speakVoiceover(message: Message<true>, content: string): Promise<void> {
|
export async function speakVoiceover(message: Message<true>, content: string): Promise<void> {
|
||||||
|
const playbackEvent: VoicePlaybackEvent = {
|
||||||
|
authorId: message.author.id,
|
||||||
|
channelId: null,
|
||||||
|
guildId: message.guildId,
|
||||||
|
outcome: "skipped",
|
||||||
|
playerStarted: false,
|
||||||
|
textLength: 0,
|
||||||
|
};
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
if (!config.elevenlabs.apiKey || !config.elevenlabs.voiceId) {
|
if (!config.elevenlabs.apiKey || !config.elevenlabs.voiceId) {
|
||||||
logger.debug("Voiceover disabled (missing config)");
|
logger.debug("Voiceover disabled (missing config)");
|
||||||
|
playbackEvent.skipReason = "missing_elevenlabs_config";
|
||||||
|
playbackEvent.durationMs = Date.now() - startedAt;
|
||||||
|
logger.info("Voice playback", playbackEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!voiceDependencyHealth.hasEncryptionLibrary) {
|
||||||
|
playbackEvent.skipReason = "missing_voice_encryption_dependency";
|
||||||
|
playbackEvent.durationMs = Date.now() - startedAt;
|
||||||
|
logger.warn("Voice playback skipped", {
|
||||||
|
...playbackEvent,
|
||||||
|
dependencyReport: voiceDependencyHealth.report,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = sanitizeForVoiceover(message, content);
|
const text = sanitizeForVoiceover(message, content);
|
||||||
|
playbackEvent.textLength = text.length;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
logger.debug("Voiceover skipped (empty text after sanitize)");
|
logger.debug("Voiceover skipped (empty text after sanitize)");
|
||||||
|
playbackEvent.skipReason = "empty_text_after_sanitize";
|
||||||
|
playbackEvent.durationMs = Date.now() - startedAt;
|
||||||
|
logger.info("Voice playback", playbackEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let connection: VoiceConnection | null = null;
|
let connection: VoiceConnection | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
connection = await getOrCreateConnection(message);
|
const connectionResult = await getOrCreateConnection(message);
|
||||||
|
playbackEvent.channelId = connectionResult.channelId;
|
||||||
|
connection = connectionResult.connection;
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
logger.debug("Voiceover skipped (no connection)", {
|
logger.debug("Voiceover skipped (no connection)", {
|
||||||
guildId: message.guildId,
|
guildId: message.guildId,
|
||||||
authorId: message.author.id,
|
authorId: message.author.id,
|
||||||
|
skipReason: connectionResult.skipReason,
|
||||||
});
|
});
|
||||||
|
playbackEvent.skipReason = connectionResult.skipReason ?? "no_connection";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Voice playback started", {
|
||||||
|
authorId: message.author.id,
|
||||||
|
channelId: playbackEvent.channelId,
|
||||||
|
guildId: message.guildId,
|
||||||
|
textLength: playbackEvent.textLength,
|
||||||
|
});
|
||||||
|
|
||||||
const voiceover = getVoiceoverService();
|
const voiceover = getVoiceoverService();
|
||||||
logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length });
|
logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length });
|
||||||
const audio = await voiceover.generate({ text });
|
const audio = await voiceover.generate({ text });
|
||||||
logger.debug("Voiceover audio received", { bytes: audio.length });
|
logger.debug("Voiceover audio received", { bytes: audio.length });
|
||||||
|
playbackEvent.audioBytes = audio.length;
|
||||||
|
|
||||||
const player = createAudioPlayer();
|
const player = createAudioPlayer();
|
||||||
const resource = createAudioResource(Readable.from(audio), {
|
const resource = createAudioResource(Readable.from([audio]), {
|
||||||
inputType: StreamType.Arbitrary,
|
inputType: StreamType.Arbitrary,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +351,7 @@ export async function speakVoiceover(message: Message<true>, content: string): P
|
|||||||
|
|
||||||
player.on(AudioPlayerStatus.Playing, () => {
|
player.on(AudioPlayerStatus.Playing, () => {
|
||||||
logger.debug("Audio player started", { guildId: message.guildId });
|
logger.debug("Audio player started", { guildId: message.guildId });
|
||||||
|
playbackEvent.playerStarted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
player.on(AudioPlayerStatus.Idle, () => {
|
player.on(AudioPlayerStatus.Idle, () => {
|
||||||
@@ -205,13 +361,33 @@ export async function speakVoiceover(message: Message<true>, content: string): P
|
|||||||
connection.subscribe(player);
|
connection.subscribe(player);
|
||||||
player.play(resource);
|
player.play(resource);
|
||||||
|
|
||||||
await entersState(player, AudioPlayerStatus.Playing, 5_000).catch(() => undefined);
|
const playingState = await entersState(player, AudioPlayerStatus.Playing, 5_000).catch(() => undefined);
|
||||||
|
if (!playingState) {
|
||||||
|
logger.warn("Voice playback did not enter playing state", {
|
||||||
|
authorId: message.author.id,
|
||||||
|
channelId: playbackEvent.channelId,
|
||||||
|
guildId: message.guildId,
|
||||||
|
playerStarted: playbackEvent.playerStarted,
|
||||||
|
});
|
||||||
|
}
|
||||||
await entersState(player, AudioPlayerStatus.Idle, PLAYBACK_TIMEOUT_MS);
|
await entersState(player, AudioPlayerStatus.Idle, PLAYBACK_TIMEOUT_MS);
|
||||||
|
playbackEvent.connectionStatus = connection.state.status;
|
||||||
|
playbackEvent.outcome = "success";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
playbackEvent.connectionStatus = connection?.state.status;
|
||||||
|
playbackEvent.errorMessage = getErrorMessage(error);
|
||||||
|
playbackEvent.outcome = "error";
|
||||||
if (!isAbortError(error)) {
|
if (!isAbortError(error)) {
|
||||||
logger.error("Voiceover playback failed", error);
|
logger.error("Voiceover playback failed", error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
playbackEvent.durationMs = Date.now() - startedAt;
|
||||||
|
if (playbackEvent.outcome === "error") {
|
||||||
|
logger.warn("Voice playback", playbackEvent);
|
||||||
|
} else {
|
||||||
|
logger.info("Voice playback", playbackEvent);
|
||||||
|
}
|
||||||
|
|
||||||
if (connection && connection.state.status !== VoiceConnectionStatus.Destroyed) {
|
if (connection && connection.state.status !== VoiceConnectionStatus.Destroyed) {
|
||||||
connection.destroy();
|
connection.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { BotClient } from "./core/client";
|
|||||||
import { config } from "./core/config";
|
import { config } from "./core/config";
|
||||||
import { createLogger } from "./core/logger";
|
import { createLogger } from "./core/logger";
|
||||||
import { registerEvents } from "./events";
|
import { registerEvents } from "./events";
|
||||||
import { stopSpontaneousMentionsCron } from "./features/joel";
|
import { logVoiceDependencyHealth, stopSpontaneousMentionsCron } from "./features/joel";
|
||||||
import { buildWebCss, startWebCssWatcher, startWebServer } from "./web";
|
import { buildWebCss, startWebCssWatcher, startWebServer } from "./web";
|
||||||
import { runMigrations } from "./database/migrate";
|
import { runMigrations } from "./database/migrate";
|
||||||
import type { FSWatcher } from "fs";
|
import type { FSWatcher } from "fs";
|
||||||
@@ -47,6 +47,7 @@ async function main(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
logVoiceDependencyHealth();
|
||||||
|
|
||||||
await client.login(config.discord.token);
|
await client.login(config.discord.token);
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ const logger = createLogger("Web");
|
|||||||
const pendingStates = new Map<string, { createdAt: number }>();
|
const pendingStates = new Map<string, { createdAt: number }>();
|
||||||
const STATE_EXPIRY_MS = 5 * 60 * 1000;
|
const STATE_EXPIRY_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof error === "string" ? error : "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll("\"", """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
export function createWebServer(client: BotClient) {
|
export function createWebServer(client: BotClient) {
|
||||||
return new Elysia()
|
return new Elysia()
|
||||||
.use(html())
|
.use(html())
|
||||||
@@ -113,8 +130,16 @@ export function createWebServer(client: BotClient) {
|
|||||||
|
|
||||||
return new Response(null, { status: 302, headers });
|
return new Response(null, { status: 302, headers });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("OAuth callback failed", err);
|
const errorMessage = getErrorMessage(err);
|
||||||
return htmlResponse("<h1>Authentication failed</h1>", 500);
|
logger.error("OAuth callback failed", {
|
||||||
|
codePresent: !!code,
|
||||||
|
errorMessage,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
return htmlResponse(
|
||||||
|
`<h1>Authentication failed</h1><p>${escapeHtml(errorMessage)}</p>`,
|
||||||
|
500
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/auth/logout", async ({ request }) => {
|
.post("/auth/logout", async ({ request }) => {
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ const userGuildsCache = new Map<string, CacheEntry<DiscordGuild[]>>();
|
|||||||
const inFlightUserRequests = new Map<string, Promise<DiscordUser>>();
|
const inFlightUserRequests = new Map<string, Promise<DiscordUser>>();
|
||||||
const inFlightGuildRequests = new Map<string, Promise<DiscordGuild[]>>();
|
const inFlightGuildRequests = new Map<string, Promise<DiscordGuild[]>>();
|
||||||
|
|
||||||
|
async function throwDiscordApiError(action: string, response: Response): Promise<never> {
|
||||||
|
const bodyText = (await response.text()).slice(0, 500);
|
||||||
|
const detail = bodyText ? ` ${bodyText}` : "";
|
||||||
|
throw new Error(`${action} failed (${response.status} ${response.statusText}).${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
function getFromCache<T>(cache: Map<string, CacheEntry<T>>, key: string): T | null {
|
function getFromCache<T>(cache: Map<string, CacheEntry<T>>, key: string): T | null {
|
||||||
const entry = cache.get(key);
|
const entry = cache.get(key);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -91,7 +97,7 @@ export async function exchangeCode(code: string): Promise<TokenResponse> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to exchange code: ${response.statusText}`);
|
await throwDiscordApiError("OAuth code exchange", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -112,7 +118,7 @@ export async function refreshToken(refreshToken: string): Promise<TokenResponse>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
await throwDiscordApiError("OAuth token refresh", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -137,7 +143,7 @@ export async function getUser(accessToken: string): Promise<DiscordUser> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to get user: ${response.statusText}`);
|
await throwDiscordApiError("Discord get user", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await response.json();
|
const user = await response.json();
|
||||||
@@ -173,7 +179,7 @@ export async function getUserGuilds(accessToken: string): Promise<DiscordGuild[]
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to get user guilds: ${response.statusText}`);
|
await throwDiscordApiError("Discord get user guilds", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const guilds = await response.json();
|
const guilds = await response.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user