verybot 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Bot, InputFile } from "grammy";
|
|
2
|
+
import { transcribe } from "../../tts/transcribe.js";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import { markdownToTelegramHtmlChunks, escapeHtml } from "./markdown.js";
|
|
5
|
+
import { CommandRouter } from "../commands.js";
|
|
6
|
+
const TELEGRAM_TEXT_LIMIT = 4000;
|
|
7
|
+
/** Send chunked HTML to a Telegram chat. Handles markdown→HTML conversion + splitting. */
|
|
8
|
+
async function sendHtmlChunks(bot, chatId, text, resolveTableMode) {
|
|
9
|
+
const tableMode = resolveTableMode();
|
|
10
|
+
const chunks = markdownToTelegramHtmlChunks(text, TELEGRAM_TEXT_LIMIT, { tableMode });
|
|
11
|
+
if (chunks.length === 0 && text) {
|
|
12
|
+
// Fallback: if conversion produced nothing, send raw text
|
|
13
|
+
await bot.api.sendMessage(chatId, text);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const chunk of chunks) {
|
|
17
|
+
await bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function createTelegramChannel(opts) {
|
|
21
|
+
const { token, onMessage, onClear, listTeams, defaultTeamId, resolveTableMode = () => "code" } = opts;
|
|
22
|
+
const bot = new Bot(token);
|
|
23
|
+
const typingIntervals = new Map();
|
|
24
|
+
const commands = new CommandRouter({ onClear, listTeams, defaultTeamId });
|
|
25
|
+
/** Format a CommandResult using Telegram HTML syntax. */
|
|
26
|
+
function formatTelegramResult(result) {
|
|
27
|
+
return result.parts
|
|
28
|
+
.map((p) => {
|
|
29
|
+
if (typeof p === "string")
|
|
30
|
+
return escapeHtml(p);
|
|
31
|
+
if ("bold" in p)
|
|
32
|
+
return `<b>${escapeHtml(p.bold)}</b>`;
|
|
33
|
+
return `<code>${escapeHtml(p.code)}</code>`;
|
|
34
|
+
})
|
|
35
|
+
.join("");
|
|
36
|
+
}
|
|
37
|
+
function startTyping(chatId) {
|
|
38
|
+
stopTyping(chatId);
|
|
39
|
+
const numId = Number(chatId);
|
|
40
|
+
// Send immediately, then every 4s (Telegram typing expires after ~5s)
|
|
41
|
+
bot.api.sendChatAction(numId, "typing").catch((err) => logger.error(`typing error: ${err}`));
|
|
42
|
+
logger.info(`typing started for ${chatId}`);
|
|
43
|
+
const interval = setInterval(() => {
|
|
44
|
+
bot.api.sendChatAction(numId, "typing").catch((err) => logger.error(`typing interval error: ${err}`));
|
|
45
|
+
}, 4000);
|
|
46
|
+
typingIntervals.set(chatId, interval);
|
|
47
|
+
}
|
|
48
|
+
function stopTyping(chatId) {
|
|
49
|
+
const interval = typingIntervals.get(chatId);
|
|
50
|
+
if (interval) {
|
|
51
|
+
clearInterval(interval);
|
|
52
|
+
typingIntervals.delete(chatId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const channel = {
|
|
56
|
+
name: "telegram",
|
|
57
|
+
start: async () => {
|
|
58
|
+
for (const cmd of ["clear", "reset"]) {
|
|
59
|
+
bot.command(cmd, async (ctx) => {
|
|
60
|
+
const chatId = String(ctx.chat.id);
|
|
61
|
+
const result = await commands.handleClear("telegram", chatId);
|
|
62
|
+
await ctx.reply(formatTelegramResult(result), { parse_mode: "HTML" });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// --- /team command for multi-team switching ---
|
|
66
|
+
if (listTeams) {
|
|
67
|
+
bot.command("team", async (ctx) => {
|
|
68
|
+
try {
|
|
69
|
+
const chatId = String(ctx.chat.id);
|
|
70
|
+
const arg = ctx.match?.trim() ?? "";
|
|
71
|
+
const result = await commands.handleTeam("telegram", chatId, arg);
|
|
72
|
+
await ctx.reply(formatTelegramResult(result), { parse_mode: "HTML" });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
logger.error(`[telegram] /team command failed: ${msg}`);
|
|
77
|
+
await ctx.reply("Failed to process team command. Please try again.");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
bot.on("message:voice", async (ctx) => {
|
|
82
|
+
const chatId = String(ctx.chat.id);
|
|
83
|
+
logger.info(`[telegram] voice message received from ${chatId}`);
|
|
84
|
+
startTyping(chatId);
|
|
85
|
+
try {
|
|
86
|
+
const file = await ctx.getFile();
|
|
87
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
throw new Error(`Download failed: ${res.status}`);
|
|
91
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
92
|
+
const text = await transcribe(buffer, file.file_path ?? "voice.ogg");
|
|
93
|
+
logger.info(`[telegram] transcribed voice: "${text.slice(0, 100)}"`);
|
|
94
|
+
onMessage({
|
|
95
|
+
channelType: "telegram",
|
|
96
|
+
channelId: chatId,
|
|
97
|
+
userId: String(ctx.from.id),
|
|
98
|
+
text,
|
|
99
|
+
isVoice: true,
|
|
100
|
+
teamId: commands.resolveTeamId(chatId),
|
|
101
|
+
}, channel);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
stopTyping(chatId);
|
|
105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
logger.error(`[telegram] voice transcription failed: ${msg}`);
|
|
107
|
+
await ctx.reply("Sorry, I couldn't process your voice message.");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
bot.on("message:text", (ctx) => {
|
|
111
|
+
// Skip bot commands — registered ones are handled by bot.command(),
|
|
112
|
+
// unregistered ones (e.g. /help) should not be forwarded as text
|
|
113
|
+
const entities = ctx.message.entities ?? [];
|
|
114
|
+
if (entities.some((e) => e.type === "bot_command" && e.offset === 0))
|
|
115
|
+
return;
|
|
116
|
+
const chatId = String(ctx.chat.id);
|
|
117
|
+
logger.info(`[telegram] message received from ${chatId}`);
|
|
118
|
+
startTyping(chatId);
|
|
119
|
+
onMessage({
|
|
120
|
+
channelType: "telegram",
|
|
121
|
+
channelId: chatId,
|
|
122
|
+
userId: String(ctx.from.id),
|
|
123
|
+
text: ctx.message.text,
|
|
124
|
+
teamId: commands.resolveTeamId(chatId),
|
|
125
|
+
}, channel);
|
|
126
|
+
});
|
|
127
|
+
// Register commands with Telegram so they appear in the "/" menu
|
|
128
|
+
const menuCommands = [
|
|
129
|
+
{ command: "clear", description: "Clear the current session." },
|
|
130
|
+
{ command: "reset", description: "Clear the current session." },
|
|
131
|
+
];
|
|
132
|
+
if (listTeams) {
|
|
133
|
+
menuCommands.push({ command: "team", description: "List or switch teams." });
|
|
134
|
+
}
|
|
135
|
+
bot.start();
|
|
136
|
+
// setMyCommands after start() so Grammy has initialized the bot
|
|
137
|
+
bot.api.setMyCommands(menuCommands).then(() => logger.info(`[telegram] registered ${menuCommands.length} commands`), (err) => logger.error(`[telegram] setMyCommands failed: ${err}`));
|
|
138
|
+
},
|
|
139
|
+
stop: async () => {
|
|
140
|
+
for (const chatId of typingIntervals.keys())
|
|
141
|
+
stopTyping(chatId);
|
|
142
|
+
await bot.stop();
|
|
143
|
+
},
|
|
144
|
+
send: async (channelId, text) => {
|
|
145
|
+
stopTyping(channelId);
|
|
146
|
+
await sendHtmlChunks(bot, Number(channelId), text, resolveTableMode);
|
|
147
|
+
},
|
|
148
|
+
sendVoice: async (channelId, audioPath) => {
|
|
149
|
+
stopTyping(channelId);
|
|
150
|
+
const chatId = Number(channelId);
|
|
151
|
+
await bot.api.sendVoice(chatId, new InputFile(audioPath));
|
|
152
|
+
logger.info(`[telegram] voice message sent to ${channelId}`);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return channel;
|
|
156
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Telegram HTML conversion.
|
|
3
|
+
* Markdown to Telegram HTML conversion.
|
|
4
|
+
*/
|
|
5
|
+
import { type MarkdownTableMode } from "../../markdown/ir.js";
|
|
6
|
+
export declare function escapeHtml(text: string): string;
|
|
7
|
+
export declare function markdownToTelegramHtml(markdown: string, options?: {
|
|
8
|
+
tableMode?: MarkdownTableMode;
|
|
9
|
+
}): string;
|
|
10
|
+
/**
|
|
11
|
+
* Convert markdown to chunked Telegram HTML strings.
|
|
12
|
+
* Each chunk is ≤ `limit` characters of the plain-text IR (the HTML tags
|
|
13
|
+
* add overhead but Telegram counts the visible text).
|
|
14
|
+
*/
|
|
15
|
+
export declare function markdownToTelegramHtmlChunks(markdown: string, limit: number, options?: {
|
|
16
|
+
tableMode?: MarkdownTableMode;
|
|
17
|
+
}): string[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Telegram HTML conversion.
|
|
3
|
+
* Markdown to Telegram HTML conversion.
|
|
4
|
+
*/
|
|
5
|
+
import { chunkMarkdownIR, markdownToIR, } from "../../markdown/ir.js";
|
|
6
|
+
import { renderMarkdownWithMarkers } from "../../markdown/render.js";
|
|
7
|
+
export function escapeHtml(text) {
|
|
8
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
}
|
|
10
|
+
function escapeHtmlAttr(text) {
|
|
11
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
12
|
+
}
|
|
13
|
+
const SAFE_LINK_PROTOCOL = /^https?:\/\/|^mailto:|^tel:/i;
|
|
14
|
+
function buildTelegramLink(link, _text) {
|
|
15
|
+
const href = link.href.trim();
|
|
16
|
+
if (!href || link.start === link.end) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!SAFE_LINK_PROTOCOL.test(href)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const safeHref = escapeHtmlAttr(href);
|
|
23
|
+
return {
|
|
24
|
+
start: link.start,
|
|
25
|
+
end: link.end,
|
|
26
|
+
open: `<a href="${safeHref}">`,
|
|
27
|
+
close: "</a>",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function renderTelegramHtml(ir) {
|
|
31
|
+
return renderMarkdownWithMarkers(ir, {
|
|
32
|
+
styleMarkers: {
|
|
33
|
+
bold: { open: "<b>", close: "</b>" },
|
|
34
|
+
italic: { open: "<i>", close: "</i>" },
|
|
35
|
+
strikethrough: { open: "<s>", close: "</s>" },
|
|
36
|
+
code: { open: "<code>", close: "</code>" },
|
|
37
|
+
code_block: { open: "<pre><code>", close: "</code></pre>" },
|
|
38
|
+
},
|
|
39
|
+
escapeText: escapeHtml,
|
|
40
|
+
buildLink: buildTelegramLink,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function markdownToTelegramHtml(markdown, options = {}) {
|
|
44
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
45
|
+
linkify: true,
|
|
46
|
+
headingStyle: "none",
|
|
47
|
+
blockquotePrefix: "",
|
|
48
|
+
tableMode: options.tableMode,
|
|
49
|
+
});
|
|
50
|
+
return renderTelegramHtml(ir);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Convert markdown to chunked Telegram HTML strings.
|
|
54
|
+
* Each chunk is ≤ `limit` characters of the plain-text IR (the HTML tags
|
|
55
|
+
* add overhead but Telegram counts the visible text).
|
|
56
|
+
*/
|
|
57
|
+
export function markdownToTelegramHtmlChunks(markdown, limit, options = {}) {
|
|
58
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
59
|
+
linkify: true,
|
|
60
|
+
headingStyle: "none",
|
|
61
|
+
blockquotePrefix: "",
|
|
62
|
+
tableMode: options.tableMode,
|
|
63
|
+
});
|
|
64
|
+
const chunks = chunkMarkdownIR(ir, limit);
|
|
65
|
+
return chunks.map((chunk) => renderTelegramHtml(chunk));
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface IncomingMessage {
|
|
2
|
+
channelType: string;
|
|
3
|
+
channelId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
/** Whether this message originated from a voice input. */
|
|
7
|
+
isVoice?: boolean;
|
|
8
|
+
/** Team ID resolved by the channel (e.g. via /team command in Telegram). */
|
|
9
|
+
teamId?: string;
|
|
10
|
+
}
|
|
11
|
+
export type MessageHandler = (msg: IncomingMessage, channel: Channel) => Promise<void>;
|
|
12
|
+
export interface ChannelMessage {
|
|
13
|
+
user: string;
|
|
14
|
+
text: string;
|
|
15
|
+
ts: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Channel {
|
|
18
|
+
name: string;
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
send(channelId: string, text: string): Promise<void>;
|
|
22
|
+
/** Send a voice message. Falls back to text if not supported by the channel. */
|
|
23
|
+
sendVoice?(channelId: string, audioPath: string): Promise<void>;
|
|
24
|
+
/** Read recent messages from a channel/thread. */
|
|
25
|
+
readHistory?(channelId: string, limit?: number, threadTs?: string): Promise<ChannelMessage[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Channel, MessageHandler } from "../types.js";
|
|
2
|
+
import type { MarkdownTableMode } from "../../markdown/ir.js";
|
|
3
|
+
/** Callback that resolves the current table mode from config at send time. */
|
|
4
|
+
export type ResolveTableMode = () => MarkdownTableMode;
|
|
5
|
+
export interface CreateWhatsAppChannelOpts {
|
|
6
|
+
/**
|
|
7
|
+
* Opaque identifier for config gating and channel fingerprinting.
|
|
8
|
+
* Baileys authenticates via QR code; this value is not sent to WhatsApp.
|
|
9
|
+
* Set any non-empty string (e.g. your phone number) to enable the channel.
|
|
10
|
+
*/
|
|
11
|
+
phoneId: string;
|
|
12
|
+
/** When true, only process messages sent by myself (fromMe). */
|
|
13
|
+
selfOnly?: boolean;
|
|
14
|
+
onMessage: MessageHandler;
|
|
15
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
16
|
+
listTeams?: () => {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}[];
|
|
20
|
+
defaultTeamId?: string;
|
|
21
|
+
resolveTableMode?: ResolveTableMode;
|
|
22
|
+
}
|
|
23
|
+
export declare function createWhatsAppChannel(opts: CreateWhatsAppChannelOpts): Channel;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import makeWASocket, { useMultiFileAuthState, DisconnectReason, downloadMediaMessage, } from "@whiskeysockets/baileys";
|
|
2
|
+
import { transcribe } from "../../tts/transcribe.js";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import { markdownToWhatsAppChunks, escapeWhatsApp, WHATSAPP_TEXT_LIMIT } from "./markdown.js";
|
|
5
|
+
import { CommandRouter } from "../commands.js";
|
|
6
|
+
import { WHATSAPP_AUTH_DIR } from "../../paths.js";
|
|
7
|
+
import { emit } from "../../events.js";
|
|
8
|
+
import QRCode from "qrcode";
|
|
9
|
+
/** Typing indicator refresh interval (WhatsApp presence expires ~10s). */
|
|
10
|
+
const TYPING_INTERVAL_MS = 5000;
|
|
11
|
+
/** Initial reconnect delay (doubles on each attempt). */
|
|
12
|
+
const BASE_RECONNECT_DELAY_MS = 1000;
|
|
13
|
+
/** Cap reconnect backoff at 60s. */
|
|
14
|
+
const MAX_RECONNECT_DELAY_MS = 60_000;
|
|
15
|
+
/** Max number of sent message IDs to track for loop prevention. */
|
|
16
|
+
const SENT_IDS_LIMIT = 500;
|
|
17
|
+
/**
|
|
18
|
+
* Thin pino-compatible logger adapter that forwards to Winston.
|
|
19
|
+
* Baileys expects a pino-shaped logger with child(), level, and log methods.
|
|
20
|
+
*/
|
|
21
|
+
function createBaileysLogger() {
|
|
22
|
+
const noop = () => { };
|
|
23
|
+
const adapter = {
|
|
24
|
+
level: "silent",
|
|
25
|
+
trace: noop,
|
|
26
|
+
debug: noop,
|
|
27
|
+
info: (msg) => logger.debug(`[baileys] ${msg}`),
|
|
28
|
+
warn: (msg) => logger.warn(`[baileys] ${msg}`),
|
|
29
|
+
error: (msg) => logger.error(`[baileys] ${msg}`),
|
|
30
|
+
fatal: (msg) => logger.error(`[baileys:fatal] ${msg}`),
|
|
31
|
+
child: () => adapter,
|
|
32
|
+
};
|
|
33
|
+
return adapter;
|
|
34
|
+
}
|
|
35
|
+
export function createWhatsAppChannel(opts) {
|
|
36
|
+
const { onMessage, onClear, listTeams, defaultTeamId, selfOnly = false, resolveTableMode = () => "bullets", } = opts;
|
|
37
|
+
let sock = null;
|
|
38
|
+
let stopped = false;
|
|
39
|
+
let reconnectAttempts = 0;
|
|
40
|
+
const typingIntervals = new Map();
|
|
41
|
+
const commands = new CommandRouter({ onClear, listTeams, defaultTeamId });
|
|
42
|
+
/** IDs of messages sent by the bot — used to avoid processing our own replies. */
|
|
43
|
+
const sentIds = new Set();
|
|
44
|
+
function trackSentId(id) {
|
|
45
|
+
if (!id)
|
|
46
|
+
return;
|
|
47
|
+
sentIds.add(id);
|
|
48
|
+
if (sentIds.size > SENT_IDS_LIMIT) {
|
|
49
|
+
const first = sentIds.values().next().value;
|
|
50
|
+
sentIds.delete(first);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Format a CommandResult using WhatsApp formatting. */
|
|
54
|
+
function formatResult(result) {
|
|
55
|
+
return result.parts
|
|
56
|
+
.map((p) => {
|
|
57
|
+
if (typeof p === "string")
|
|
58
|
+
return escapeWhatsApp(p);
|
|
59
|
+
if ("bold" in p)
|
|
60
|
+
return `*${escapeWhatsApp(p.bold)}*`;
|
|
61
|
+
return `\`${p.code}\``;
|
|
62
|
+
})
|
|
63
|
+
.join("");
|
|
64
|
+
}
|
|
65
|
+
function startTyping(jid) {
|
|
66
|
+
stopTyping(jid);
|
|
67
|
+
sock?.sendPresenceUpdate("composing", jid).catch((err) => logger.error(`[whatsapp] typing error: ${err}`));
|
|
68
|
+
const interval = setInterval(() => {
|
|
69
|
+
sock?.sendPresenceUpdate("composing", jid).catch((err) => logger.error(`[whatsapp] typing interval error: ${err}`));
|
|
70
|
+
}, TYPING_INTERVAL_MS);
|
|
71
|
+
typingIntervals.set(jid, interval);
|
|
72
|
+
}
|
|
73
|
+
function stopTyping(jid) {
|
|
74
|
+
const interval = typingIntervals.get(jid);
|
|
75
|
+
if (interval) {
|
|
76
|
+
clearInterval(interval);
|
|
77
|
+
typingIntervals.delete(jid);
|
|
78
|
+
}
|
|
79
|
+
sock?.sendPresenceUpdate("paused", jid).catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
function clearAllTyping() {
|
|
82
|
+
for (const jid of typingIntervals.keys())
|
|
83
|
+
stopTyping(jid);
|
|
84
|
+
}
|
|
85
|
+
async function connectSocket() {
|
|
86
|
+
// Clear stale typing intervals from previous connection
|
|
87
|
+
clearAllTyping();
|
|
88
|
+
const { state, saveCreds } = await useMultiFileAuthState(WHATSAPP_AUTH_DIR);
|
|
89
|
+
sock = makeWASocket({
|
|
90
|
+
auth: state,
|
|
91
|
+
logger: createBaileysLogger(),
|
|
92
|
+
printQRInTerminal: false, // QR is sent to UI via event bus
|
|
93
|
+
});
|
|
94
|
+
sock.ev.on("creds.update", saveCreds);
|
|
95
|
+
sock.ev.on("connection.update", async (update) => {
|
|
96
|
+
const { connection, lastDisconnect, qr } = update;
|
|
97
|
+
// Broadcast QR code to UI clients
|
|
98
|
+
if (qr) {
|
|
99
|
+
try {
|
|
100
|
+
const dataUrl = await QRCode.toDataURL(qr, { width: 256 });
|
|
101
|
+
emit("whatsapp", { type: "qr", dataUrl });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.error(`[whatsapp] QR generation failed: ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (connection === "close") {
|
|
108
|
+
emit("whatsapp", { type: "disconnected" });
|
|
109
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
110
|
+
const isLoggedOut = statusCode === DisconnectReason.loggedOut;
|
|
111
|
+
if (isLoggedOut) {
|
|
112
|
+
logger.warn("[whatsapp] logged out — will not reconnect");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!stopped) {
|
|
116
|
+
const delay = Math.min(BASE_RECONNECT_DELAY_MS * 2 ** reconnectAttempts, MAX_RECONNECT_DELAY_MS);
|
|
117
|
+
reconnectAttempts++;
|
|
118
|
+
logger.info(`[whatsapp] disconnected (code ${statusCode}), reconnecting in ${delay}ms...`);
|
|
119
|
+
setTimeout(() => connectSocket(), delay);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (connection === "open") {
|
|
123
|
+
reconnectAttempts = 0;
|
|
124
|
+
emit("whatsapp", { type: "connected" });
|
|
125
|
+
logger.info("[whatsapp] connected");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
sock.ev.on("messages.upsert", async ({ messages, type }) => {
|
|
129
|
+
logger.info(`[whatsapp] messages.upsert: type=${type}, count=${messages.length}`);
|
|
130
|
+
for (const msg of messages) {
|
|
131
|
+
try {
|
|
132
|
+
logger.info(`[whatsapp] msg: fromMe=${msg.key.fromMe}, jid=${msg.key.remoteJid}, id=${msg.key.id}, hasMessage=${!!msg.message}`);
|
|
133
|
+
if (!msg.message)
|
|
134
|
+
continue;
|
|
135
|
+
// Skip messages sent by the bot to avoid loops
|
|
136
|
+
if (msg.key.id && sentIds.has(msg.key.id))
|
|
137
|
+
continue;
|
|
138
|
+
// In self-only mode, ignore messages from others
|
|
139
|
+
if (selfOnly && !msg.key.fromMe)
|
|
140
|
+
continue;
|
|
141
|
+
const jid = msg.key.remoteJid;
|
|
142
|
+
if (!jid)
|
|
143
|
+
continue;
|
|
144
|
+
const userId = msg.key.participant ?? jid;
|
|
145
|
+
// Voice message handling
|
|
146
|
+
const audioMsg = msg.message.audioMessage;
|
|
147
|
+
if (audioMsg?.ptt) {
|
|
148
|
+
logger.info(`[whatsapp] voice message received`);
|
|
149
|
+
startTyping(jid);
|
|
150
|
+
try {
|
|
151
|
+
const buffer = await downloadMediaMessage(msg, "buffer", {});
|
|
152
|
+
const text = await transcribe(buffer, "voice.ogg");
|
|
153
|
+
logger.info(`[whatsapp] transcribed voice (${text.length} chars)`);
|
|
154
|
+
await onMessage({
|
|
155
|
+
channelType: "whatsapp",
|
|
156
|
+
channelId: jid,
|
|
157
|
+
userId,
|
|
158
|
+
text,
|
|
159
|
+
isVoice: true,
|
|
160
|
+
teamId: commands.resolveTeamId(jid),
|
|
161
|
+
}, channel);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
stopTyping(jid);
|
|
165
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
166
|
+
logger.error(`[whatsapp] voice transcription failed: ${errMsg}`);
|
|
167
|
+
const errReply = await sock?.sendMessage(jid, { text: "Sorry, I couldn't process your voice message." });
|
|
168
|
+
trackSentId(errReply?.key?.id);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Text message handling
|
|
173
|
+
const text = msg.message.conversation ??
|
|
174
|
+
msg.message.extendedTextMessage?.text;
|
|
175
|
+
if (!text)
|
|
176
|
+
continue;
|
|
177
|
+
// Try command handling first
|
|
178
|
+
const cmdResult = await commands.handle("whatsapp", jid, text);
|
|
179
|
+
if (cmdResult) {
|
|
180
|
+
const cmdReply = await sock?.sendMessage(jid, { text: formatResult(cmdResult) });
|
|
181
|
+
trackSentId(cmdReply?.key?.id);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
logger.info(`[whatsapp] message received`);
|
|
185
|
+
startTyping(jid);
|
|
186
|
+
await onMessage({
|
|
187
|
+
channelType: "whatsapp",
|
|
188
|
+
channelId: jid,
|
|
189
|
+
userId,
|
|
190
|
+
text,
|
|
191
|
+
teamId: commands.resolveTeamId(jid),
|
|
192
|
+
}, channel);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
196
|
+
logger.error(`[whatsapp] error handling message: ${errMsg}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const channel = {
|
|
202
|
+
name: "whatsapp",
|
|
203
|
+
start: async () => {
|
|
204
|
+
stopped = false;
|
|
205
|
+
reconnectAttempts = 0;
|
|
206
|
+
await connectSocket();
|
|
207
|
+
},
|
|
208
|
+
stop: async () => {
|
|
209
|
+
stopped = true;
|
|
210
|
+
clearAllTyping();
|
|
211
|
+
sock?.end(undefined);
|
|
212
|
+
sock = null;
|
|
213
|
+
},
|
|
214
|
+
send: async (channelId, text) => {
|
|
215
|
+
stopTyping(channelId);
|
|
216
|
+
const tableMode = resolveTableMode();
|
|
217
|
+
const chunks = markdownToWhatsAppChunks(text, WHATSAPP_TEXT_LIMIT, { tableMode });
|
|
218
|
+
if (chunks.length === 0 && text) {
|
|
219
|
+
const sent = await sock?.sendMessage(channelId, { text });
|
|
220
|
+
trackSentId(sent?.key?.id);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const chunk of chunks) {
|
|
224
|
+
const sent = await sock?.sendMessage(channelId, { text: chunk });
|
|
225
|
+
trackSentId(sent?.key?.id);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
sendVoice: async (channelId, audioPath) => {
|
|
229
|
+
stopTyping(channelId);
|
|
230
|
+
const { readFile } = await import("fs/promises");
|
|
231
|
+
const audio = await readFile(audioPath);
|
|
232
|
+
const sent = await sock?.sendMessage(channelId, {
|
|
233
|
+
audio,
|
|
234
|
+
mimetype: "audio/ogg; codecs=opus",
|
|
235
|
+
ptt: true,
|
|
236
|
+
});
|
|
237
|
+
trackSentId(sent?.key?.id);
|
|
238
|
+
logger.info(`[whatsapp] voice message sent`);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
return channel;
|
|
242
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → WhatsApp text conversion.
|
|
3
|
+
* WhatsApp formatting: *bold*, _italic_, ~strikethrough~, `code`, ```code block```
|
|
4
|
+
* No link syntax — WhatsApp auto-linkifies URLs.
|
|
5
|
+
*/
|
|
6
|
+
import { type MarkdownTableMode } from "../../markdown/ir.js";
|
|
7
|
+
export declare function escapeWhatsApp(text: string): string;
|
|
8
|
+
/** Single-shot conversion (no chunking). */
|
|
9
|
+
export declare function markdownToWhatsApp(markdown: string, options?: {
|
|
10
|
+
tableMode?: MarkdownTableMode;
|
|
11
|
+
}): string;
|
|
12
|
+
/** WhatsApp message size limit. */
|
|
13
|
+
export declare const WHATSAPP_TEXT_LIMIT = 4096;
|
|
14
|
+
/**
|
|
15
|
+
* Convert markdown to chunked WhatsApp text strings.
|
|
16
|
+
* Each chunk is ≤ `limit` characters.
|
|
17
|
+
*/
|
|
18
|
+
export declare function markdownToWhatsAppChunks(markdown: string, limit: number, options?: {
|
|
19
|
+
tableMode?: MarkdownTableMode;
|
|
20
|
+
}): string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → WhatsApp text conversion.
|
|
3
|
+
* WhatsApp formatting: *bold*, _italic_, ~strikethrough~, `code`, ```code block```
|
|
4
|
+
* No link syntax — WhatsApp auto-linkifies URLs.
|
|
5
|
+
*/
|
|
6
|
+
import { chunkMarkdownIR, markdownToIR, } from "../../markdown/ir.js";
|
|
7
|
+
import { renderMarkdownWithMarkers } from "../../markdown/render.js";
|
|
8
|
+
/** Characters that have formatting meaning in WhatsApp. */
|
|
9
|
+
const WA_SPECIAL = /[*_~`]/g;
|
|
10
|
+
export function escapeWhatsApp(text) {
|
|
11
|
+
return text.replace(WA_SPECIAL, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
function renderWhatsApp(ir) {
|
|
14
|
+
return renderMarkdownWithMarkers(ir, {
|
|
15
|
+
styleMarkers: {
|
|
16
|
+
bold: { open: "*", close: "*" },
|
|
17
|
+
italic: { open: "_", close: "_" },
|
|
18
|
+
strikethrough: { open: "~", close: "~" },
|
|
19
|
+
code: { open: "`", close: "`" },
|
|
20
|
+
code_block: { open: "```\n", close: "\n```" },
|
|
21
|
+
},
|
|
22
|
+
escapeText: escapeWhatsApp,
|
|
23
|
+
// No buildLink — WhatsApp auto-linkifies URLs
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Single-shot conversion (no chunking). */
|
|
27
|
+
export function markdownToWhatsApp(markdown, options = {}) {
|
|
28
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
29
|
+
linkify: true,
|
|
30
|
+
headingStyle: "none",
|
|
31
|
+
blockquotePrefix: "",
|
|
32
|
+
tableMode: options.tableMode,
|
|
33
|
+
});
|
|
34
|
+
return renderWhatsApp(ir);
|
|
35
|
+
}
|
|
36
|
+
/** WhatsApp message size limit. */
|
|
37
|
+
export const WHATSAPP_TEXT_LIMIT = 4096;
|
|
38
|
+
/**
|
|
39
|
+
* Convert markdown to chunked WhatsApp text strings.
|
|
40
|
+
* Each chunk is ≤ `limit` characters.
|
|
41
|
+
*/
|
|
42
|
+
export function markdownToWhatsAppChunks(markdown, limit, options = {}) {
|
|
43
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
44
|
+
linkify: true,
|
|
45
|
+
headingStyle: "none",
|
|
46
|
+
blockquotePrefix: "",
|
|
47
|
+
tableMode: options.tableMode,
|
|
48
|
+
});
|
|
49
|
+
const chunks = chunkMarkdownIR(ir, limit);
|
|
50
|
+
return chunks.map((chunk) => renderWhatsApp(chunk));
|
|
51
|
+
}
|