refactor: Implement DiscordRateLimiter for API requests to manage rate limits
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import { RowDataPacket } from 'mysql2/promise';
|
import { RowDataPacket } from 'mysql2/promise';
|
||||||
|
import { DiscordRateLimiter } from '@/lib/discordRateLimiter';
|
||||||
|
|
||||||
|
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10'; // Or the appropriate version
|
||||||
|
|
||||||
interface FileId {
|
interface FileId {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,6 +17,8 @@ if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID || !proces
|
|||||||
throw new Error('Discord or Cron secret environment variables are not configured');
|
throw new Error('Discord or Cron secret environment variables are not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimiter = new DiscordRateLimiter(DISCORD_API_BASE_URL, process.env.DISCORD_BOT_TOKEN);
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get('authorization');
|
||||||
@@ -44,19 +49,19 @@ export async function POST(request: Request) {
|
|||||||
if (parts[0].length > 0) {
|
if (parts[0].length > 0) {
|
||||||
for (const part of parts[0] as FilePart[]) {
|
for (const part of parts[0] as FilePart[]) {
|
||||||
if (!part.discord_message_id) continue;
|
if (!part.discord_message_id) continue;
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(deleteUrl, {
|
const res = await rateLimiter.request(
|
||||||
|
`/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`,
|
||||||
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}` },
|
}
|
||||||
});
|
);
|
||||||
if (!res.ok && res.status !== 404) {
|
if (!res.ok && res.status !== 404) {
|
||||||
console.warn(`Cleanup: Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
console.warn(`Cleanup: Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Cleanup: Error deleting message ${part.discord_message_id}:`, e);
|
console.error(`Cleanup: Error deleting message ${part.discord_message_id}:`, e);
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 1s delay
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,19 +88,19 @@ export async function POST(request: Request) {
|
|||||||
if (partsRows[0].length > 0) {
|
if (partsRows[0].length > 0) {
|
||||||
for (const part of partsRows[0] as FilePart[]) {
|
for (const part of partsRows[0] as FilePart[]) {
|
||||||
if (!part.discord_message_id) continue;
|
if (!part.discord_message_id) continue;
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(deleteUrl, {
|
const res = await rateLimiter.request(
|
||||||
|
`/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`,
|
||||||
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}` },
|
}
|
||||||
});
|
);
|
||||||
if (!res.ok && res.status !== 404) {
|
if (!res.ok && res.status !== 404) {
|
||||||
console.warn(`Cleanup: Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
console.warn(`Cleanup: Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Cleanup: Error deleting message ${part.discord_message_id}:`, e);
|
console.error(`Cleanup: Error deleting message ${part.discord_message_id}:`, e);
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 1s delay
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { NextResponse } from 'next/server';
|
|||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { RowDataPacket } from 'mysql2/promise';
|
import { RowDataPacket } from 'mysql2/promise';
|
||||||
|
import { DiscordRateLimiter } from '@/lib/discordRateLimiter';
|
||||||
|
|
||||||
|
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10'; // Or the appropriate version
|
||||||
|
|
||||||
interface FileData extends RowDataPacket {
|
interface FileData extends RowDataPacket {
|
||||||
token_hash: string;
|
token_hash: string;
|
||||||
@@ -15,6 +18,8 @@ if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
|||||||
throw new Error('Discord bot token or channel ID is not configured');
|
throw new Error('Discord bot token or channel ID is not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimiter = new DiscordRateLimiter(DISCORD_API_BASE_URL, process.env.DISCORD_BOT_TOKEN);
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { file_id, token } = await request.json();
|
const { file_id, token } = await request.json();
|
||||||
@@ -48,12 +53,13 @@ export async function POST(request: Request) {
|
|||||||
for (const part of partsRows[0] as FilePart[]) {
|
for (const part of partsRows[0] as FilePart[]) {
|
||||||
if (!part.discord_message_id) continue;
|
if (!part.discord_message_id) continue;
|
||||||
|
|
||||||
const deleteUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(deleteUrl, {
|
const res = await rateLimiter.request(
|
||||||
|
`/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`,
|
||||||
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}` },
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!res.ok && res.status !== 404) { // Don't warn on 404 (already deleted)
|
if (!res.ok && res.status !== 404) { // Don't warn on 404 (already deleted)
|
||||||
console.warn(`Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
console.warn(`Failed to delete message ${part.discord_message_id}. Status: ${res.status}`);
|
||||||
@@ -61,9 +67,6 @@ export async function POST(request: Request) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error deleting message ${part.discord_message_id}:`, e);
|
console.error(`Error deleting message ${part.discord_message_id}:`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait 1 second between delete requests to avoid hitting Discord's rate limits.
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Hard delete the file from the database
|
// 4. Hard delete the file from the database
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { NextResponse, NextRequest } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import { RowDataPacket } from 'mysql2/promise';
|
import { RowDataPacket } from 'mysql2/promise';
|
||||||
|
import { DiscordRateLimiter } from '@/lib/discordRateLimiter';
|
||||||
|
|
||||||
|
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10'; // Or the appropriate version
|
||||||
|
|
||||||
interface FilePart extends RowDataPacket {
|
interface FilePart extends RowDataPacket {
|
||||||
discord_message_id: string | null;
|
discord_message_id: string | null;
|
||||||
@@ -17,6 +20,8 @@ if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
|||||||
throw new Error('Discord bot token or channel ID is not configured');
|
throw new Error('Discord bot token or channel ID is not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimiter = new DiscordRateLimiter(DISCORD_API_BASE_URL, process.env.DISCORD_BOT_TOKEN);
|
||||||
|
|
||||||
export async function GET(request: NextRequest, context: RouteContext) {
|
export async function GET(request: NextRequest, context: RouteContext) {
|
||||||
try {
|
try {
|
||||||
const { file_id, part_index } = await context.params;
|
const { file_id, part_index } = await context.params;
|
||||||
@@ -43,18 +48,10 @@ export async function GET(request: NextRequest, context: RouteContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Get fresh message data from Discord
|
// 1. Get fresh message data from Discord
|
||||||
const messageUrl = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`;
|
const messageData = await rateLimiter.request(
|
||||||
const discordRes = await fetch(messageUrl, {
|
`/channels/${process.env.DISCORD_CHANNEL_ID}/messages/${part.discord_message_id}`
|
||||||
headers: {
|
);
|
||||||
'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!discordRes.ok) {
|
|
||||||
return NextResponse.json({ error: 'Failed to fetch message from Discord' }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageData = await discordRes.json();
|
|
||||||
const attachment = messageData.attachments[0];
|
const attachment = messageData.attachments[0];
|
||||||
|
|
||||||
if (!attachment || !attachment.url) {
|
if (!attachment || !attachment.url) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
|
import { DiscordRateLimiter } from '@/lib/discordRateLimiter';
|
||||||
|
|
||||||
|
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10'; // Or the appropriate version
|
||||||
|
|
||||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||||
throw new Error('Please define DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID in .env.local');
|
throw new Error('Please define DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID in .env.local');
|
||||||
@@ -7,6 +10,8 @@ if (!process.env.DISCORD_BOT_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
|||||||
|
|
||||||
const DISCORD_API_URL = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages`;
|
const DISCORD_API_URL = `https://discord.com/api/v10/channels/${process.env.DISCORD_CHANNEL_ID}/messages`;
|
||||||
|
|
||||||
|
const rateLimiter = new DiscordRateLimiter(DISCORD_API_BASE_URL, process.env.DISCORD_BOT_TOKEN);
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
@@ -22,13 +27,13 @@ export async function POST(request: Request) {
|
|||||||
const discordFormData = new FormData();
|
const discordFormData = new FormData();
|
||||||
discordFormData.append('file', chunk, `chunk-${partIndex}.bin`);
|
discordFormData.append('file', chunk, `chunk-${partIndex}.bin`);
|
||||||
|
|
||||||
const discordRes = await fetch(DISCORD_API_URL, {
|
const discordRes = await rateLimiter.request(
|
||||||
|
`/channels/${process.env.DISCORD_CHANNEL_ID}/messages`,
|
||||||
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Authorization': `Bot ${process.env.DISCORD_BOT_TOKEN}`,
|
|
||||||
},
|
|
||||||
body: discordFormData,
|
body: discordFormData,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!discordRes.ok) {
|
if (!discordRes.ok) {
|
||||||
const errorBody = await discordRes.json();
|
const errorBody = await discordRes.json();
|
||||||
|
|||||||
46
src/lib/discordRateLimiter.ts
Normal file
46
src/lib/discordRateLimiter.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export class DiscordRateLimiter {
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly botToken: string;
|
||||||
|
|
||||||
|
constructor(baseUrl: string, botToken: string) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.botToken = botToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to the Discord API, handling rate limits automatically.
|
||||||
|
* @param path The API endpoint path (e.g., '/channels/123/messages').
|
||||||
|
* @param options Fetch options (method, headers, body, etc.).
|
||||||
|
* @returns The JSON response from the Discord API.
|
||||||
|
*/
|
||||||
|
public async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bot ${this.botToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...options, headers });
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
const retryAfter = response.headers.get('Retry-After');
|
||||||
|
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000; // Default to 1 second if header is missing
|
||||||
|
|
||||||
|
console.warn(`Discord API rate limit hit. Retrying after ${delay / 1000} seconds.`);
|
||||||
|
await sleep(delay);
|
||||||
|
return this.request<T>(path, options); // Retry the request
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
|
throw new Error(`Discord API error: ${response.status} - ${errorData.message || JSON.stringify(errorData)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user