Make APIs more similar
This commit is contained in:
parent
9f0e1f8b61
commit
c40d7d056e
57 changed files with 3994 additions and 1801 deletions
3
apps/bridge-whatsapp/eslint.config.mjs
Normal file
3
apps/bridge-whatsapp/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import config from "@link-stack/eslint-config/node";
|
||||
|
||||
export default config;
|
||||
|
|
@ -4,25 +4,33 @@
|
|||
"main": "build/main/index.js",
|
||||
"author": "Darren Clarke <darren@redaranj.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"prettier": "@link-stack/prettier-config",
|
||||
"dependencies": {
|
||||
"@adiwajshing/keyed-db": "0.2.4",
|
||||
"@hono/node-server": "^1.13.8",
|
||||
"@whiskeysockets/baileys": "6.7.21",
|
||||
"hono": "^4.7.4",
|
||||
"link-preview-js": "^3.1.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
"@link-stack/logger": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@link-stack/eslint-config": "workspace:*",
|
||||
"@link-stack/prettier-config": "workspace:*",
|
||||
"@link-stack/typescript-config": "workspace:*",
|
||||
"@types/long": "^5",
|
||||
"@types/node": "*",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "dotenv -- tsx src/index.ts",
|
||||
"start": "node build/main/index.js"
|
||||
"start": "node build/main/index.js",
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@
|
|||
*/
|
||||
export function getMaxAttachmentSize(): number {
|
||||
const envValue = process.env.BRIDGE_MAX_ATTACHMENT_SIZE_MB;
|
||||
const sizeInMB = envValue ? parseInt(envValue, 10) : 50;
|
||||
const sizeInMB = envValue ? Number.parseInt(envValue, 10) : 50;
|
||||
|
||||
// Validate the value
|
||||
if (isNaN(sizeInMB) || sizeInMB <= 0) {
|
||||
if (Number.isNaN(sizeInMB) || sizeInMB <= 0) {
|
||||
console.warn(`Invalid BRIDGE_MAX_ATTACHMENT_SIZE_MB value: ${envValue}, using default 50MB`);
|
||||
return 50 * 1024 * 1024;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { serve } from "@hono/node-server";
|
||||
import WhatsappService from "./service.ts";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
import { createRoutes } from "./routes.ts";
|
||||
import { createLogger } from "./lib/logger";
|
||||
import WhatsappService from "./service.ts";
|
||||
|
||||
const logger = createLogger("bridge-whatsapp-index");
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ const main = async () => {
|
|||
await service.initialize();
|
||||
|
||||
const app = createRoutes(service);
|
||||
const port = parseInt(process.env.PORT || "5000", 10);
|
||||
const port = Number.parseInt(process.env.PORT || "5000", 10);
|
||||
|
||||
serve({ fetch: app.fetch, port }, (info) => {
|
||||
logger.info({ port: info.port }, "bridge-whatsapp listening");
|
||||
|
|
@ -26,7 +27,7 @@ const main = async () => {
|
|||
process.on("SIGINT", shutdown);
|
||||
};
|
||||
|
||||
main().catch((err) => {
|
||||
logger.error(err);
|
||||
main().catch((error) => {
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import pino, { Logger as PinoLogger, LoggerOptions } from 'pino';
|
||||
|
||||
export type Logger = PinoLogger;
|
||||
|
||||
const getLogLevel = (): string => {
|
||||
return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
|
||||
};
|
||||
|
||||
const getPinoConfig = (): LoggerOptions => {
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const baseConfig: LoggerOptions = {
|
||||
level: getLogLevel(),
|
||||
formatters: {
|
||||
level: (label) => {
|
||||
return { level: label.toUpperCase() };
|
||||
},
|
||||
},
|
||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||
redact: {
|
||||
paths: [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'api_key',
|
||||
'apiKey',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'*.password',
|
||||
'*.token',
|
||||
'*.secret',
|
||||
'*.api_key',
|
||||
'*.apiKey',
|
||||
'*.authorization',
|
||||
'*.cookie',
|
||||
'*.access_token',
|
||||
'*.refresh_token',
|
||||
'headers.authorization',
|
||||
'headers.cookie',
|
||||
'headers.Authorization',
|
||||
'headers.Cookie',
|
||||
'credentials.password',
|
||||
'credentials.secret',
|
||||
'credentials.token',
|
||||
],
|
||||
censor: '[REDACTED]',
|
||||
},
|
||||
};
|
||||
|
||||
if (isDevelopment) {
|
||||
return {
|
||||
...baseConfig,
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'SYS:standard',
|
||||
ignore: 'pid,hostname',
|
||||
singleLine: false,
|
||||
messageFormat: '{msg}',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
};
|
||||
|
||||
export const logger: Logger = pino(getPinoConfig());
|
||||
|
||||
export const createLogger = (name: string, context?: Record<string, any>): Logger => {
|
||||
return logger.child({ name, ...context });
|
||||
};
|
||||
|
||||
export default logger;
|
||||
|
|
@ -1,12 +1,36 @@
|
|||
import { createLogger } from "@link-stack/logger";
|
||||
import { Hono } from "hono";
|
||||
|
||||
import type WhatsappService from "./service.ts";
|
||||
import { createLogger } from "./lib/logger";
|
||||
|
||||
const logger = createLogger("bridge-whatsapp-routes");
|
||||
|
||||
const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
|
||||
export function createRoutes(service: WhatsappService): Hono {
|
||||
const app = new Hono();
|
||||
|
||||
app.post("/api/bots/:id/register", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
try {
|
||||
await service.register(id);
|
||||
logger.info({ id }, "Bot registered");
|
||||
return c.body(null, 200);
|
||||
} catch (error) {
|
||||
logger.error({ id, error: errorMessage(error) }, "Failed to register bot");
|
||||
return c.json({ error: errorMessage(error) }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/bots/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
try {
|
||||
return c.json(service.getBot(id));
|
||||
} catch (error) {
|
||||
return c.json({ error: errorMessage(error) }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/bots/:id/send", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const { phoneNumber, message, attachments } = await c.req.json<{
|
||||
|
|
@ -15,43 +39,30 @@ export function createRoutes(service: WhatsappService): Hono {
|
|||
attachments?: Array<{ data: string; filename: string; mime_type: string }>;
|
||||
}>();
|
||||
|
||||
await service.send(id, phoneNumber, message, attachments);
|
||||
logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message");
|
||||
|
||||
return c.json({
|
||||
result: {
|
||||
recipient: phoneNumber,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/bots/:id/receive", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const date = new Date();
|
||||
const twoDaysAgo = new Date(date.getTime());
|
||||
twoDaysAgo.setDate(date.getDate() - 2);
|
||||
|
||||
const messages = await service.receive(id, twoDaysAgo);
|
||||
return c.json(messages);
|
||||
});
|
||||
|
||||
app.post("/api/bots/:id/register", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
await service.register(id);
|
||||
return c.body(null, 200);
|
||||
try {
|
||||
const result = await service.send(id, phoneNumber, message, attachments);
|
||||
logger.info({ id, attachmentCount: attachments?.length || 0 }, "Sent message");
|
||||
return c.json({ result });
|
||||
} catch (error) {
|
||||
logger.error({ id, error: errorMessage(error) }, "Failed to send message");
|
||||
return c.json({ error: errorMessage(error) }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/bots/:id/unverify", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
await service.unverify(id);
|
||||
return c.body(null, 200);
|
||||
try {
|
||||
await service.unverify(id);
|
||||
logger.info({ id }, "Bot unverified");
|
||||
return c.body(null, 200);
|
||||
} catch (error) {
|
||||
logger.error({ id, error: errorMessage(error) }, "Failed to unverify bot");
|
||||
return c.json({ error: errorMessage(error) }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/bots/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
return c.json(service.getBot(id));
|
||||
app.get("/api/health", (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
return app;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import fs from "node:fs";
|
||||
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
import makeWASocket, {
|
||||
type WASocket,
|
||||
type SocketConfig,
|
||||
DisconnectReason,
|
||||
proto,
|
||||
downloadContentFromMessage,
|
||||
|
|
@ -8,22 +13,21 @@ import makeWASocket, {
|
|||
useMultiFileAuthState,
|
||||
} from "@whiskeysockets/baileys";
|
||||
|
||||
import { getMaxAttachmentSize, getMaxTotalAttachmentSize, MAX_ATTACHMENTS } from "./attachments";
|
||||
|
||||
type MediaType = "audio" | "document" | "image" | "video" | "sticker";
|
||||
import fs from "fs";
|
||||
import { createLogger } from "./lib/logger";
|
||||
import {
|
||||
getMaxAttachmentSize,
|
||||
getMaxTotalAttachmentSize,
|
||||
MAX_ATTACHMENTS,
|
||||
} from "./attachments";
|
||||
|
||||
const logger = createLogger("bridge-whatsapp-service");
|
||||
|
||||
export type AuthCompleteCallback = (error?: string) => void;
|
||||
|
||||
interface BotConnection {
|
||||
socket: WASocket;
|
||||
}
|
||||
|
||||
export default class WhatsappService {
|
||||
connections: { [key: string]: any } = {};
|
||||
loginConnections: { [key: string]: any } = {};
|
||||
connections: Record<string, BotConnection> = {};
|
||||
loginConnections: Record<string, BotConnection> = {};
|
||||
|
||||
static browserDescription: [string, string, string] = ["Bridge", "Chrome", "2.0"];
|
||||
|
||||
|
|
@ -71,7 +75,7 @@ export default class WhatsappService {
|
|||
private async resetConnections() {
|
||||
for (const connection of Object.values(this.connections)) {
|
||||
try {
|
||||
connection.end(null);
|
||||
connection.socket.end(new Error("Connection reset"));
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Connection reset error");
|
||||
}
|
||||
|
|
@ -81,18 +85,16 @@ export default class WhatsappService {
|
|||
|
||||
private async createConnection(
|
||||
botID: string,
|
||||
options: any,
|
||||
authCompleteCallback?: any,
|
||||
options: Partial<SocketConfig>,
|
||||
authCompleteCallback?: AuthCompleteCallback
|
||||
) {
|
||||
const authDirectory = this.getAuthDirectory(botID);
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authDirectory);
|
||||
const msgRetryCounterMap: any = {};
|
||||
const socket = makeWASocket({
|
||||
...options,
|
||||
auth: state,
|
||||
generateHighQualityLinkPreview: false,
|
||||
syncFullHistory: true,
|
||||
msgRetryCounterMap,
|
||||
shouldIgnoreJid: (jid) => isJidBroadcast(jid) || isJidStatusBroadcast(jid),
|
||||
});
|
||||
let pause = 5000;
|
||||
|
|
@ -115,7 +117,8 @@ export default class WhatsappService {
|
|||
logger.info("opened connection");
|
||||
} else if (connectionState === "close") {
|
||||
logger.info({ lastDisconnect }, "connection closed");
|
||||
const disconnectStatusCode = (lastDisconnect?.error as any)?.output?.statusCode;
|
||||
const disconnectStatusCode = (lastDisconnect?.error as { output?: { statusCode?: number } } | undefined)
|
||||
?.output?.statusCode;
|
||||
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
||||
logger.info("reconnecting after got new login");
|
||||
await this.createConnection(botID, options);
|
||||
|
|
@ -145,17 +148,14 @@ export default class WhatsappService {
|
|||
|
||||
if (events["messaging-history.set"]) {
|
||||
const { messages, isLatest } = events["messaging-history.set"];
|
||||
logger.info(
|
||||
{ messageCount: messages.length, isLatest },
|
||||
"received message history on connection",
|
||||
);
|
||||
logger.info({ messageCount: messages.length, isLatest }, "received message history on connection");
|
||||
if (messages.length > 0) {
|
||||
await this.queueUnreadMessages(botID, messages);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.connections[botID] = { socket, msgRetryCounterMap };
|
||||
this.connections[botID] = { socket };
|
||||
}
|
||||
|
||||
private async updateConnections() {
|
||||
|
|
@ -188,17 +188,13 @@ export default class WhatsappService {
|
|||
const { id, fromMe, remoteJid } = key;
|
||||
// Baileys 7 uses LIDs (Linked IDs) instead of phone numbers in some cases.
|
||||
// senderPn contains the actual phone number when available.
|
||||
const senderPn = (key as any).senderPn as string | undefined;
|
||||
const participantPn = (key as any).participantPn as string | undefined;
|
||||
logger.info(
|
||||
{ remoteJid, senderPn, participantPn, fromMe },
|
||||
"Processing incoming message",
|
||||
);
|
||||
const senderPn = (key as { senderPn?: string }).senderPn;
|
||||
const participantPn = (key as { participantPn?: string }).participantPn;
|
||||
logger.info({ remoteJid, senderPn, participantPn, fromMe }, "Processing incoming message");
|
||||
const isValidMessage = message && remoteJid !== "status@broadcast" && !fromMe;
|
||||
if (isValidMessage) {
|
||||
const { audioMessage, documentMessage, imageMessage, videoMessage } = message;
|
||||
const isMediaMessage =
|
||||
audioMessage || documentMessage || imageMessage || videoMessage;
|
||||
const isMediaMessage = audioMessage || documentMessage || imageMessage || videoMessage;
|
||||
|
||||
const messageContent = Object.values(message)[0];
|
||||
let messageType: MediaType;
|
||||
|
|
@ -229,8 +225,8 @@ export default class WhatsappService {
|
|||
|
||||
const stream = await downloadContentFromMessage(
|
||||
messageContent,
|
||||
// @ts-ignore
|
||||
messageType,
|
||||
// @ts-expect-error messageType is dynamically resolved
|
||||
messageType
|
||||
);
|
||||
let buffer = Buffer.from([]);
|
||||
for await (const chunk of stream) {
|
||||
|
|
@ -244,12 +240,9 @@ export default class WhatsappService {
|
|||
const extendedTextMessage = message?.extendedTextMessage?.text;
|
||||
const imageMessage = message?.imageMessage?.caption;
|
||||
const videoMessage = message?.videoMessage?.caption;
|
||||
const messageText = [
|
||||
conversation,
|
||||
extendedTextMessage,
|
||||
imageMessage,
|
||||
videoMessage,
|
||||
].find((text) => text && text !== "");
|
||||
const messageText = [conversation, extendedTextMessage, imageMessage, videoMessage].find(
|
||||
(text) => text && text !== ""
|
||||
);
|
||||
|
||||
// Extract phone number and user ID (LID) separately
|
||||
// remoteJid may contain LIDs (Baileys 7+) which are not phone numbers
|
||||
|
|
@ -257,7 +250,8 @@ export default class WhatsappService {
|
|||
const isLidJid = remoteJid?.endsWith("@lid");
|
||||
|
||||
// Phone number: prefer senderPn/participantPn, fall back to remoteJid only if it's not a LID
|
||||
const senderPhone = senderPn?.split("@")[0] || participantPn?.split("@")[0] || (!isLidJid ? jidValue : undefined);
|
||||
const senderPhone =
|
||||
senderPn?.split("@")[0] || participantPn?.split("@")[0] || (isLidJid ? undefined : jidValue);
|
||||
|
||||
// User ID (LID): extract from remoteJid if it's a LID format
|
||||
const senderUserId = isLidJid ? jidValue : undefined;
|
||||
|
|
@ -271,27 +265,24 @@ export default class WhatsappService {
|
|||
const payload = {
|
||||
to: botID,
|
||||
from: senderPhone,
|
||||
userId: senderUserId,
|
||||
messageId: id,
|
||||
sentAt: new Date((messageTimestamp as number) * 1000).toISOString(),
|
||||
user_id: senderUserId,
|
||||
message_id: id,
|
||||
sent_at: new Date((messageTimestamp as number) * 1000).toISOString(),
|
||||
message: messageText,
|
||||
attachment,
|
||||
filename,
|
||||
mimeType,
|
||||
mime_type: mimeType,
|
||||
};
|
||||
|
||||
// Send directly to Zammad's WhatsApp webhook
|
||||
const zammadUrl = process.env.ZAMMAD_URL || 'http://zammad-nginx:8080';
|
||||
const response = await fetch(
|
||||
`${zammadUrl}/api/v1/channels_cdr_whatsapp_bot_webhook/${botID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080";
|
||||
const response = await fetch(`${zammadUrl}/api/v1/channels_cdr_whatsapp_bot_webhook/${botID}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
|
@ -307,7 +298,7 @@ export default class WhatsappService {
|
|||
}
|
||||
}
|
||||
|
||||
getBot(botID: string): Record<string, any> {
|
||||
getBot(botID: string): Record<string, unknown> {
|
||||
const botDirectory = this.getBotDirectory(botID);
|
||||
const qrPath = `${botDirectory}/qr.txt`;
|
||||
const verifiedFile = `${botDirectory}/verified`;
|
||||
|
|
@ -327,7 +318,7 @@ export default class WhatsappService {
|
|||
} catch (error) {
|
||||
logger.warn({ botID, error }, "Error during logout, forcing disconnect");
|
||||
try {
|
||||
connection.socket.end(undefined);
|
||||
connection.socket.end(new Error("Forced disconnect"));
|
||||
} catch (endError) {
|
||||
logger.warn({ botID, endError }, "Error ending socket connection");
|
||||
}
|
||||
|
|
@ -346,11 +337,7 @@ export default class WhatsappService {
|
|||
|
||||
async register(botID: string, callback?: AuthCompleteCallback): Promise<void> {
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
await this.createConnection(
|
||||
botID,
|
||||
{ version, browser: WhatsappService.browserDescription },
|
||||
callback,
|
||||
);
|
||||
await this.createConnection(botID, { version, browser: WhatsappService.browserDescription }, callback);
|
||||
callback?.();
|
||||
}
|
||||
|
||||
|
|
@ -358,10 +345,10 @@ export default class WhatsappService {
|
|||
botID: string,
|
||||
phoneNumber: string,
|
||||
message: string,
|
||||
attachments?: Array<{ data: string; filename: string; mime_type: string }>,
|
||||
): Promise<void> {
|
||||
attachments?: Array<{ data: string; filename: string; mime_type: string }>
|
||||
): Promise<{ recipient: string; timestamp: string; source: string }> {
|
||||
const connection = this.connections[botID]?.socket;
|
||||
const digits = phoneNumber.replace(/\D+/g, "");
|
||||
const digits = phoneNumber.replaceAll(/\D+/g, "");
|
||||
// LIDs are 15+ digits, phone numbers with country code are typically 10-14 digits
|
||||
const suffix = digits.length > 14 ? "@lid" : "@s.whatsapp.net";
|
||||
const recipient = `${digits}${suffix}`;
|
||||
|
|
@ -377,9 +364,7 @@ export default class WhatsappService {
|
|||
const MAX_TOTAL_SIZE = getMaxTotalAttachmentSize();
|
||||
|
||||
if (attachments.length > MAX_ATTACHMENTS) {
|
||||
throw new Error(
|
||||
`Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`,
|
||||
);
|
||||
throw new Error(`Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`);
|
||||
}
|
||||
|
||||
let totalSize = 0;
|
||||
|
|
@ -395,7 +380,7 @@ export default class WhatsappService {
|
|||
size: estimatedSize,
|
||||
maxSize: MAX_ATTACHMENT_SIZE,
|
||||
},
|
||||
"Attachment exceeds size limit, skipping",
|
||||
"Attachment exceeds size limit, skipping"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -407,7 +392,7 @@ export default class WhatsappService {
|
|||
totalSize,
|
||||
maxTotalSize: MAX_TOTAL_SIZE,
|
||||
},
|
||||
"Total attachment size exceeds limit, skipping remaining",
|
||||
"Total attachment size exceeds limit, skipping remaining"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -438,16 +423,11 @@ export default class WhatsappService {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async receive(
|
||||
_botID: string,
|
||||
_lastReceivedDate: Date,
|
||||
): Promise<proto.IWebMessageInfo[]> {
|
||||
throw new Error(
|
||||
"Message polling is no longer supported in Baileys 7.x. " +
|
||||
"Please configure a webhook to receive messages instead. " +
|
||||
"Messages are automatically forwarded to Zammad via ZAMMAD_URL/api/v1/channels_cdr_whatsapp_bot_webhook/{id}"
|
||||
);
|
||||
return {
|
||||
recipient: phoneNumber,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: botID,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,8 @@
|
|||
{
|
||||
"extends": "@link-stack/typescript-config/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "build/main",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"types": ["node"],
|
||||
"lib": ["es2022", "DOM"]
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/.*.ts"],
|
||||
"exclude": ["node_modules/**"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue