feat: Update webhook and WA services
- Update webhook and WhatsApp related pages and services. - Clean up temporary files.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
|
||||
|
||||
\n# build artifacts\nseed\nprisma/seed-linux-x64
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
293
uuid-mcp.json
293
uuid-mcp.json
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
14
webhook.json
14
webhook.json
File diff suppressed because one or more lines are too long
4
x.sh
4
x.sh
@@ -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
33
x.ts
@@ -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
23
x.txt
@@ -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
37
x.yml
@@ -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
|
||||
87
xx.ts
87
xx.ts
@@ -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
146
xxx.ts
@@ -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.");
|
||||
Reference in New Issue
Block a user