diff --git a/.env.example b/.env.example index a3b37f0..2fc0516 100644 --- a/.env.example +++ b/.env.example @@ -8,5 +8,6 @@ WWEBJS_CACHE=./.wwebjs_cache WA_TOKEN= WA_APP_SECRET= WA_PHONE_NUMBER_ID= +WA_WEBHOOK_TOKEN= APP_LOGS_PATH="./.logs" \ No newline at end of file diff --git a/.logs/app.log b/.logs/app.log index 5bb1d99..fe5b5ca 100644 --- a/.logs/app.log +++ b/.logs/app.log @@ -4,3 +4,98 @@ {"level":30,"time":"2025-10-22T04:07:38.479Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} {"level":30,"time":"2025-10-22T04:07:40.637Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} {"level":30,"time":"2025-10-22T04:10:28.223Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:17:44.914Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:18:07.234Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:27:01.449Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:35:33.674Z","pid":87670,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:35:51.898Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:35:53.196Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:35:54.357Z","pid":87915,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:27.379Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:29.792Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:34.987Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:36.275Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:36.275Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:41.043Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:41.043Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:44.457Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:44.457Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:49.453Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:49.454Z","pid":64348,"hostname":"air-malik","msg":"WA API ini"} +{"level":30,"time":"2025-10-22T04:36:51.173Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:36:51.173Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:37:48.974Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:37:48.974Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:39:29.849Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:39:29.849Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:39:35.135Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:39:35.135Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:39:38.554Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:39:38.554Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:46:20.635Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:46:20.637Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:46:49.278Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:46:58.002Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:46:58.002Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:47:05.377Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:47:05.377Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:47:31.502Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:47:31.503Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:28.289Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:28.290Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:31.061Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:31.062Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:32.485Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:32.488Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:40.362Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:40.364Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:41.783Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:41.784Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:45.292Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:45.293Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:48.125Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:48.126Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:49.248Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:49.249Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:51.666Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:51.668Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:54.594Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:54.595Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:48:58.386Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:48:58.387Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:16.542Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:16.542Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:23.718Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:23.718Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:36.162Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:36.163Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:39.258Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:39.259Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:52.424Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:52.425Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:53.562Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:53.562Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:49:56.779Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:49:56.779Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:50:00.263Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:50:00.264Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:50:03.569Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:50:03.571Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:50:14.937Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:50:14.939Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:50:38.299Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:50:38.299Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:50:39.719Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:50:39.719Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:00.146Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:00.146Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:02.178Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:02.179Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:10.312Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:10.313Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:12.567Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:12.568Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:13.909Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:13.909Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} +{"level":30,"time":"2025-10-22T04:51:30.228Z","pid":64348,"hostname":"air-malik","msg":"WA API started"} +{"level":30,"time":"2025-10-22T04:51:30.238Z","pid":64348,"hostname":"air-malik","msg":"WA API initialized"} diff --git a/src/server/lib/wa-api/wa-api.ts b/src/server/lib/wa-api/wa-api.ts index e19a035..512cede 100644 --- a/src/server/lib/wa-api/wa-api.ts +++ b/src/server/lib/wa-api/wa-api.ts @@ -11,9 +11,11 @@ const APP_SECRET: string = process.env.WA_APP_SECRET!; logger.info("WA API started"); // Inisialisasi WhatsApp API dengan typing generik jika diperlukan (contoh: number sebagai tipe session) -const Whatsapp = new WhatsAppAPI({ +export const Whatsapp = new WhatsAppAPI({ token: TOKEN, - appSecret: APP_SECRET + appSecret: APP_SECRET, + webhookVerifyToken: process.env.WA_WEBHOOK_TOKEN!, + v: "v23.0" }); // Tipe untuk request body dari server (bisa disesuaikan dengan framework seperti Express, Elysia, Hono, dll) @@ -36,8 +38,10 @@ export async function post(req: PostRequest) { } export function whatsappApiInit() { + logger.info("WA API initialized"); // Handler jika ada pesan masuk dari user Whatsapp.on.message = async ({ phoneID, from, message, name, Whatsapp, reply }) => { + logger.info("WA API received"); logger.info( `User ${name} (${from}) sent to bot ${phoneID} ${JSON.stringify(message)}` ); diff --git a/src/server/routes/logs_route.ts b/src/server/routes/logs_route.ts index 9d386b5..72774c6 100644 --- a/src/server/routes/logs_route.ts +++ b/src/server/routes/logs_route.ts @@ -4,23 +4,73 @@ import path from "path"; const LOGS_PATH = process.env.APP_LOGS_PATH || "./.logs"; -const LogsRoute = new Elysia({ - prefix: "/logs", // lebih aman pakai "/" di depan - tags: ["logs"] -}) - .get("/app", async () => { - const filePath = path.join(LOGS_PATH, "app.log"); +// Pastikan folder log ada saat startup +(async () => { + try { + await fs.access(LOGS_PATH); + } catch { + await fs.mkdir(LOGS_PATH, { recursive: true }); + } +})(); - try { - const logs = await fs.readFile(filePath, "utf-8"); - // Format: array line-by-line (optional) - return logs.split("\n").filter(Boolean); - } catch (err) { +// Helper baca log file + optional filter +async function readLogs(limit?: number, level?: string) { + const filePath = path.join(LOGS_PATH, "app.log"); + + try { + const data = await fs.readFile(filePath, "utf-8"); + let lines = data.trim().split("\n").filter(Boolean); // tiap baris = JSON log + + if (limit) { + lines = lines.slice(-limit); + } + + let parsed = lines.map((line) => { + try { + return JSON.parse(line); + } catch { + return { raw: line }; + } + }); + + // Filter berdasarkan level (error, info, debug, warn) + if (level) { + parsed = parsed.filter((log) => + log.level ? String(log.level) === level || log.level === level : false + ); + } + + return parsed; + } catch { + return null; + } +} + +const LogsRoute = new Elysia({ + prefix: "/logs", + tags: ["logs"], +}) + /** + * GET /logs/app?lines=100&level=error + */ + .get("/app", async ({ query }) => { + const lines = query.lines ? Number(query.lines) : undefined; + const level = query.level || undefined; + + const logs = await readLogs(lines, level); + + if (!logs) { return { - error: "Log file not found or cannot be read", - details: (err as Error).message, + success: false, + message: "Log file not found or unreadable", }; } + + return { + success: true, + total: logs.length, + data: logs, + }; }); export default LogsRoute; diff --git a/src/server/routes/wa_hook_route.ts b/src/server/routes/wa_hook_route.ts index ae6a291..7f7dfde 100644 --- a/src/server/routes/wa_hook_route.ts +++ b/src/server/routes/wa_hook_route.ts @@ -2,9 +2,9 @@ import Elysia, { t } from "elysia"; import { prisma } from "../lib/prisma"; import type { WAHookMessage } from "types/wa_messages"; import _ from "lodash"; -import { whatsappApiInit } from "../lib/wa-api/wa-api"; - -whatsappApiInit() +import { Whatsapp, whatsappApiInit } from "../lib/wa-api/wa-api"; +import type { GetParams, PostData } from "whatsapp-api-js/types"; +import { logger } from "../lib/logger"; async function fetchWithTimeout(input: RequestInfo, init: RequestInit, timeoutMs = 120_000) { const controller = new AbortController() @@ -40,6 +40,7 @@ const WaHookRoute = new Elysia({ }) // ✅ Handle verifikasi Webhook (GET) .get("/hook", async (ctx) => { + Whatsapp.get(ctx.query as GetParams) const { query, set } = ctx; const mode = query["hub.mode"]; const challenge = query["hub.challenge"]; @@ -79,7 +80,8 @@ const WaHookRoute = new Elysia({ // ✅ Handle incoming message (POST) .post("/hook", async ({ body }) => { - console.log("Incoming WhatsApp Webhook:", body); + Whatsapp.post(body as PostData) + logger.info("[POST] Incoming WhatsApp Webhook:", body) const create = await prisma.waHook.create({ data: { @@ -95,7 +97,7 @@ const WaHookRoute = new Elysia({ }) if (!flow) { - console.log("no flow found") + logger.info("[POST] no flow found") } if (flow?.defaultFlow && flow.active) { @@ -205,3 +207,6 @@ const WaHookRoute = new Elysia({ }); export default WaHookRoute; + +// Initialize WhatsApp API +whatsappApiInit() \ No newline at end of file diff --git a/types/env.d.ts b/types/env.d.ts index 245ef48..8d200a6 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -9,6 +9,7 @@ declare namespace NodeJS { WA_TOKEN?: string; WA_APP_SECRET?: string; WA_PHONE_NUMBER_ID?: string; + WA_WEBHOOK_TOKEN?: string; APP_LOGS_PATH?: string; } } diff --git a/xx.ts b/xx.ts index f8046f2..7b138e5 100644 --- a/xx.ts +++ b/xx.ts @@ -1,79 +1,3 @@ -import { WhatsAppAPI } from "whatsapp-api-js"; -import { Document, Image, Text } from "whatsapp-api-js/messages"; -import type { IncomingHttpHeaders } from "http"; +import { whatsappApiInit } from "@/server/lib/wa-api/wa-api"; -// Jangan hardcode — ini hanya contoh -const TOKEN: string = "YOUR_TOKEN"; -const APP_SECRET: string = "YOUR_SECRET"; - -// Inisialisasi WhatsApp API dengan typing generik jika diperlukan (contoh: number sebagai tipe session) -const Whatsapp = new WhatsAppAPI({ - token: TOKEN, - appSecret: APP_SECRET -}); - -// Tipe untuk request body dari server (bisa disesuaikan dengan framework seperti Express, Elysia, Hono, dll) -interface PostRequest { - data: string | Buffer; - headers: IncomingHttpHeaders & { - "x-hub-signature-256"?: string; - }; -} - -// Fungsi handler webhook POST -export async function post(req: PostRequest) { - const signature = req.headers["x-hub-signature-256"] ?? ""; - return await Whatsapp.post( - JSON.parse(req.data.toString()), - req.data.toString(), - signature, - ); -} - -// Handler jika ada pesan masuk dari user -Whatsapp.on.message = async ({ phoneID, from, message, name, Whatsapp, reply }) => { - console.log( - `User ${name} (${from}) sent to bot ${phoneID} ${JSON.stringify(message)}` - ); - - let response; - - switch (message.type) { - case "text": - response = await reply( - new Text(`*${name}* said:\n\n${message.text.body}`), - true - ); - break; - - case "image": - response = await reply( - new Image(message.image.id, true, `Nice photo, ${name}`) - ); - break; - - case "document": - response = await reply( - new Document(message.document.id, true, undefined, "Our document") - ); - break; - - default: - console.log( - "Unhandled message type. More types available: contacts, locations, templates, interactive, reactions, audio, video, etc." - ); - break; - } - - console.log(response); - - // Tandai pesan sudah dibaca - Whatsapp.markAsRead(phoneID, message.id); - - return 200; -}; - -// Handler saat pesan berhasil terkirim -Whatsapp.on.sent = ({ phoneID, to, message }) => { - console.log(`Bot ${phoneID} sent to user ${to} ${message}`); -}; +whatsappApiInit() \ No newline at end of file