104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|||
|
|
import { ARTALK_SERVER, ARTALK_SITE } from "@/lib/config";
|
||
|
|
|
||
|
|
async function getArtalkData(path: string, token: string) {
|
||
|
|
const url = `${ARTALK_SERVER}${path}`;
|
||
|
|
const res = await fetch(url, {
|
||
|
|
headers: { Authorization: `Bearer ${token}` },
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`Artalk API error: ${res.status}`);
|
||
|
|
return res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function deleteArtalkPage(id: number, token: string) {
|
||
|
|
const url = `${ARTALK_SERVER}/api/v2/pages/${id}`;
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: "DELETE",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ site_name: ARTALK_SITE }),
|
||
|
|
});
|
||
|
|
return res.ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function POST(request: Request) {
|
||
|
|
try {
|
||
|
|
const body = await request.json().catch(() => ({}));
|
||
|
|
const token = (body as Record<string, string>).token;
|
||
|
|
if (!token) {
|
||
|
|
return NextResponse.json({ error: "token required" }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. Login to get a valid token
|
||
|
|
const allPages: { id: number; key: string; title: string; comment_count?: number }[] = [];
|
||
|
|
let offset = 0;
|
||
|
|
const limit = 200;
|
||
|
|
|
||
|
|
// Fetch all pages
|
||
|
|
while (true) {
|
||
|
|
const data = await getArtalkData(
|
||
|
|
`/api/v2/pages?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=${limit}&offset=${offset}`,
|
||
|
|
token
|
||
|
|
);
|
||
|
|
const pages = data.pages || data.data || [];
|
||
|
|
if (pages.length === 0) break;
|
||
|
|
allPages.push(...pages);
|
||
|
|
offset += limit;
|
||
|
|
if (pages.length < limit) break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Group by title
|
||
|
|
const byTitle = new Map<string, typeof allPages>();
|
||
|
|
for (const p of allPages) {
|
||
|
|
const t = (p.title || "").trim();
|
||
|
|
if (!byTitle.has(t)) byTitle.set(t, []);
|
||
|
|
byTitle.get(t)!.push(p);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. For each duplicate group, keep the one with most comments, delete the rest (especially 0-comment ones)
|
||
|
|
let deleted = 0;
|
||
|
|
const deletedIds: number[] = [];
|
||
|
|
|
||
|
|
for (const [title, pages] of byTitle) {
|
||
|
|
if (pages.length <= 1) continue;
|
||
|
|
|
||
|
|
// Sort: most comments first, then by id desc (newest first)
|
||
|
|
pages.sort((a, b) => {
|
||
|
|
const ca = a.comment_count ?? 0;
|
||
|
|
const cb = b.comment_count ?? 0;
|
||
|
|
if (ca !== cb) return cb - ca;
|
||
|
|
return b.id - a.id;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Keep the first one, delete the rest
|
||
|
|
const [keep, ...toDelete] = pages;
|
||
|
|
for (const p of toDelete) {
|
||
|
|
try {
|
||
|
|
const ok = await deleteArtalkPage(p.id, token);
|
||
|
|
if (ok) {
|
||
|
|
deleted++;
|
||
|
|
deletedIds.push(p.id);
|
||
|
|
}
|
||
|
|
} catch { /* skip failed deletions */ }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Also clean up pages with 0 comments that are NOT part of duplicate groups
|
||
|
|
// (only if they look like test/404 pages — very short titles or empty keys)
|
||
|
|
// Skip this step to be safe — only deduplicate.
|
||
|
|
|
||
|
|
return NextResponse.json({
|
||
|
|
total: allPages.length,
|
||
|
|
duplicateGroups: [...byTitle.values()].filter((ps) => ps.length > 1).length,
|
||
|
|
deleted,
|
||
|
|
deletedIds,
|
||
|
|
});
|
||
|
|
} catch (error: unknown) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: error instanceof Error ? error.message : "Unknown error" },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|