89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { NextRequest } from "next/server"
|
|
import crypto from "crypto"
|
|
import { createPost, getPostByGid, updatePost } from "@/lib/db"
|
|
import { cleanTitle, slugify } from "@/lib/slug"
|
|
import { cacheCover } from "@/lib/cover-cache"
|
|
|
|
function verifySignature(bodyText: string, signature: string, secret: string): boolean {
|
|
try {
|
|
const hmac = crypto.createHmac("sha256", secret)
|
|
hmac.update(bodyText)
|
|
const expected = hmac.digest("hex")
|
|
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const rawBody = await req.text()
|
|
const secret = process.env.WEBHOOK_SECRET || process.env.FANSUB_WEBHOOK_SECRET || ""
|
|
|
|
if (secret) {
|
|
const signature = req.headers.get("x-signature-256") || req.headers.get("X-Signature-256") || ""
|
|
if (!signature || !verifySignature(rawBody, signature, secret)) {
|
|
return Response.json({ error: "Unauthorized: invalid signature" }, { status: 401 })
|
|
}
|
|
}
|
|
|
|
const payload = JSON.parse(rawBody)
|
|
|
|
const gid = payload.gid
|
|
const rawTitle = payload.title
|
|
if (!gid || !rawTitle) {
|
|
return Response.json({ error: "Invalid payload: gid and title required" }, { status: 400 })
|
|
}
|
|
|
|
const title = cleanTitle(rawTitle)
|
|
const tags = Array.isArray(payload.tags) ? payload.tags : []
|
|
const numPages = payload.num_pages || payload.pages || 0
|
|
const synopsis = payload.synopsis || ""
|
|
const sourceUrl = payload.url || (payload.source === "nhentai" ? `https://nhentai.net/g/${gid}/` : (payload.source === "ehentai" ? `https://e-hentai.org/g/${gid}/` : undefined))
|
|
|
|
const slug = slugify(title, gid)
|
|
|
|
let post = getPostByGid(gid)
|
|
if (!post) {
|
|
post = createPost({
|
|
gid,
|
|
title,
|
|
title_jpn: payload.title_jpn || undefined,
|
|
artist: payload.artist || undefined,
|
|
parody: payload.parody || undefined,
|
|
tags,
|
|
num_pages: numPages,
|
|
source: payload.source || undefined,
|
|
cover_url: payload.cover_url || undefined,
|
|
url: sourceUrl,
|
|
summary: synopsis || undefined,
|
|
slug,
|
|
published: 1,
|
|
})
|
|
} else {
|
|
post = updatePost(post.id, {
|
|
title,
|
|
title_jpn: payload.title_jpn || post.title_jpn || undefined,
|
|
artist: payload.artist || post.artist || undefined,
|
|
parody: payload.parody || post.parody || undefined,
|
|
tags: tags.length > 0 ? tags : post.tags,
|
|
num_pages: numPages || post.num_pages,
|
|
source: payload.source || post.source || undefined,
|
|
cover_url: payload.cover_url || post.cover_url || undefined,
|
|
url: sourceUrl || post.url || undefined,
|
|
summary: synopsis || post.summary || undefined,
|
|
})!
|
|
}
|
|
|
|
await cacheCover(gid)
|
|
|
|
return Response.json({
|
|
success: true,
|
|
gid,
|
|
post,
|
|
})
|
|
} catch (err) {
|
|
return Response.json({ error: String(err) }, { status: 500 })
|
|
}
|
|
}
|