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 = [
|
const user = [
|
||||||
{
|
{
|
||||||
name: "Bip",
|
name: "wibu",
|
||||||
email: "bip@bip.com",
|
email: "wibu@bip.com",
|
||||||
password: "bip",
|
password: "Production_123",
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Elysia, { t } from "elysia";
|
|||||||
import Swagger from "@elysiajs/swagger";
|
import Swagger from "@elysiajs/swagger";
|
||||||
import html from "./index.html";
|
import html from "./index.html";
|
||||||
import Dashboard from "./server/routes/darmasaba";
|
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 Auth from "./server/routes/auth_route";
|
||||||
import ApiKeyRoute from "./server/routes/apikey_route";
|
import ApiKeyRoute from "./server/routes/apikey_route";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
Group,
|
Group,
|
||||||
|
PasswordInput,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -68,7 +69,7 @@ export default function Login() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<PasswordInput
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function WebhookCreate() {
|
|||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
<Stack
|
<Stack
|
||||||
gap="md"
|
gap="md"
|
||||||
maw={900}
|
w={"100%"}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
bg="rgba(45,45,45,0.6)"
|
bg="rgba(45,45,45,0.6)"
|
||||||
p="xl"
|
p="xl"
|
||||||
@@ -186,7 +186,7 @@ export default function WebhookCreate() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack gap="xs">
|
{/* <Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Headers (JSON)
|
Headers (JSON)
|
||||||
</Text>
|
</Text>
|
||||||
@@ -204,9 +204,9 @@ export default function WebhookCreate() {
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack> */}
|
||||||
|
|
||||||
<Stack gap="xs">
|
{/* <Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Payload
|
Payload
|
||||||
</Text>
|
</Text>
|
||||||
@@ -226,7 +226,7 @@ export default function WebhookCreate() {
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack> */}
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Enable Webhook"
|
label="Enable Webhook"
|
||||||
@@ -237,7 +237,7 @@ export default function WebhookCreate() {
|
|||||||
label: { color: "#EAEAEA" },
|
label: { color: "#EAEAEA" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
label="Enable Replay"
|
label="Enable Replay"
|
||||||
checked={replay}
|
checked={replay}
|
||||||
onChange={(e) => setReplay(e.currentTarget.checked)}
|
onChange={(e) => setReplay(e.currentTarget.checked)}
|
||||||
@@ -245,16 +245,16 @@ export default function WebhookCreate() {
|
|||||||
styles={{
|
styles={{
|
||||||
label: { color: "#EAEAEA" },
|
label: { color: "#EAEAEA" },
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
<TextInput
|
{/* <TextInput
|
||||||
description="Replay Key is used to identify the webhook example: data.text"
|
description="Replay Key is used to identify the webhook example: data.text"
|
||||||
label="Replay Key"
|
label="Replay Key"
|
||||||
placeholder="Replay Key"
|
placeholder="Replay Key"
|
||||||
value={replayKey}
|
value={replayKey}
|
||||||
onChange={(e) => setReplayKey(e.target.value)}
|
onChange={(e) => setReplayKey(e.target.value)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<Card
|
{/* <Card
|
||||||
radius="xl"
|
radius="xl"
|
||||||
p="md"
|
p="md"
|
||||||
style={{
|
style={{
|
||||||
@@ -281,7 +281,7 @@ export default function WebhookCreate() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card> */}
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
import useSWR from "swr";
|
import clientRoutes from "@/clientRoutes";
|
||||||
import apiFetch from "@/lib/apiFetch";
|
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 type { WebHook } from "generated/prisma";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMemo } from "react";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { notifications } from "@mantine/notifications";
|
import useSWR from "swr";
|
||||||
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}}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function WebhookEdit() {
|
export default function WebhookEdit() {
|
||||||
const [searchParams] = useSearchParams();
|
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 navigate = useNavigate();
|
||||||
const [name, setName] = useState(webhook?.name || "");
|
const [name, setName] = useState(webhook?.name || "");
|
||||||
const [description, setDescription] = useState(webhook?.description || "");
|
const [description, setDescription] = useState(webhook?.description || "");
|
||||||
const [url, setUrl] = useState(webhook?.url || "");
|
const [url, setUrl] = useState(webhook?.url || "");
|
||||||
const [method, setMethod] = useState(webhook?.method || "POST");
|
const [method, setMethod] = useState(webhook?.method || "POST");
|
||||||
const [headers, setHeaders] = useState(webhook?.headers || "{}");
|
const [headers, setHeaders] = useState(webhook?.headers || "{}");
|
||||||
const [payload, setPayload] = useState(webhook?.payload || "{}");
|
|
||||||
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
|
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
|
||||||
const [enabled, setEnabled] = useState(webhook?.enabled || true);
|
const [enabled, setEnabled] = useState(webhook?.enabled );
|
||||||
const [replay, setReplay] = useState(webhook?.replay || false);
|
|
||||||
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
|
|
||||||
|
|
||||||
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() {
|
async function onSubmit() {
|
||||||
if (!webhook?.id) {
|
if (!webhook?.id) {
|
||||||
@@ -163,10 +118,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
payload,
|
enabled: enabled || false,
|
||||||
enabled,
|
|
||||||
replay,
|
|
||||||
replayKey,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
@@ -191,7 +143,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
<Stack
|
<Stack
|
||||||
gap="md"
|
gap="md"
|
||||||
maw={900}
|
w={"100%"}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
bg="rgba(45,45,45,0.6)"
|
bg="rgba(45,45,45,0.6)"
|
||||||
p="xl"
|
p="xl"
|
||||||
@@ -204,7 +156,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
>
|
>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={2} c="#EAEAEA" fw={600}>
|
<Title order={2} c="#EAEAEA" fw={600}>
|
||||||
Create Webhook
|
Edit Webhook
|
||||||
</Title>
|
</Title>
|
||||||
<IconCode color="#00FFFF" size={28} />
|
<IconCode color="#00FFFF" size={28} />
|
||||||
</Group>
|
</Group>
|
||||||
@@ -261,7 +213,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack gap="xs">
|
{/* <Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Headers (JSON)
|
Headers (JSON)
|
||||||
</Text>
|
</Text>
|
||||||
@@ -279,9 +231,9 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack> */}
|
||||||
|
|
||||||
<Stack gap="xs">
|
{/* <Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Payload
|
Payload
|
||||||
</Text>
|
</Text>
|
||||||
@@ -301,11 +253,10 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack> */}
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Enable Webhook"
|
label="Enable Webhook"
|
||||||
checked={enabled}
|
defaultChecked={enabled}
|
||||||
onChange={(e) => setEnabled(e.target.checked as any)}
|
onChange={(e) => setEnabled(e.target.checked as any)}
|
||||||
color="teal"
|
color="teal"
|
||||||
styles={{
|
styles={{
|
||||||
@@ -313,7 +264,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
label="Enable Replay"
|
label="Enable Replay"
|
||||||
checked={replay}
|
checked={replay}
|
||||||
onChange={(e) => setReplay(e.target.checked as any)}
|
onChange={(e) => setReplay(e.target.checked as any)}
|
||||||
@@ -321,17 +272,17 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
styles={{
|
styles={{
|
||||||
label: { color: "#EAEAEA" },
|
label: { color: "#EAEAEA" },
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<TextInput
|
{/* <TextInput
|
||||||
description="Replay Key is used to identify the webhook example: data.text"
|
description="Replay Key is used to identify the webhook example: data.text"
|
||||||
label="Replay Key"
|
label="Replay Key"
|
||||||
placeholder="Replay Key"
|
placeholder="Replay Key"
|
||||||
value={replayKey}
|
value={replayKey}
|
||||||
onChange={(e) => setReplayKey(e.target.value)}
|
onChange={(e) => setReplayKey(e.target.value)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<Card
|
{/* <Card
|
||||||
radius="xl"
|
radius="xl"
|
||||||
p="md"
|
p="md"
|
||||||
style={{
|
style={{
|
||||||
@@ -358,7 +309,7 @@ function EditView({ webhook }: { webhook: WebHook | null }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card> */}
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -109,10 +109,11 @@ export default function WebhookHome() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
<Group justify="space-between" mb="lg">
|
<Title order={2} c="#EAEAEA" fw={600}>
|
||||||
<Title order={2} c="#EAEAEA" fw={600}>
|
Webhook Manager
|
||||||
Webhook Manager
|
</Title>
|
||||||
</Title>
|
<Group justify="end" mb="lg">
|
||||||
|
|
||||||
<ButtonCreate />
|
<ButtonCreate />
|
||||||
<Tooltip label="Refresh webhooks" withArrow color="cyan">
|
<Tooltip label="Refresh webhooks" withArrow color="cyan">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -229,7 +230,7 @@ export default function WebhookHome() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="xs">
|
{/* <Group gap="xs">
|
||||||
<Text c="#9A9A9A" size="sm">
|
<Text c="#9A9A9A" size="sm">
|
||||||
Headers:
|
Headers:
|
||||||
</Text>
|
</Text>
|
||||||
@@ -238,9 +239,9 @@ export default function WebhookHome() {
|
|||||||
? webhook.headers
|
? webhook.headers
|
||||||
: "No headers configured"}
|
: "No headers configured"}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group> */}
|
||||||
|
|
||||||
<Group gap="xs">
|
{/* <Group gap="xs">
|
||||||
<Text c="#9A9A9A" size="sm">
|
<Text c="#9A9A9A" size="sm">
|
||||||
Payload:
|
Payload:
|
||||||
</Text>
|
</Text>
|
||||||
@@ -249,7 +250,7 @@ export default function WebhookHome() {
|
|||||||
? webhook.payload
|
? webhook.payload
|
||||||
: "Empty payload"}
|
: "Empty payload"}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,72 +1,45 @@
|
|||||||
import WAWebJS, { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js';
|
import "colors";
|
||||||
import qrcode from 'qrcode-terminal';
|
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuid } from 'uuid';
|
import qrcode from 'qrcode-terminal';
|
||||||
import { prisma } from '../prisma';
|
import WAWebJS, { Client, LocalAuth } from 'whatsapp-web.js';
|
||||||
import { getValueByPath } from '../get_value_by_path';
|
|
||||||
import "colors"
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import _ from 'lodash';
|
import { prisma } from '../prisma';
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
async function handleHook(data: HookData) {
|
||||||
try {
|
const webHooks = await prisma.webHook.findMany({ where: { enabled: true } });
|
||||||
await fs.access(dir);
|
if (webHooks.length === 0) return;
|
||||||
} catch {
|
await Promise.allSettled(
|
||||||
await fs.mkdir(dir, { recursive: true });
|
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 ===
|
// === STATE GLOBAL ===
|
||||||
@@ -168,6 +141,7 @@ async function startClient() {
|
|||||||
state.qr = qr;
|
state.qr = qr;
|
||||||
qrcode.generate(qr, { small: true });
|
qrcode.generate(qr, { small: true });
|
||||||
log('🔑 QR code baru diterbitkan');
|
log('🔑 QR code baru diterbitkan');
|
||||||
|
handleHook({ eventType: "qr", qr });
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
@@ -177,6 +151,7 @@ async function startClient() {
|
|||||||
state.isReconnecting = false;
|
state.isReconnecting = false;
|
||||||
state.isStarting = false;
|
state.isStarting = false;
|
||||||
state.qr = null;
|
state.qr = null;
|
||||||
|
handleHook({ eventType: "ready" });
|
||||||
if (state.reconnectTimeout) {
|
if (state.reconnectTimeout) {
|
||||||
clearTimeout(state.reconnectTimeout);
|
clearTimeout(state.reconnectTimeout);
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
@@ -191,16 +166,19 @@ async function startClient() {
|
|||||||
client.on('disconnected', async (reason) => {
|
client.on('disconnected', async (reason) => {
|
||||||
log('⚠️ Client terputus:', reason);
|
log('⚠️ Client terputus:', reason);
|
||||||
state.ready = false;
|
state.ready = false;
|
||||||
|
handleHook({ eventType: "disconnected", reason });
|
||||||
|
|
||||||
if (state.reconnectTimeout) clearTimeout(state.reconnectTimeout);
|
if (state.reconnectTimeout) clearTimeout(state.reconnectTimeout);
|
||||||
|
|
||||||
|
state.isReconnecting = true;
|
||||||
log('⏳ Mencoba reconnect dalam 5 detik...');
|
log('⏳ Mencoba reconnect dalam 5 detik...');
|
||||||
|
|
||||||
state.reconnectTimeout = setTimeout(async () => {
|
state.reconnectTimeout = setTimeout(async () => {
|
||||||
state.isReconnecting = false;
|
|
||||||
await startClient();
|
await startClient();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
client.on('message', handleIncomingMessage);
|
client.on('message', handleIncomingMessage);
|
||||||
|
|
||||||
// === INISIALISASI ===
|
// === INISIALISASI ===
|
||||||
@@ -220,8 +198,10 @@ async function startClient() {
|
|||||||
|
|
||||||
// === HANDLER PESAN MASUK ===
|
// === HANDLER PESAN MASUK ===
|
||||||
async function handleIncomingMessage(msg: WAWebJS.Message) {
|
async function handleIncomingMessage(msg: WAWebJS.Message) {
|
||||||
|
|
||||||
const chat = await msg.getChat();
|
const chat = await msg.getChat();
|
||||||
await chat.sendStateTyping();
|
|
||||||
|
// await chat.sendStateTyping();
|
||||||
log(`💬 Pesan dari ${msg.from}: ${msg.body || '[MEDIA]'}`);
|
log(`💬 Pesan dari ${msg.from}: ${msg.body || '[MEDIA]'}`);
|
||||||
|
|
||||||
if (!connectedAt) return;
|
if (!connectedAt) return;
|
||||||
@@ -237,166 +217,26 @@ async function handleIncomingMessage(msg: WAWebJS.Message) {
|
|||||||
(msg as any).media = media;
|
(msg as any).media = media;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("kirim ke webhook")
|
handleHook({ eventType: "message", ...msg })
|
||||||
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;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// // 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 ===
|
// === CLEANUP SAAT EXIT ===
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', () => {
|
||||||
log('🛑 SIGINT diterima, menutup client...');
|
log('🛑 SIGINT diterima, menutup client...');
|
||||||
await destroyClient();
|
destroyClient().then(() => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
}).catch((err) => {
|
||||||
|
log('⚠️ Error saat destroyClient:', err);
|
||||||
process.on('SIGTERM', async () => {
|
process.exit(1);
|
||||||
log('🛑 SIGTERM diterima, menutup client...');
|
});
|
||||||
await destroyClient();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const getState = () => state;
|
const getState = () => state;
|
||||||
|
|
||||||
export { startClient, destroyClient, getState };
|
export { destroyClient, getState, startClient };
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
await startClient();
|
await startClient();
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { prisma } from '../lib/prisma'
|
|||||||
|
|
||||||
const secret = process.env.JWT_SECRET
|
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) {
|
if (!secret) {
|
||||||
throw new Error('JWT_SECRET is not defined')
|
throw new Error('JWT_SECRET is not defined')
|
||||||
}
|
}
|
||||||
@@ -16,37 +20,63 @@ export default function apiAuth(app: Elysia) {
|
|||||||
secret,
|
secret,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.derive(async ({ cookie, headers, jwt }) => {
|
.derive(async ({ cookie, headers, jwt, request }) => {
|
||||||
let token: string | undefined
|
let token: string | undefined
|
||||||
|
|
||||||
if (cookie?.token?.value) {
|
// 🔸 Ambil token dari Cookie
|
||||||
token = cookie.token.value as any
|
if (cookie?.token?.value) token = cookie.token.value as string
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
let user: null | Awaited<ReturnType<typeof prisma.user.findUnique>> = null
|
// 🔸 Ambil token dari Header (case-insensitive)
|
||||||
if (token) {
|
const possibleHeaders = [
|
||||||
try {
|
'authorization',
|
||||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
'Authorization',
|
||||||
if (decoded.sub) {
|
'x-token',
|
||||||
user = await prisma.user.findUnique({
|
'X-Token',
|
||||||
where: { id: decoded.sub as string },
|
]
|
||||||
})
|
|
||||||
}
|
for (const key of possibleHeaders) {
|
||||||
} catch (err) {
|
const value = headers[key]
|
||||||
console.warn('[SERVER][apiAuth] Invalid token', err)
|
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) {
|
if (!user) {
|
||||||
|
console.warn(
|
||||||
|
`[AUTH] Unauthorized access: ${request.method} ${request.url}`
|
||||||
|
)
|
||||||
set.status = 401
|
set.status = 401
|
||||||
return { error: 'Unauthorized' }
|
return { error: 'Unauthorized' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const WaRoute = new Elysia({
|
|||||||
number: t.String({ minLength: 10, maxLength: 15, examples: ["6281234567890"] }),
|
number: t.String({ minLength: 10, maxLength: 15, examples: ["6281234567890"] }),
|
||||||
caption: t.Optional(t.String({ maxLength: 255, examples: ["Hello World"] })),
|
caption: t.Optional(t.String({ maxLength: 255, examples: ["Hello World"] })),
|
||||||
media: t.Object({
|
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"] }),
|
filename: t.String({ minLength: 1, maxLength: 255, examples: ["file.png"] }),
|
||||||
mimetype: t.String({ minLength: 1, maxLength: 255, examples: ["image/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"
|
"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;
|
export default WaRoute;
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ const WebhookRoute = new Elysia({
|
|||||||
where: {
|
where: {
|
||||||
id: ctx.params.id,
|
id: ctx.params.id,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
url: true,
|
||||||
|
method: true,
|
||||||
|
headers: true,
|
||||||
|
apiToken: true,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
webhook,
|
webhook,
|
||||||
@@ -93,7 +103,7 @@ const WebhookRoute = new Elysia({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.put("/update/:id", async (ctx) => {
|
.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({
|
await prisma.webHook.update({
|
||||||
where: {
|
where: {
|
||||||
id: ctx.params.id,
|
id: ctx.params.id,
|
||||||
@@ -104,11 +114,8 @@ const WebhookRoute = new Elysia({
|
|||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
payload: payload,
|
|
||||||
apiToken,
|
apiToken,
|
||||||
enabled,
|
enabled,
|
||||||
replay,
|
|
||||||
replayKey,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -125,11 +132,8 @@ const WebhookRoute = new Elysia({
|
|||||||
url: t.String(),
|
url: t.String(),
|
||||||
method: t.String(),
|
method: t.String(),
|
||||||
headers: t.String(),
|
headers: t.String(),
|
||||||
payload: t.String(),
|
|
||||||
apiToken: t.String(),
|
apiToken: t.String(),
|
||||||
enabled: t.Boolean(),
|
enabled: t.Boolean(),
|
||||||
replay: t.Boolean(),
|
|
||||||
replayKey: t.String(),
|
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Update webhook",
|
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