feat: notificacion automatica en Discord via webhook (nuevos mangas)
This commit is contained in:
@@ -11,6 +11,10 @@ WEB_PASSWORD=
|
|||||||
# Webhook validation
|
# Webhook validation
|
||||||
WEBHOOK_SECRET=
|
WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# Discord notification (opcional: avisa en canal cuando llega un manga nuevo)
|
||||||
|
# Crear en Discord: canal -> Configuración -> Integraciones -> Webhooks
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|
||||||
# DB path
|
# DB path
|
||||||
DB_PATH=/app/data/worst-scan.db
|
DB_PATH=/app/data/worst-scan.db
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import crypto from "crypto"
|
|||||||
import { createPost, getPostByGid, updatePost } from "@/lib/db"
|
import { createPost, getPostByGid, updatePost } from "@/lib/db"
|
||||||
import { cleanTitle, slugify } from "@/lib/slug"
|
import { cleanTitle, slugify } from "@/lib/slug"
|
||||||
import { cacheCover } from "@/lib/cover-cache"
|
import { cacheCover } from "@/lib/cover-cache"
|
||||||
|
import { notifyNewPost } from "@/lib/discord"
|
||||||
|
|
||||||
function verifySignature(bodyText: string, signature: string, secret: string): boolean {
|
function verifySignature(bodyText: string, signature: string, secret: string): boolean {
|
||||||
try {
|
try {
|
||||||
@@ -44,6 +45,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const slug = slugify(title, gid)
|
const slug = slugify(title, gid)
|
||||||
|
|
||||||
let post = getPostByGid(gid)
|
let post = getPostByGid(gid)
|
||||||
|
let isNew = false
|
||||||
if (!post) {
|
if (!post) {
|
||||||
post = createPost({
|
post = createPost({
|
||||||
gid,
|
gid,
|
||||||
@@ -60,6 +62,7 @@ export async function POST(req: NextRequest) {
|
|||||||
slug,
|
slug,
|
||||||
published: 1,
|
published: 1,
|
||||||
})
|
})
|
||||||
|
isNew = true
|
||||||
} else {
|
} else {
|
||||||
post = updatePost(post.id, {
|
post = updatePost(post.id, {
|
||||||
title,
|
title,
|
||||||
@@ -77,6 +80,11 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
await cacheCover(gid)
|
await cacheCover(gid)
|
||||||
|
|
||||||
|
// Notificar en Discord si es un manga nuevo.
|
||||||
|
if (isNew && post) {
|
||||||
|
await notifyNewPost(post).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
gid,
|
gid,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Post } from "./db"
|
||||||
|
|
||||||
|
// Notificación a Discord vía Webhook.
|
||||||
|
// Si DISCORD_WEBHOOK_URL no está configurada, no hace nada (silencioso).
|
||||||
|
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || ""
|
||||||
|
|
||||||
|
export async function notifyNewPost(post: Post): Promise<boolean> {
|
||||||
|
if (!WEBHOOK_URL) return false
|
||||||
|
|
||||||
|
const description = post.summary && !post.summary.startsWith("**") && post.summary.length > 40
|
||||||
|
? post.summary.slice(0, 200)
|
||||||
|
: `${post.num_pages} páginas${post.artist ? ` por ${post.artist}` : ""}`
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
content: `📚 **Nuevo manga disponible:** ${post.title}`,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: post.title,
|
||||||
|
url: `https://worstscan.xyz/p/${post.slug}`,
|
||||||
|
description,
|
||||||
|
color: 0x6366f1, // indigo, acorde al tema
|
||||||
|
thumbnail: { url: `https://worstscan.xyz/api/cover/${post.gid}` },
|
||||||
|
footer: { text: "worst-scan fansub" },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
fields: [
|
||||||
|
...(post.artist ? [{ name: "Artista", value: post.artist, inline: true }] : []),
|
||||||
|
...(post.parody ? [{ name: "Franquicia", value: post.parody, inline: true }] : []),
|
||||||
|
...(post.num_pages > 0 ? [{ name: "Páginas", value: String(post.num_pages), inline: true }] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(WEBHOOK_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal: AbortSignal.timeout(8000),
|
||||||
|
})
|
||||||
|
return res.ok
|
||||||
|
} catch {
|
||||||
|
return false // nunca romper el poller por un webhook caído
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -3,6 +3,7 @@ import { api } from "./api"
|
|||||||
import { cleanTitle, slugify } from "./slug"
|
import { cleanTitle, slugify } from "./slug"
|
||||||
import { createPost, getPostByGid, updatePost } from "./db"
|
import { createPost, getPostByGid, updatePost } from "./db"
|
||||||
import { cacheCover } from "./cover-cache"
|
import { cacheCover } from "./cover-cache"
|
||||||
|
import { notifyNewPost } from "./discord"
|
||||||
|
|
||||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||||
const POLL_INTERVAL_MS = 60_000
|
const POLL_INTERVAL_MS = 60_000
|
||||||
@@ -74,6 +75,11 @@ export async function pollOnce(): Promise<{ newPosts: number }> {
|
|||||||
published: 1,
|
published: 1,
|
||||||
})
|
})
|
||||||
newPosts++
|
newPosts++
|
||||||
|
// Notificar en Discord cuando aparece un manga nuevo.
|
||||||
|
const created = getPostByGid(gid)
|
||||||
|
if (created) {
|
||||||
|
await notifyNewPost(created).catch(() => {})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updatePost(existing.id, {
|
updatePost(existing.id, {
|
||||||
title: cleanT,
|
title: cleanT,
|
||||||
@@ -88,7 +94,6 @@ export async function pollOnce(): Promise<{ newPosts: number }> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = process.env.PORT || "3000"
|
|
||||||
await cacheCover(gid)
|
await cacheCover(gid)
|
||||||
} catch {
|
} catch {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user