@johpaz/hive-core 0.1.1
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.
- package/package.json +43 -0
- package/src/agent/compaction.ts +161 -0
- package/src/agent/context-guard.ts +91 -0
- package/src/agent/context.ts +148 -0
- package/src/agent/ethics.ts +102 -0
- package/src/agent/hooks.ts +166 -0
- package/src/agent/index.ts +67 -0
- package/src/agent/providers/index.ts +278 -0
- package/src/agent/providers.ts +1 -0
- package/src/agent/soul.ts +89 -0
- package/src/agent/stuck-loop.ts +133 -0
- package/src/agent/user.ts +86 -0
- package/src/channels/base.ts +91 -0
- package/src/channels/discord.ts +185 -0
- package/src/channels/index.ts +7 -0
- package/src/channels/manager.ts +204 -0
- package/src/channels/slack.ts +209 -0
- package/src/channels/telegram.ts +177 -0
- package/src/channels/webchat.ts +83 -0
- package/src/channels/whatsapp.ts +305 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +508 -0
- package/src/gateway/index.ts +5 -0
- package/src/gateway/lane-queue.ts +169 -0
- package/src/gateway/router.ts +124 -0
- package/src/gateway/server.ts +347 -0
- package/src/gateway/session.ts +131 -0
- package/src/gateway/slash-commands.ts +176 -0
- package/src/heartbeat/index.ts +157 -0
- package/src/index.ts +21 -0
- package/src/memory/index.ts +1 -0
- package/src/memory/notes.ts +170 -0
- package/src/multi-agent/bindings.ts +171 -0
- package/src/multi-agent/index.ts +4 -0
- package/src/multi-agent/manager.ts +182 -0
- package/src/multi-agent/sandbox.ts +130 -0
- package/src/multi-agent/subagents.ts +302 -0
- package/src/security/index.ts +187 -0
- package/src/tools/cron.ts +156 -0
- package/src/tools/exec.ts +105 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/memory.ts +176 -0
- package/src/tools/notify.ts +53 -0
- package/src/tools/read.ts +154 -0
- package/src/tools/registry.ts +115 -0
- package/src/tools/web.ts +186 -0
- package/src/utils/crypto.ts +73 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/logger.ts +254 -0
- package/src/utils/retry.ts +70 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Bot, GrammyError, type Context } from "grammy";
|
|
2
|
+
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
export interface TelegramConfig extends ChannelConfig {
|
|
6
|
+
botToken: string;
|
|
7
|
+
groups?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class TelegramChannel extends BaseChannel {
|
|
11
|
+
name = "telegram";
|
|
12
|
+
accountId: string;
|
|
13
|
+
config: TelegramConfig;
|
|
14
|
+
|
|
15
|
+
private bot?: Bot;
|
|
16
|
+
private log = logger.child("telegram");
|
|
17
|
+
|
|
18
|
+
constructor(accountId: string, config: TelegramConfig) {
|
|
19
|
+
super();
|
|
20
|
+
this.accountId = accountId;
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async start(): Promise<void> {
|
|
25
|
+
if (!this.config.botToken) {
|
|
26
|
+
throw new Error("Telegram bot token not configured");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.bot = new Bot(this.config.botToken);
|
|
30
|
+
|
|
31
|
+
this.bot.on("message:text", async (ctx) => {
|
|
32
|
+
await this.handleTelegramMessage(ctx);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.bot.on("edited_message:text", async (ctx) => {
|
|
36
|
+
await this.handleTelegramMessage(ctx);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.bot.catch((err) => {
|
|
40
|
+
this.log.error(`Telegram error: ${err.message}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await this.bot.init();
|
|
45
|
+
this.running = true;
|
|
46
|
+
|
|
47
|
+
this.bot.start({
|
|
48
|
+
onStart: () => {
|
|
49
|
+
this.log.info(`Telegram bot started: @${this.bot?.botInfo?.username ?? "unknown"}`);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.log.error(`Failed to start Telegram bot: ${(error as Error).message}`);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async handleTelegramMessage(ctx: Context): Promise<void> {
|
|
59
|
+
const message = ctx.message ?? ctx.editedMessage;
|
|
60
|
+
if (!message?.text) return;
|
|
61
|
+
|
|
62
|
+
const chatId = message.chat.id.toString();
|
|
63
|
+
const isGroup = message.chat.type === "group" || message.chat.type === "supergroup";
|
|
64
|
+
const kind = isGroup ? "group" : "direct";
|
|
65
|
+
const peerId = isGroup
|
|
66
|
+
? `${message.chat.id}:${message.from?.id ?? "unknown"}`
|
|
67
|
+
: chatId;
|
|
68
|
+
|
|
69
|
+
if (!isGroup && !this.isUserAllowed(chatId)) {
|
|
70
|
+
this.log.debug(`Message from unauthorized user: ${chatId}`);
|
|
71
|
+
await ctx.reply("Sorry, you are not authorized to use this bot.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isGroup && !this.config.groups) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const incomingMessage: IncomingMessage = {
|
|
80
|
+
sessionId: this.formatSessionId(peerId, kind),
|
|
81
|
+
channel: "telegram",
|
|
82
|
+
accountId: this.accountId,
|
|
83
|
+
peerId,
|
|
84
|
+
peerKind: kind,
|
|
85
|
+
content: message.text,
|
|
86
|
+
metadata: {
|
|
87
|
+
telegram: {
|
|
88
|
+
chatId: message.chat.id,
|
|
89
|
+
userId: message.from?.id,
|
|
90
|
+
username: message.from?.username,
|
|
91
|
+
messageId: message.message_id,
|
|
92
|
+
chatType: message.chat.type,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
replyToId: message.reply_to_message
|
|
96
|
+
? `tg:${message.reply_to_message.message_id}`
|
|
97
|
+
: undefined,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await this.handleMessage(incomingMessage);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async stop(): Promise<void> {
|
|
104
|
+
if (this.bot) {
|
|
105
|
+
this.bot.stop();
|
|
106
|
+
this.running = false;
|
|
107
|
+
this.log.info("Telegram bot stopped");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
112
|
+
if (!this.bot) {
|
|
113
|
+
throw new Error("Telegram bot not started");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parts = sessionId.split(":");
|
|
117
|
+
const peerPart = parts.slice(3).join(":");
|
|
118
|
+
const chatIdStr = peerPart?.split(":")[0] ?? peerPart;
|
|
119
|
+
const chatId = Number(chatIdStr);
|
|
120
|
+
|
|
121
|
+
if (isNaN(chatId)) {
|
|
122
|
+
throw new Error(`Invalid chat ID from session: ${sessionId}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = message.content ?? "";
|
|
126
|
+
const maxLength = 4096;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (content.length <= maxLength) {
|
|
130
|
+
await this.bot.api.sendMessage(chatId, content, { parse_mode: "Markdown" });
|
|
131
|
+
} else {
|
|
132
|
+
const chunks = this.chunkMessage(content, maxLength);
|
|
133
|
+
for (const chunk of chunks) {
|
|
134
|
+
await this.bot.api.sendMessage(chatId, chunk, { parse_mode: "Markdown" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof GrammyError) {
|
|
139
|
+
this.log.error(`Telegram API error: ${error.description}`);
|
|
140
|
+
|
|
141
|
+
if (error.error_code === 403) {
|
|
142
|
+
this.log.warn(`Bot was blocked by user: ${chatId}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private chunkMessage(content: string, maxLength: number): string[] {
|
|
150
|
+
const chunks: string[] = [];
|
|
151
|
+
let remaining = content;
|
|
152
|
+
|
|
153
|
+
while (remaining.length > 0) {
|
|
154
|
+
if (remaining.length <= maxLength) {
|
|
155
|
+
chunks.push(remaining);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let splitPoint = remaining.lastIndexOf("\n", maxLength);
|
|
160
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
161
|
+
splitPoint = remaining.lastIndexOf(" ", maxLength);
|
|
162
|
+
}
|
|
163
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
164
|
+
splitPoint = maxLength;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
chunks.push(remaining.slice(0, splitPoint));
|
|
168
|
+
remaining = remaining.slice(splitPoint).trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return chunks;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createTelegramChannel(accountId: string, config: TelegramConfig): TelegramChannel {
|
|
176
|
+
return new TelegramChannel(accountId, config);
|
|
177
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
export interface WebChatConfig extends ChannelConfig {
|
|
6
|
+
// WebChat doesn't need extra config, it's served from the gateway
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface WebSocketData {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
peerId: string;
|
|
12
|
+
authenticatedAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class WebChatChannel extends BaseChannel {
|
|
16
|
+
name = "webchat";
|
|
17
|
+
accountId = "default";
|
|
18
|
+
config: WebChatConfig;
|
|
19
|
+
|
|
20
|
+
private connections: Map<string, ServerWebSocket<WebSocketData>> = new Map();
|
|
21
|
+
private log = logger.child("webchat");
|
|
22
|
+
|
|
23
|
+
constructor(config: WebChatConfig) {
|
|
24
|
+
super();
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async start(): Promise<void> {
|
|
29
|
+
this.running = true;
|
|
30
|
+
this.log.info("WebChat channel ready");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async stop(): Promise<void> {
|
|
34
|
+
this.connections.clear();
|
|
35
|
+
this.running = false;
|
|
36
|
+
this.log.info("WebChat channel stopped");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
registerConnection(ws: ServerWebSocket<WebSocketData>): void {
|
|
40
|
+
const data = ws.data as WebSocketData;
|
|
41
|
+
this.connections.set(data.sessionId, ws);
|
|
42
|
+
this.log.debug(`WebChat connection registered: ${data.sessionId}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
unregisterConnection(sessionId: string): void {
|
|
46
|
+
this.connections.delete(sessionId);
|
|
47
|
+
this.log.debug(`WebChat connection unregistered: ${sessionId}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
51
|
+
const ws = this.connections.get(sessionId);
|
|
52
|
+
|
|
53
|
+
if (!ws) {
|
|
54
|
+
this.log.warn(`No WebChat connection for session: ${sessionId}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
ws.send(JSON.stringify(message));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.log.error(`Failed to send WebChat message: ${(error as Error).message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createIncomingMessage(
|
|
66
|
+
sessionId: string,
|
|
67
|
+
content: string,
|
|
68
|
+
peerId: string
|
|
69
|
+
): IncomingMessage {
|
|
70
|
+
return {
|
|
71
|
+
sessionId,
|
|
72
|
+
channel: "webchat",
|
|
73
|
+
accountId: this.accountId,
|
|
74
|
+
peerId,
|
|
75
|
+
peerKind: "direct",
|
|
76
|
+
content,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createWebChatChannel(config: WebChatConfig): WebChatChannel {
|
|
82
|
+
return new WebChatChannel(config);
|
|
83
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import makeWASocket, {
|
|
2
|
+
DisconnectReason,
|
|
3
|
+
useMultiFileAuthState,
|
|
4
|
+
type WASocket,
|
|
5
|
+
type ConnectionState,
|
|
6
|
+
} from "@whiskeysockets/baileys";
|
|
7
|
+
import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts";
|
|
8
|
+
import { BaseChannel } from "./base.ts";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { logger } from "../utils/logger.ts";
|
|
12
|
+
|
|
13
|
+
export interface WhatsAppConfig extends ChannelConfig {
|
|
14
|
+
accountId: string;
|
|
15
|
+
agentId: string;
|
|
16
|
+
reconnectMaxAttempts?: number;
|
|
17
|
+
reconnectBaseDelayMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WhatsAppConnectionState {
|
|
21
|
+
status: "connecting" | "connected" | "disconnected" | "qr" | "error";
|
|
22
|
+
qrCode?: string;
|
|
23
|
+
lastConnected?: Date;
|
|
24
|
+
reconnectAttempts: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WhatsAppChannel extends BaseChannel {
|
|
29
|
+
name = "whatsapp";
|
|
30
|
+
accountId: string;
|
|
31
|
+
config: WhatsAppConfig;
|
|
32
|
+
|
|
33
|
+
private socket: WASocket | null = null;
|
|
34
|
+
private connectionState: WhatsAppConnectionState = {
|
|
35
|
+
status: "disconnected",
|
|
36
|
+
reconnectAttempts: 0,
|
|
37
|
+
};
|
|
38
|
+
private authPath: string;
|
|
39
|
+
private reconnectTimeout: Timer | null = null;
|
|
40
|
+
private log = logger.child("whatsapp");
|
|
41
|
+
|
|
42
|
+
constructor(config: WhatsAppConfig) {
|
|
43
|
+
super();
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.accountId = config.accountId;
|
|
46
|
+
this.authPath = this.getAuthPath(config.agentId, config.accountId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private getAuthPath(agentId: string, accountId: string): string {
|
|
50
|
+
const baseDir = process.env.HOME ?? "";
|
|
51
|
+
const authDir = path.join(baseDir, ".hive", "agents", agentId, "whatsapp", accountId);
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(authDir)) {
|
|
54
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return authDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async start(): Promise<void> {
|
|
61
|
+
this.running = true;
|
|
62
|
+
await this.connect();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async stop(): Promise<void> {
|
|
66
|
+
this.running = false;
|
|
67
|
+
|
|
68
|
+
if (this.reconnectTimeout) {
|
|
69
|
+
clearTimeout(this.reconnectTimeout);
|
|
70
|
+
this.reconnectTimeout = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.socket) {
|
|
74
|
+
try {
|
|
75
|
+
await this.socket.end(undefined);
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore close errors
|
|
78
|
+
}
|
|
79
|
+
this.socket = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.connectionState.status = "disconnected";
|
|
83
|
+
this.log.info("WhatsApp channel stopped");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async connect(): Promise<void> {
|
|
87
|
+
if (!this.running) return;
|
|
88
|
+
|
|
89
|
+
this.connectionState.status = "connecting";
|
|
90
|
+
this.log.info("Connecting to WhatsApp...");
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.authPath);
|
|
94
|
+
|
|
95
|
+
this.socket = makeWASocket({
|
|
96
|
+
auth: state,
|
|
97
|
+
printQRInTerminal: false,
|
|
98
|
+
logger: this.createBaileysLogger(),
|
|
99
|
+
getMessage: async () => ({ conversation: "" }),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.socket.ev.on("connection.update", async (update) => {
|
|
103
|
+
await this.handleConnectionUpdate(update, saveCreds);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.socket.ev.on("messages.upsert", async (update) => {
|
|
107
|
+
await this.handleMessages(update);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.socket.ev.on("creds.update", saveCreds);
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.connectionState.status = "error";
|
|
114
|
+
this.connectionState.error = (error as Error).message;
|
|
115
|
+
this.log.error(`WhatsApp connection error: ${(error as Error).message}`);
|
|
116
|
+
this.scheduleReconnect();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private createBaileysLogger(): undefined {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async handleConnectionUpdate(
|
|
125
|
+
update: Partial<ConnectionState>,
|
|
126
|
+
saveCreds: () => Promise<void>
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const { connection, lastDisconnect, qr } = update;
|
|
129
|
+
|
|
130
|
+
if (qr) {
|
|
131
|
+
this.connectionState.status = "qr";
|
|
132
|
+
this.connectionState.qrCode = qr;
|
|
133
|
+
this.printQR(qr);
|
|
134
|
+
this.log.info("Scan the QR code above with WhatsApp");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (connection === "close") {
|
|
138
|
+
const statusCode = (lastDisconnect?.error as { output?: { statusCode: number } })?.output?.statusCode;
|
|
139
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
140
|
+
|
|
141
|
+
this.connectionState.status = "disconnected";
|
|
142
|
+
this.log.warn(`WhatsApp disconnected: ${statusCode}`);
|
|
143
|
+
|
|
144
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
145
|
+
this.log.error("WhatsApp logged out - session invalidated. Need to re-scan QR.");
|
|
146
|
+
fs.rmSync(this.authPath, { recursive: true, force: true });
|
|
147
|
+
fs.mkdirSync(this.authPath, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (shouldReconnect && this.running) {
|
|
151
|
+
this.scheduleReconnect();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (connection === "open") {
|
|
156
|
+
this.connectionState.status = "connected";
|
|
157
|
+
this.connectionState.lastConnected = new Date();
|
|
158
|
+
this.connectionState.reconnectAttempts = 0;
|
|
159
|
+
this.connectionState.qrCode = undefined;
|
|
160
|
+
void saveCreds();
|
|
161
|
+
this.log.info("WhatsApp connected successfully");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private printQR(qr: string): void {
|
|
166
|
+
console.log("\n" + "=".repeat(50));
|
|
167
|
+
console.log(" WHATSAPP QR CODE - Scan with your phone");
|
|
168
|
+
console.log("=".repeat(50) + "\n");
|
|
169
|
+
|
|
170
|
+
const qrcode = require("qrcode-terminal");
|
|
171
|
+
qrcode.generate(qr, { small: false }, (qrString: string) => {
|
|
172
|
+
console.log(qrString);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log("\n" + "=".repeat(50));
|
|
176
|
+
console.log(" Open WhatsApp > Settings > Linked Devices");
|
|
177
|
+
console.log("=".repeat(50) + "\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async handleMessages(update: { messages: unknown[]; type: string }): Promise<void> {
|
|
181
|
+
if (update.type !== "notify") return;
|
|
182
|
+
|
|
183
|
+
for (const msg of update.messages) {
|
|
184
|
+
const typedMsg = msg as {
|
|
185
|
+
key: { fromMe?: boolean; remoteJid?: string; id?: string };
|
|
186
|
+
message?: Record<string, unknown>;
|
|
187
|
+
messageTimestamp?: number;
|
|
188
|
+
pushName?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (typedMsg.key.fromMe) continue;
|
|
192
|
+
|
|
193
|
+
const from = typedMsg.key.remoteJid;
|
|
194
|
+
if (!from) continue;
|
|
195
|
+
|
|
196
|
+
const content = this.extractMessageContent(typedMsg.message);
|
|
197
|
+
if (!content) continue;
|
|
198
|
+
|
|
199
|
+
const isGroup = from.includes("@g.us");
|
|
200
|
+
const peerId = isGroup ? from : from.replace("@s.whatsapp.net", "");
|
|
201
|
+
|
|
202
|
+
const incoming: IncomingMessage = {
|
|
203
|
+
sessionId: this.formatSessionId(peerId, isGroup ? "group" : "direct"),
|
|
204
|
+
channel: "whatsapp",
|
|
205
|
+
accountId: this.accountId,
|
|
206
|
+
peerId,
|
|
207
|
+
peerKind: isGroup ? "group" : "direct",
|
|
208
|
+
content,
|
|
209
|
+
metadata: {
|
|
210
|
+
messageId: typedMsg.key.id,
|
|
211
|
+
timestamp: typedMsg.messageTimestamp,
|
|
212
|
+
pushName: typedMsg.pushName,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await this.handleMessage(incoming);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private extractMessageContent(message?: Record<string, unknown>): string | null {
|
|
221
|
+
if (!message) return null;
|
|
222
|
+
|
|
223
|
+
if (message.conversation) {
|
|
224
|
+
return message.conversation as string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const extendedText = message.extendedTextMessage as { text?: string } | undefined;
|
|
228
|
+
if (extendedText?.text) {
|
|
229
|
+
return extendedText.text;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const imageMsg = message.imageMessage as { caption?: string } | undefined;
|
|
233
|
+
if (imageMsg?.caption) {
|
|
234
|
+
return `[Image] ${imageMsg.caption}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const videoMsg = message.videoMessage as { caption?: string } | undefined;
|
|
238
|
+
if (videoMsg?.caption) {
|
|
239
|
+
return `[Video] ${videoMsg.caption}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const docMsg = message.documentMessage as { caption?: string } | undefined;
|
|
243
|
+
if (docMsg?.caption) {
|
|
244
|
+
return `[Document] ${docMsg.caption}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (message.audioMessage) {
|
|
248
|
+
return "[Audio message]";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private scheduleReconnect(): void {
|
|
255
|
+
if (!this.running) return;
|
|
256
|
+
|
|
257
|
+
const maxAttempts = this.config.reconnectMaxAttempts ?? 10;
|
|
258
|
+
const baseDelay = this.config.reconnectBaseDelayMs ?? 5000;
|
|
259
|
+
const attempts = this.connectionState.reconnectAttempts;
|
|
260
|
+
|
|
261
|
+
if (attempts >= maxAttempts) {
|
|
262
|
+
this.log.error(`Max reconnection attempts (${maxAttempts}) reached`);
|
|
263
|
+
this.connectionState.status = "error";
|
|
264
|
+
this.connectionState.error = "Max reconnection attempts reached";
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempts), 60000);
|
|
269
|
+
this.connectionState.reconnectAttempts++;
|
|
270
|
+
|
|
271
|
+
this.log.info(`Reconnecting in ${delay / 1000}s (attempt ${attempts + 1}/${maxAttempts})`);
|
|
272
|
+
|
|
273
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
274
|
+
await this.connect();
|
|
275
|
+
}, delay);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
279
|
+
if (!this.socket || this.connectionState.status !== "connected") {
|
|
280
|
+
throw new Error("WhatsApp not connected");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const text = message.content ?? message.chunk ?? "";
|
|
284
|
+
if (!text) return;
|
|
285
|
+
|
|
286
|
+
const peerId = this.extractPeerId(sessionId);
|
|
287
|
+
const jid = peerId.includes("@") ? peerId : `${peerId}@s.whatsapp.net`;
|
|
288
|
+
|
|
289
|
+
await this.socket.sendMessage(jid, { text });
|
|
290
|
+
this.log.debug(`Sent message to ${peerId}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private extractPeerId(sessionId: string): string {
|
|
294
|
+
const parts = sessionId.split(":");
|
|
295
|
+
return parts[parts.length - 1] ?? "";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getState(): WhatsAppConnectionState {
|
|
299
|
+
return { ...this.connectionState };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function createWhatsAppChannel(config: WhatsAppConfig): WhatsAppChannel {
|
|
304
|
+
return new WhatsAppChannel(config);
|
|
305
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./loader.ts";
|