feat: Update webhook and WA services

- Update webhook and WhatsApp related pages and services.
- Clean up temporary files.
This commit is contained in:
bipproduction
2025-11-19 16:24:46 +08:00
parent 65da8c3963
commit d827efe8a3
20 changed files with 267 additions and 991 deletions

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
\n# build artifacts\nseed\nprisma/seed-linux-x64

View File

@@ -2,9 +2,9 @@ import { prisma } from "@/server/lib/prisma";
const user = [
{
name: "Bip",
email: "bip@bip.com",
password: "bip",
name: "wibu",
email: "wibu@bip.com",
password: "Production_123",
}
];

View File

@@ -2,7 +2,7 @@ import Elysia, { t } from "elysia";
import Swagger from "@elysiajs/swagger";
import html from "./index.html";
import Dashboard from "./server/routes/darmasaba";
import apiAuth from "./server/middlewares/apiAuth";
import {apiAuth} from "./server/middlewares/apiAuth";
import Auth from "./server/routes/auth_route";
import ApiKeyRoute from "./server/routes/apikey_route";
import type { User } from "generated/prisma";

View File

@@ -2,6 +2,7 @@ import {
Button,
Container,
Group,
PasswordInput,
Stack,
Text,
TextInput,
@@ -68,7 +69,7 @@ export default function Login() {
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextInput
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}

View File

@@ -116,7 +116,7 @@ export default function WebhookCreate() {
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
w={"100%"}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
@@ -186,7 +186,7 @@ export default function WebhookCreate() {
}}
/>
<Stack gap="xs">
{/* <Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
@@ -204,9 +204,9 @@ export default function WebhookCreate() {
automaticLayout: true,
}}
/>
</Stack>
</Stack> */}
<Stack gap="xs">
{/* <Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
@@ -226,7 +226,7 @@ export default function WebhookCreate() {
automaticLayout: true,
}}
/>
</Stack>
</Stack> */}
<Checkbox
label="Enable Webhook"
@@ -237,7 +237,7 @@ export default function WebhookCreate() {
label: { color: "#EAEAEA" },
}}
/>
<Checkbox
{/* <Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.currentTarget.checked)}
@@ -245,16 +245,16 @@ export default function WebhookCreate() {
styles={{
label: { color: "#EAEAEA" },
}}
/>
<TextInput
/> */}
{/* <TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
/> */}
<Card
{/* <Card
radius="xl"
p="md"
style={{
@@ -281,7 +281,7 @@ export default function WebhookCreate() {
}}
/>
</Stack>
</Card>
</Card> */}
<Group justify="flex-end" mt="md">
<Button

View File

@@ -1,32 +1,24 @@
import useSWR from "swr";
import clientRoutes from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
import { useSearchParams, useNavigate } from "react-router-dom";
import {
Button,
Checkbox,
Divider,
Group,
Select,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { modals } from "@mantine/modals";
import { notifications } from "@mantine/notifications";
import { IconCheck, IconCode, IconX } from "@tabler/icons-react";
import type { WebHook } from "generated/prisma";
import { useState } from "react";
import { useMemo } from "react";
import { notifications } from "@mantine/notifications";
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
import Editor from "@monaco-editor/react";
import {
Stack,
Group,
Title,
Divider,
TextInput,
Select,
Checkbox,
Card,
Button,
Text,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import clientRoutes from "@/clientRoutes";
import { useShallowEffect } from "@mantine/hooks";
const templateData = `
Available variables:
{{data.from}}, {{data.fromNumber}}, {{data.fromMe}}, {{data.body}}, {{data.hasMedia}}, {{data.type}}, {{data.to}}, {{data.deviceType}}, {{data.notifyName}}, {{data.media.data}}, {{data.media.mimetype}}, {{data.media.filename}}, {{data.media.filesize}}
`;
import { useNavigate, useSearchParams } from "react-router-dom";
import useSWR from "swr";
export default function WebhookEdit() {
const [searchParams] = useSearchParams();
@@ -94,54 +86,17 @@ export default function WebhookEdit() {
);
}
function EditView({ webhook }: { webhook: WebHook | null }) {
function EditView({ webhook }: { webhook: Partial<WebHook> | null }) {
const navigate = useNavigate();
const [name, setName] = useState(webhook?.name || "");
const [description, setDescription] = useState(webhook?.description || "");
const [url, setUrl] = useState(webhook?.url || "");
const [method, setMethod] = useState(webhook?.method || "POST");
const [headers, setHeaders] = useState(webhook?.headers || "{}");
const [payload, setPayload] = useState(webhook?.payload || "{}");
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
const [enabled, setEnabled] = useState(webhook?.enabled || true);
const [replay, setReplay] = useState(webhook?.replay || false);
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
const [enabled, setEnabled] = useState(webhook?.enabled );
const safeJson = (value: string) => {
try {
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
} catch {
return value || "{}";
}
};
// useShallowEffect(() => {
// let headerObj: Record<string, string> = {};
// try {
// headerObj = JSON.parse(headers);
// } catch { }
// if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
// setHeaders(JSON.stringify(headerObj, null, 2));
// }, [apiToken]);
const previewCode = useMemo(() => {
let headerObj: Record<string, string> = {};
try {
headerObj = JSON.parse(headers);
} catch {}
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
const prettyHeaders = safeJson(JSON.stringify(headerObj));
const prettyPayload = safeJson(payload);
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
return `fetch("${url || "https://example.com/webhook"}", {
method: "${method}",
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
})
.then(res => res.json())
.then(console.log)
.catch(console.error);`;
}, [url, method, headers, payload, apiToken]);
async function onSubmit() {
if (!webhook?.id) {
@@ -163,10 +118,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
url,
method,
headers,
payload,
enabled,
replay,
replayKey,
enabled: enabled || false,
});
if (data?.success) {
@@ -191,7 +143,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Stack
gap="md"
maw={900}
w={"100%"}
mx="auto"
bg="rgba(45,45,45,0.6)"
p="xl"
@@ -204,7 +156,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
>
<Group justify="space-between">
<Title order={2} c="#EAEAEA" fw={600}>
Create Webhook
Edit Webhook
</Title>
<IconCode color="#00FFFF" size={28} />
</Group>
@@ -261,7 +213,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
}}
/>
<Stack gap="xs">
{/* <Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Headers (JSON)
</Text>
@@ -279,9 +231,9 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
automaticLayout: true,
}}
/>
</Stack>
</Stack> */}
<Stack gap="xs">
{/* <Stack gap="xs">
<Text fw={600} c="#EAEAEA">
Payload
</Text>
@@ -301,11 +253,10 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
automaticLayout: true,
}}
/>
</Stack>
</Stack> */}
<Checkbox
label="Enable Webhook"
checked={enabled}
defaultChecked={enabled}
onChange={(e) => setEnabled(e.target.checked as any)}
color="teal"
styles={{
@@ -313,7 +264,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
}}
/>
<Checkbox
{/* <Checkbox
label="Enable Replay"
checked={replay}
onChange={(e) => setReplay(e.target.checked as any)}
@@ -321,17 +272,17 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
styles={{
label: { color: "#EAEAEA" },
}}
/>
/> */}
<TextInput
{/* <TextInput
description="Replay Key is used to identify the webhook example: data.text"
label="Replay Key"
placeholder="Replay Key"
value={replayKey}
onChange={(e) => setReplayKey(e.target.value)}
/>
/> */}
<Card
{/* <Card
radius="xl"
p="md"
style={{
@@ -358,7 +309,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
}}
/>
</Stack>
</Card>
</Card> */}
<Group justify="flex-end" mt="md">
<Button

View File

@@ -109,10 +109,11 @@ export default function WebhookHome() {
return (
<Stack style={{ backgroundColor: "#191919" }} p="xl">
<Group justify="space-between" mb="lg">
<Title order={2} c="#EAEAEA" fw={600}>
Webhook Manager
</Title>
<Title order={2} c="#EAEAEA" fw={600}>
Webhook Manager
</Title>
<Group justify="end" mb="lg">
<ButtonCreate />
<Tooltip label="Refresh webhooks" withArrow color="cyan">
<ActionIcon
@@ -229,7 +230,7 @@ export default function WebhookHome() {
</Text>
</Group>
<Group gap="xs">
{/* <Group gap="xs">
<Text c="#9A9A9A" size="sm">
Headers:
</Text>
@@ -238,9 +239,9 @@ export default function WebhookHome() {
? webhook.headers
: "No headers configured"}
</Text>
</Group>
</Group> */}
<Group gap="xs">
{/* <Group gap="xs">
<Text c="#9A9A9A" size="sm">
Payload:
</Text>
@@ -249,7 +250,7 @@ export default function WebhookHome() {
? webhook.payload
: "Empty payload"}
</Text>
</Group>
</Group> */}
</Stack>
</Card>
))}

View File

@@ -1,72 +1,45 @@
import WAWebJS, { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js';
import qrcode from 'qrcode-terminal';
import "colors";
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuid } from 'uuid';
import { prisma } from '../prisma';
import { getValueByPath } from '../get_value_by_path';
import "colors"
import qrcode from 'qrcode-terminal';
import WAWebJS, { Client, LocalAuth } from 'whatsapp-web.js';
import { logger } from '../logger';
import _ from 'lodash';
import MimeType from '../mim_utils';
import sharp from "sharp";
interface Base64ImageResult {
fileName: string;
base64: string;
sizeBeforeKB: number;
sizeAfterKB: number;
}
export async function convertImageToPngBase64(
inputPath: string,
): Promise<Base64ImageResult> {
// Baca buffer asli
const originalBuffer = await fs.readFile(inputPath);
const sizeBeforeKB = originalBuffer.length / 1024;
// Konversi & kompres ke PNG
const optimizedBuffer = await sharp(originalBuffer)
.png({ compressionLevel: 9 })
.toBuffer();
const sizeAfterKB = optimizedBuffer.length / 1024;
// Convert ke base64
const base64 = `data:image/png;base64,${optimizedBuffer.toString("base64")}`;
return {
fileName: inputPath.split("/").pop() || "image.png",
base64,
sizeBeforeKB,
sizeAfterKB,
};
}
import { prisma } from '../prisma';
type HookData =
| { eventType: "qr"; qr: string }
| { eventType: "ready" }
| { eventType: "disconnected"; reason?: string }
| { eventType: "message" } & Partial<WAWebJS.Message>;
const MEDIA_DIR = path.join(process.cwd(), 'downloads');
await ensureDir(MEDIA_DIR);
async function ensureDir(dir: string) {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
}
async function handleHook(data: HookData) {
const webHooks = await prisma.webHook.findMany({ where: { enabled: true } });
if (webHooks.length === 0) return;
await Promise.allSettled(
webHooks.map(async (hook) => {
try {
log(`🌐 Mengirim webhook ke ${hook.url}`);
let res: Response = {} as Response;
res = await fetch(hook.url, {
method: hook.method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${hook.apiToken}`,
},
body: JSON.stringify(data),
});
const json = await res.text();
logger.info(`[RESPONSE] ${hook.url}: ${json}`);
} catch (err) {
logger.error(`[ERROR] ${hook.url}:`);
}
})
)
type DataMessage = {
from: string;
fromNumber: string;
fromMe: boolean;
body: string;
hasMedia: boolean;
type: WAWebJS.MessageTypes;
to: string;
deviceType: string;
media: Record<string, any>;
notifyName: string;
}
// === STATE GLOBAL ===
@@ -168,6 +141,7 @@ async function startClient() {
state.qr = qr;
qrcode.generate(qr, { small: true });
log('🔑 QR code baru diterbitkan');
handleHook({ eventType: "qr", qr });
});
client.on('ready', () => {
@@ -177,6 +151,7 @@ async function startClient() {
state.isReconnecting = false;
state.isStarting = false;
state.qr = null;
handleHook({ eventType: "ready" });
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
@@ -191,16 +166,19 @@ async function startClient() {
client.on('disconnected', async (reason) => {
log('⚠️ Client terputus:', reason);
state.ready = false;
handleHook({ eventType: "disconnected", reason });
if (state.reconnectTimeout) clearTimeout(state.reconnectTimeout);
state.isReconnecting = true;
log('⏳ Mencoba reconnect dalam 5 detik...');
state.reconnectTimeout = setTimeout(async () => {
state.isReconnecting = false;
await startClient();
}, 5000);
});
client.on('message', handleIncomingMessage);
// === INISIALISASI ===
@@ -220,8 +198,10 @@ async function startClient() {
// === HANDLER PESAN MASUK ===
async function handleIncomingMessage(msg: WAWebJS.Message) {
const chat = await msg.getChat();
await chat.sendStateTyping();
// await chat.sendStateTyping();
log(`💬 Pesan dari ${msg.from}: ${msg.body || '[MEDIA]'}`);
if (!connectedAt) return;
@@ -236,167 +216,27 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
const media = await msg.downloadMedia();
(msg as any).media = media;
}
console.log("kirim ke webhook")
const res = await fetch("https://n8n.wibudev.com/webhook/dc164759-b7ba-47d5-b5d8-ffd9d5840090", {
body: JSON.stringify(msg),
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
const json = await res.text();
console.log(json);
try {
// const notifyName = (msg as any)._data.notifyName;
// const dataMessage: DataMessage = {
// from: msg.from,
// fromNumber: msg.from.split('@')[0] || '',
// fromMe: msg.fromMe,
// body: msg.body,
// hasMedia: msg.hasMedia,
// type: msg.type,
// to: msg.to,
// deviceType: msg.deviceType,
// media: (msg as any).media,
// notifyName,
// };
// === KIRIM KE WEBHOOK ===
// try {
// const webhooks = await prisma.webHook.findMany({ where: { enabled: true } });
// if (!webhooks.length) {
// log('🚫 Tidak ada webhook yang aktif');
// return;
// }
handleHook({ eventType: "message", ...msg })
// // await Promise.allSettled(
// // webhooks.map(async (hook) => {
// // try {
// // log(`🌐 Mengirim webhook ke ${hook.url}`);
// // let res: Response = {} as Response;
// // if (!dataMessage.hasMedia) {
// // logger.info(`[SEND NO MEDIA] ${hook.url}`);
// // res = await fetch(hook.url, {
// // method: hook.method,
// // headers: {
// // "Content-Type": "application/json",
// // Authorization: `Bearer ${hook.apiToken}`,
// // },
// // body: JSON.stringify({
// // question: msg.body,
// // overrideConfig: {
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
// // }
// // }),
// // });
// // }
// // if (dataMessage.hasMedia) {
// // logger.info(`[SEND MEDIA] ${hook.url}`);
// // const media = await msg.downloadMedia();
// // const mimeMessage = media.mimetype || 'application/octet-stream';
// // const typeMime = new MimeType(mimeMessage);
// // const prefixedBase64 = `data:${mimeMessage};base64,${media.data}`;
// // dataMessage.media = {
// // type: typeMime.getCategory() === "image" ? "file" : "file:full",
// // data: prefixedBase64,
// // mime: mimeMessage,
// // name: media.filename || `${uuid()}.${typeMime.getExtension()}`
// // };
// // res = await fetch(hook.url, {
// // method: hook.method,
// // headers: {
// // "Content-Type": "application/json",
// // Authorization: `Bearer ${hook.apiToken}`,
// // },
// // body: JSON.stringify({
// // question: msg.body || dataMessage.media.mime,
// // overrideConfig: {
// // sessionId: `${_.kebabCase(dataMessage.fromNumber)}_x_${dataMessage.fromNumber}`,
// // vars: { userName: _.kebabCase(dataMessage.fromNumber), userPhone: dataMessage.fromNumber },
// // },
// // uploads: [dataMessage.media],
// // }),
// // });
// // }
// // const responseText = await res.text();
// // if (!res.ok) {
// // log(`⚠️ Webhook ${hook.url} gagal: ${res.status}`);
// // logger.error(`[REPLY] Response: ${responseText}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR01]");
// // return;
// // }
// // const responseJson = JSON.parse(responseText);
// // logger.info(`[REPLY] Response: ${responseJson.text}`);
// // if (hook.replay) {
// // try {
// // const textResponseRaw = hook.replayKey
// // ? getValueByPath(responseJson, hook.replayKey, JSON.stringify(responseJson))
// // : JSON.stringify(responseJson, null, 2);
// // await chat.clearState();
// // // send message
// // await chat.sendMessage(textResponseRaw);
// // logger.info(`💬 Balasan dikirim ke ${msg.from}`);
// // } catch (err) {
// // logger.error(`⚠️ Gagal menampilkan status mengetik: ${err}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR03]");
// // }
// // }
// // } catch (err) {
// // logger.error(`❌ Gagal kirim ke ${hook.url}: ${err}`);
// // await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR04]");
// // }
// // })
// // );
// } catch (error) {
// logger.error(`❌ Error mengirim webhook [ERR05]: ${error}`);
// await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR05]");
// }
} catch (err) {
logger.error(`❌ Error handling pesan [ERR06]: ${err}`);
await msg.reply("Maaf, terjadi kesalahan saat memproses pesan Anda [ERR06]");
} finally {
await chat.clearState();
}
}
// === CLEANUP SAAT EXIT ===
process.on('SIGINT', async () => {
process.on('SIGINT', () => {
log('🛑 SIGINT diterima, menutup client...');
await destroyClient();
process.exit(0);
});
process.on('SIGTERM', async () => {
log('🛑 SIGTERM diterima, menutup client...');
await destroyClient();
process.exit(0);
destroyClient().then(() => {
process.exit(0);
}).catch((err) => {
log('⚠️ Error saat destroyClient:', err);
process.exit(1);
});
});
const getState = () => state;
export { startClient, destroyClient, getState };
export { destroyClient, getState, startClient };
if (import.meta.main) {
await startClient();

View File

@@ -5,7 +5,11 @@ import { prisma } from '../lib/prisma'
const secret = process.env.JWT_SECRET
export default function apiAuth(app: Elysia) {
if (!secret) {
throw new Error('JWT_SECRET is not defined')
}
export function apiAuth(app: Elysia) {
if (!secret) {
throw new Error('JWT_SECRET is not defined')
}
@@ -16,37 +20,63 @@ export default function apiAuth(app: Elysia) {
secret,
})
)
.derive(async ({ cookie, headers, jwt }) => {
.derive(async ({ cookie, headers, jwt, request }) => {
let token: string | undefined
if (cookie?.token?.value) {
token = cookie.token.value as any
}
if (headers['x-token']?.startsWith('Bearer ')) {
token = (headers['x-token'] as string).slice(7)
}
if (headers['authorization']?.startsWith('Bearer ')) {
token = (headers['authorization'] as string).slice(7)
}
// 🔸 Ambil token dari Cookie
if (cookie?.token?.value) token = cookie.token.value as string
let user: null | Awaited<ReturnType<typeof prisma.user.findUnique>> = null
if (token) {
try {
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
if (decoded.sub) {
user = await prisma.user.findUnique({
where: { id: decoded.sub as string },
})
}
} catch (err) {
console.warn('[SERVER][apiAuth] Invalid token', err)
// 🔸 Ambil token dari Header (case-insensitive)
const possibleHeaders = [
'authorization',
'Authorization',
'x-token',
'X-Token',
]
for (const key of possibleHeaders) {
const value = headers[key]
if (typeof value === 'string') {
token = value.startsWith('Bearer ') ? value.slice(7) : value
break
}
}
return { user }
// 🔸 Tidak ada token
if (!token) {
console.warn(`[AUTH] No token found for ${request.method} ${request.url}`)
return { user: null }
}
// 🔸 Verifikasi token
try {
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
if (!decoded?.sub) {
console.warn('[AUTH] Token missing sub field:', decoded)
return { user: null }
}
const user = await prisma.user.findUnique({
where: { id: decoded.sub as string },
})
if (!user) {
console.warn('[AUTH] User not found for sub:', decoded.sub)
return { user: null }
}
return { user }
} catch (err) {
console.warn('[AUTH] Invalid JWT token:', err)
return { user: null }
}
})
.onBeforeHandle(({ user, set }) => {
.onBeforeHandle(({ user, set, request }) => {
if (!user) {
console.warn(
`[AUTH] Unauthorized access: ${request.method} ${request.url}`
)
set.status = 401
return { error: 'Unauthorized' }
}

View File

@@ -140,7 +140,7 @@ const WaRoute = new Elysia({
number: t.String({ minLength: 10, maxLength: 15, examples: ["6281234567890"] }),
caption: t.Optional(t.String({ maxLength: 255, examples: ["Hello World"] })),
media: t.Object({
data: t.String({ examples: ["iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="] }), // base64 tanpa prefix
data: t.String({ examples: ["iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC..."], description: "Base64 encoded media data" }),
filename: t.String({ minLength: 1, maxLength: 255, examples: ["file.png"] }),
mimetype: t.String({ minLength: 1, maxLength: 255, examples: ["image/png"] }),
}),
@@ -151,6 +151,91 @@ const WaRoute = new Elysia({
"Send media (image, audio, video, PDF, or any file) to WhatsApp"
},
}
);
)
.get("/code", async (ctx: Context) => {
const { nom, text } = ctx.query
if (!nom || !text) {
ctx.set.status = 400;
return {
message: "[QUERY] Nomor dan teks harus diisi",
};
}
const state = getState();
if (!state.ready) {
ctx.set.status = 400;
return {
message: "[READY] WhatsApp client tidak siap",
};
}
if (!state.client) {
ctx.set.status = 400;
return {
message: "[CLIENT] WhatsApp client tidak siap",
};
}
const chat = await state.client.sendMessage(`${nom}@c.us`, text);
return {
message: "✅ Message sent",
info: chat.id,
};
}, {
query: t.Object({
nom: t.String({ minLength: 10, maxLength: 15, examples: ["6281234567890"] }),
text: t.String({ examples: ["Hello World"] }),
}),
detail: {
summary: "Send text to WhatsApp",
description:
"Send text to WhatsApp via GET request"
},
})
.post("/send-typing", async (ctx: Context) => {
const { nom } = ctx.query
if (!nom) {
ctx.set.status = 400;
return {
message: "[QUERY] Nomor harus diisi",
};
}
const state = getState();
if (!state.ready) {
ctx.set.status = 400;
return {
message: "[READY] WhatsApp client tidak siap",
};
}
if (!state.client) {
ctx.set.status = 400;
return {
message: "[CLIENT] WhatsApp client tidak siap",
};
}
const chat = await state.client.getChatById(`${nom}@c.us`);
await chat.sendSeen();
await chat.sendStateTyping();
return {
message: "✅ Typing sent",
info: chat.id,
};
}, {
query: t.Object({
nom: t.String({ minLength: 10, maxLength: 15, examples: ["6281234567890"] }),
}),
detail: {
summary: "Send typing to WhatsApp",
description:
"Send typing to WhatsApp via GET request"
},
});
export default WaRoute;

View File

@@ -60,6 +60,16 @@ const WebhookRoute = new Elysia({
where: {
id: ctx.params.id,
},
select: {
id: true,
name: true,
description: true,
url: true,
method: true,
headers: true,
apiToken: true,
enabled: true,
}
});
return {
webhook,
@@ -93,7 +103,7 @@ const WebhookRoute = new Elysia({
},
})
.put("/update/:id", async (ctx) => {
const { name, description, url, method, headers, payload, apiToken, enabled, replay, replayKey } = ctx.body;
const { name, description, url, method, headers, apiToken, enabled } = ctx.body;
await prisma.webHook.update({
where: {
id: ctx.params.id,
@@ -104,11 +114,8 @@ const WebhookRoute = new Elysia({
url,
method,
headers: headers,
payload: payload,
apiToken,
enabled,
replay,
replayKey,
enabled,
},
});
return {
@@ -125,11 +132,8 @@ const WebhookRoute = new Elysia({
url: t.String(),
method: t.String(),
headers: t.String(),
payload: t.String(),
apiToken: t.String(),
enabled: t.Boolean(),
replay: t.Boolean(),
replayKey: t.String(),
}),
detail: {
summary: "Update webhook",

View File

@@ -1,293 +0,0 @@
[
{
"name": "parse",
"type": "function",
"args": [
{
"name": "uuid",
"type": "string",
"optional": false
}
],
"returns": "Uint8Array<ArrayBufferLike>",
"description": "uuid function parse",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "parse"
}
},
{
"name": "stringify",
"type": "function",
"args": [
{
"name": "arr",
"type": "Uint8Array<ArrayBufferLike>",
"optional": false
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function stringify",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "stringify"
}
},
{
"name": "v1",
"type": "function",
"args": [
{
"name": "options",
"type": "Version1Options | undefined",
"optional": true
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v1",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v1"
}
},
{
"name": "v1ToV6",
"type": "function",
"args": [
{
"name": "uuid",
"type": "string",
"optional": false
}
],
"returns": "string",
"description": "uuid function v1ToV6",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v1ToV6"
}
},
{
"name": "v3",
"type": "function",
"args": [
{
"name": "value",
"type": "string | Uint8Array<ArrayBufferLike>",
"optional": false
},
{
"name": "namespace",
"type": "UUIDTypes",
"optional": false
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v3",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v3"
}
},
{
"name": "v4",
"type": "function",
"args": [
{
"name": "options",
"type": "Version4Options | undefined",
"optional": true
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v4",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v4"
}
},
{
"name": "v5",
"type": "function",
"args": [
{
"name": "value",
"type": "string | Uint8Array<ArrayBufferLike>",
"optional": false
},
{
"name": "namespace",
"type": "UUIDTypes",
"optional": false
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v5",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v5"
}
},
{
"name": "v6",
"type": "function",
"args": [
{
"name": "options",
"type": "Version1Options | undefined",
"optional": true
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v6",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v6"
}
},
{
"name": "v6ToV1",
"type": "function",
"args": [
{
"name": "uuid",
"type": "string",
"optional": false
}
],
"returns": "string",
"description": "uuid function v6ToV1",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v6ToV1"
}
},
{
"name": "v7",
"type": "function",
"args": [
{
"name": "options",
"type": "Version7Options | undefined",
"optional": true
},
{
"name": "buf",
"type": "undefined",
"optional": true
},
{
"name": "offset",
"type": "number | undefined",
"optional": true
}
],
"returns": "string",
"description": "uuid function v7",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "v7"
}
},
{
"name": "validate",
"type": "function",
"args": [
{
"name": "uuid",
"type": "unknown",
"optional": false
}
],
"returns": "boolean",
"description": "uuid function validate",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "validate"
}
},
{
"name": "version",
"type": "function",
"args": [
{
"name": "uuid",
"type": "string",
"optional": false
}
],
"returns": "number",
"description": "uuid function version",
"x-props": {
"module": "uuid",
"kind": "function",
"operationId": "version"
}
}
]

File diff suppressed because one or more lines are too long

4
x.sh
View File

@@ -1,4 +0,0 @@
curl -X POST "https://n8n.wibudev.com/form/d65f7cda-4fb6-40cc-aaa0-59127e224429" \
-H "accept: */*" \
-H "Content-Type: multipart/form-data" \
-F "data=@//Users/bip/Documents/projects/jenna/wajs-server/xarif.pdf"

33
x.ts
View File

@@ -1,33 +0,0 @@
async function query(data: any) {
const file = Buffer.from(await Bun.file("./downloads/billing-server-20-06-2024.pdf").arrayBuffer()).toString("base64");
const base64File = `data:application/pdf;base64,${file}`;
const fileObject = {
type: "file:full",
data: base64File,
mime: "application/pdf",
name: "billing-server-20-06-2024.pdf"
}
const response = await fetch(
"https://cloud-aiflow.wibudev.com/api/v1/prediction/4da85628-c638-43d3-9491-4cd0a7e6b1b8",
{
headers: {
Authorization: "Bearer v3WdPjn61bNDsEYCO5_LYPRs16ICKjpQE6lF60DjpNo",
"Content-Type": "application/json"
},
method: "POST",
body: JSON.stringify({
...data,
uploads: [fileObject]
})
}
);
const result = await response.text();
return result;
}
query({"question": "apa isi data ini ?"}).then((response) => {
console.log(response);
});

23
x.txt
View File

@@ -1,23 +0,0 @@
ini adalah response dari fetch api
{
"from": "6289505046093@c.us",
"fromMe": false,
"body": "halo gaes",
"hasMedia": false,
"type": "chat",
"to": "6289697338821@c.us",
"deviceType": "android",
"media": {
"data": null,
"mimetype": null,
"filename": null,
"filesize": null
},
"notifyName": "jenna ai"
}
saya ingin mengambil data dynamic dari response tersebut menggunakan string
function getData(responseJson, key){
}

37
x.yml
View File

@@ -1,37 +0,0 @@
services:
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: wibu@bip.com
PGADMIN_DEFAULT_PASSWORD: Production_123
volumes:
- ./data/pgadmin:/var/lib/pgadmin
networks:
- pgadmin-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
pgadmin-frpc:
image: snowdreamtech/frpc:latest
container_name: pgadmin-frpc
restart: always
volumes:
- ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- pgadmin-network
depends_on:
- pgadmin
networks:
pgadmin-network:
driver: bridge

BIN
xarif.pdf

Binary file not shown.

87
xx.ts
View File

@@ -1,87 +0,0 @@
// file: gen-lodash-mcp.ts
import ts from "typescript";
import fs from "fs";
const moduleName = "lodash";
const tmpFile = `./tmp-${moduleName}.ts`;
// generate file sementara untuk memastikan simbol dapat di-resolve
fs.writeFileSync(
tmpFile,
`
import * as _ from "${moduleName}";
type LodashType = typeof _;
export type { LodashType };
`
);
// buat program TS yang bisa resolve definisi lodash
const program = ts.createProgram([tmpFile], {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
allowSyntheticDefaultImports: true,
moduleResolution: ts.ModuleResolutionKind.Node10,
types: ["lodash"],
});
const checker = program.getTypeChecker();
const source = program.getSourceFile(tmpFile)!;
// ambil type alias LodashType
let lodashType: ts.Type | null = null;
ts.forEachChild(source, (node) => {
if (ts.isTypeAliasDeclaration(node) && node.name.text === "LodashType") {
lodashType = checker.getTypeFromTypeNode(node.type);
}
});
if (!lodashType) {
console.error("❌ Tidak menemukan tipe lodash");
process.exit(1);
}
const props = checker.getPropertiesOfType(lodashType);
const results: any[] = [];
for (const prop of props) {
const name = prop.getName();
const propType = checker.getTypeOfSymbolAtLocation(prop, source);
const callSignatures = propType.getCallSignatures();
if (!callSignatures.length) continue; // skip non-function
const sig = callSignatures[0] as ts.Signature;
const params = sig.getParameters().map((p) => {
const decl = p.getDeclarations()?.[0];
return {
name: p.getName(),
type: checker.typeToString(
checker.getTypeOfSymbolAtLocation(p, decl || source)
),
};
});
const returnType = checker.typeToString(sig.getReturnType());
results.push({
name,
type: "function",
args: params,
returns: returnType,
description: `Lodash function ${name}`,
"x-props": {
module: moduleName,
kind: "function",
operationId: name,
},
});
}
fs.unlinkSync(tmpFile);
fs.writeFileSync(`lodash-mcp.json`, JSON.stringify(results, null, 2));
console.log(`✅ Generated ${results.length} lodash MCP tools`);

146
xxx.ts
View File

@@ -1,146 +0,0 @@
// file: gen-mcp.ts
import ts from "typescript";
import fs from "fs";
import path from "path";
const pkgs = process.argv.slice(2);
if (!pkgs.length) {
console.error("❌ Usage: bun run gen-mcp.ts <package-name> [more-packages...]");
process.exit(1);
}
for (const pkg of pkgs) {
console.log(`\n🔍 Generating MCP JSON for: ${pkg} ...`);
const tmpFile = path.resolve(`./tmp-${pkg}.ts`);
fs.writeFileSync(
tmpFile,
`
import * as Pkg from "${pkg}";
type TargetType = typeof Pkg;
export type { TargetType };
`
);
const program = ts.createProgram([tmpFile], {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
allowSyntheticDefaultImports: true,
moduleResolution: ts.ModuleResolutionKind.Node10,
types: [pkg],
});
const checker = program.getTypeChecker();
const source = program.getSourceFile(tmpFile)!;
let targetType: ts.Type | null = null;
ts.forEachChild(source, (node) => {
if (ts.isTypeAliasDeclaration(node) && node.name.text === "TargetType") {
targetType = checker.getTypeFromTypeNode(node.type);
}
});
if (!targetType) {
console.error(`❌ Tidak menemukan tipe untuk ${pkg}`);
fs.unlinkSync(tmpFile);
continue;
}
const props = checker.getPropertiesOfType(targetType);
const results: any[] = [];
for (const prop of props) {
const name = prop.getName();
const propType = checker.getTypeOfSymbolAtLocation(prop, source);
// === Jika fungsi ===
const callSignatures = propType.getCallSignatures();
if (callSignatures.length) {
const sig = callSignatures[0] as ts.Signature;
const params = sig.getParameters().map((p, i) => {
const decl = p.getDeclarations()?.[0] as ts.ParameterDeclaration | undefined;
const paramType = checker.getTypeOfSymbolAtLocation(p, decl || source);
const isOptional =
!!(decl && (decl.questionToken || decl.initializer)) ||
(decl && ts.isParameter(decl) && !!decl.dotDotDotToken); // cek rest parameter
return {
name: p.getName(),
type: checker.typeToString(paramType),
optional: isOptional,
};
});
const returnType = checker.typeToString(sig.getReturnType());
results.push({
name,
type: "function",
args: params,
returns: returnType,
description: `${pkg} function ${name}`,
"x-props": {
module: pkg,
kind: "function",
operationId: name,
},
});
continue;
}
// === Jika class ===
const symbolDecls = prop.getDeclarations() ?? [];
for (const decl of symbolDecls) {
if (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) {
const classType = checker.getTypeAtLocation(decl);
const classMethods = classType
.getProperties()
.filter((p) =>
checker.getTypeOfSymbolAtLocation(p, decl).getCallSignatures().length
);
const methods = classMethods.map((m) => {
const sig = checker
.getTypeOfSymbolAtLocation(m, decl)
.getCallSignatures()[0] as ts.Signature;
const params = sig.getParameters().map((p) => {
const d = p.getDeclarations()?.[0] as ts.ParameterDeclaration | undefined;
const t = checker.getTypeOfSymbolAtLocation(p, d || decl);
return {
name: p.getName(),
type: checker.typeToString(t),
optional: !!(d && (d.questionToken || d.initializer || d.dotDotDotToken)),
};
});
const returnType = checker.typeToString(sig.getReturnType());
return { name: m.getName(), params, returns: returnType };
});
results.push({
name,
type: "class",
methods,
description: `${pkg} class ${name}`,
"x-props": {
module: pkg,
kind: "class",
operationId: name,
},
});
}
}
}
fs.unlinkSync(tmpFile);
const outFile = `${pkg}-mcp.json`;
fs.writeFileSync(outFile, JSON.stringify(results, null, 2));
console.log(`✅ Generated ${results.length} entries → ${outFile}`);
}
console.log("\n🎉 Done! Ready to use in MCP tools.");