wagent 1.0.0
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/README.md +103 -0
- package/dist/channels/baileys/baileys.adapter.js +992 -0
- package/dist/channels/baileys/baileys.auth.js +96 -0
- package/dist/channels/baileys/baileys.events.js +284 -0
- package/dist/channels/baileys/baileys.version.js +86 -0
- package/dist/channels/channel.interface.js +6 -0
- package/dist/config.js +120 -0
- package/dist/constants.js +59 -0
- package/dist/db/client.js +160 -0
- package/dist/db/schema.js +189 -0
- package/dist/index.js +873 -0
- package/dist/services/instance-manager.js +185 -0
- package/dist/types/channel.types.js +4 -0
- package/dist/types/db.types.js +4 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +43 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Database Client
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
9
|
+
import * as schema from "./schema.js";
|
|
10
|
+
function getDataDir() {
|
|
11
|
+
if (process.env.WAGENT_HOME)
|
|
12
|
+
return process.env.WAGENT_HOME;
|
|
13
|
+
if (process.env.WAGENT_DATA_DIR)
|
|
14
|
+
return process.env.WAGENT_DATA_DIR;
|
|
15
|
+
if (process.platform === "win32")
|
|
16
|
+
return join(process.env.APPDATA ?? homedir(), "wagent");
|
|
17
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "wagent");
|
|
18
|
+
}
|
|
19
|
+
const DB_PATH = join(getDataDir(), "wagent.db");
|
|
20
|
+
// Ensure the data directory exists
|
|
21
|
+
const dir = dirname(DB_PATH);
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const sqlite = new Database(DB_PATH);
|
|
26
|
+
// Enable WAL mode for better concurrent read performance
|
|
27
|
+
sqlite.pragma("journal_mode = WAL");
|
|
28
|
+
sqlite.pragma("foreign_keys = ON");
|
|
29
|
+
export const db = drizzle(sqlite, { schema });
|
|
30
|
+
// Run table creation on init (push-based, no migration files needed)
|
|
31
|
+
function initializeDatabase() {
|
|
32
|
+
sqlite.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS instances (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
name TEXT NOT NULL,
|
|
36
|
+
channel TEXT NOT NULL DEFAULT 'baileys',
|
|
37
|
+
phone_number TEXT,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'disconnected',
|
|
39
|
+
wa_version TEXT,
|
|
40
|
+
cloud_access_token TEXT,
|
|
41
|
+
cloud_phone_number_id TEXT,
|
|
42
|
+
cloud_business_id TEXT,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
updated_at INTEGER NOT NULL,
|
|
45
|
+
last_connected INTEGER,
|
|
46
|
+
last_disconnected INTEGER
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS auth_keys (
|
|
50
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
51
|
+
key_id TEXT NOT NULL,
|
|
52
|
+
key_data TEXT NOT NULL,
|
|
53
|
+
PRIMARY KEY (instance_id, key_id)
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_auth_keys_instance ON auth_keys(instance_id);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
58
|
+
id TEXT NOT NULL,
|
|
59
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
60
|
+
chat_id TEXT NOT NULL,
|
|
61
|
+
sender_id TEXT NOT NULL,
|
|
62
|
+
type TEXT NOT NULL,
|
|
63
|
+
content TEXT,
|
|
64
|
+
media_url TEXT,
|
|
65
|
+
media_local TEXT,
|
|
66
|
+
media_mimetype TEXT,
|
|
67
|
+
quoted_id TEXT,
|
|
68
|
+
is_from_me INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
is_forwarded INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
status TEXT NOT NULL DEFAULT 'received',
|
|
71
|
+
timestamp INTEGER NOT NULL,
|
|
72
|
+
raw_data TEXT,
|
|
73
|
+
PRIMARY KEY (instance_id, id)
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(instance_id, chat_id, timestamp DESC);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(instance_id, timestamp DESC);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS processed_messages (
|
|
79
|
+
message_id TEXT NOT NULL,
|
|
80
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
81
|
+
processed_at INTEGER NOT NULL,
|
|
82
|
+
PRIMARY KEY (instance_id, message_id)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
86
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
87
|
+
jid TEXT NOT NULL,
|
|
88
|
+
name TEXT,
|
|
89
|
+
notify_name TEXT,
|
|
90
|
+
phone TEXT,
|
|
91
|
+
lid TEXT,
|
|
92
|
+
profile_pic_url TEXT,
|
|
93
|
+
is_business INTEGER NOT NULL DEFAULT 0,
|
|
94
|
+
is_blocked INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
updated_at INTEGER NOT NULL,
|
|
96
|
+
PRIMARY KEY (instance_id, jid)
|
|
97
|
+
);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_phone ON contacts(instance_id, phone);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_lid ON contacts(instance_id, lid);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS groups_cache (
|
|
102
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
103
|
+
jid TEXT NOT NULL,
|
|
104
|
+
subject TEXT,
|
|
105
|
+
description TEXT,
|
|
106
|
+
owner_jid TEXT,
|
|
107
|
+
participants TEXT NOT NULL DEFAULT '[]',
|
|
108
|
+
participant_count INTEGER NOT NULL DEFAULT 0,
|
|
109
|
+
is_announce INTEGER NOT NULL DEFAULT 0,
|
|
110
|
+
is_locked INTEGER NOT NULL DEFAULT 0,
|
|
111
|
+
ephemeral_duration INTEGER,
|
|
112
|
+
invite_code TEXT,
|
|
113
|
+
created_at INTEGER,
|
|
114
|
+
updated_at INTEGER NOT NULL,
|
|
115
|
+
PRIMARY KEY (instance_id, jid)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
119
|
+
instance_id TEXT NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
|
|
120
|
+
jid TEXT NOT NULL,
|
|
121
|
+
name TEXT,
|
|
122
|
+
is_group INTEGER NOT NULL DEFAULT 0,
|
|
123
|
+
unread_count INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
125
|
+
is_muted INTEGER NOT NULL DEFAULT 0,
|
|
126
|
+
mute_until INTEGER,
|
|
127
|
+
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
128
|
+
last_message_id TEXT,
|
|
129
|
+
last_message_at INTEGER,
|
|
130
|
+
updated_at INTEGER NOT NULL,
|
|
131
|
+
PRIMARY KEY (instance_id, jid)
|
|
132
|
+
);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_chats_recent ON chats(instance_id, last_message_at DESC);
|
|
134
|
+
|
|
135
|
+
CREATE TABLE IF NOT EXISTS wa_version_cache (
|
|
136
|
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
137
|
+
version_json TEXT NOT NULL,
|
|
138
|
+
fetched_at INTEGER NOT NULL,
|
|
139
|
+
is_latest INTEGER NOT NULL DEFAULT 1
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS queue_stats (
|
|
143
|
+
instance_id TEXT NOT NULL PRIMARY KEY REFERENCES instances(id) ON DELETE CASCADE,
|
|
144
|
+
messages_sent INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
messages_failed INTEGER NOT NULL DEFAULT 0,
|
|
146
|
+
last_sent_at INTEGER,
|
|
147
|
+
rate_limited_until INTEGER
|
|
148
|
+
);
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
// Inline migration: add `lid` column for existing databases (pre-v7 upgrade)
|
|
152
|
+
// Must run BEFORE initializeDatabase() so the index creation succeeds
|
|
153
|
+
try {
|
|
154
|
+
sqlite.exec(`ALTER TABLE contacts ADD COLUMN lid TEXT`);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// column already exists or table doesn't exist yet — ignore
|
|
158
|
+
}
|
|
159
|
+
initializeDatabase();
|
|
160
|
+
export { sqlite };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Drizzle ORM Schema Definitions
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { sqliteTable, text, integer, primaryKey, index } from "drizzle-orm/sqlite-core";
|
|
5
|
+
// ============================================================
|
|
6
|
+
// INSTANCES
|
|
7
|
+
// Core entity: each instance = one WhatsApp number connection
|
|
8
|
+
// ============================================================
|
|
9
|
+
export const instances = sqliteTable("instances", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
name: text("name").notNull(),
|
|
12
|
+
channel: text("channel", { enum: ["baileys", "cloud"] })
|
|
13
|
+
.notNull()
|
|
14
|
+
.default("baileys"),
|
|
15
|
+
phoneNumber: text("phone_number"),
|
|
16
|
+
status: text("status", {
|
|
17
|
+
enum: ["connected", "disconnected", "connecting", "qr_pending"],
|
|
18
|
+
})
|
|
19
|
+
.notNull()
|
|
20
|
+
.default("disconnected"),
|
|
21
|
+
waVersion: text("wa_version"),
|
|
22
|
+
// Cloud API specific
|
|
23
|
+
cloudAccessToken: text("cloud_access_token"),
|
|
24
|
+
cloudPhoneNumberId: text("cloud_phone_number_id"),
|
|
25
|
+
cloudBusinessId: text("cloud_business_id"),
|
|
26
|
+
// Metadata
|
|
27
|
+
createdAt: integer("created_at").notNull(),
|
|
28
|
+
updatedAt: integer("updated_at").notNull(),
|
|
29
|
+
lastConnected: integer("last_connected"),
|
|
30
|
+
lastDisconnected: integer("last_disconnected"),
|
|
31
|
+
});
|
|
32
|
+
// ============================================================
|
|
33
|
+
// AUTH KEYS
|
|
34
|
+
// Baileys session persistence (Signal protocol keys)
|
|
35
|
+
// ============================================================
|
|
36
|
+
export const authKeys = sqliteTable("auth_keys", {
|
|
37
|
+
instanceId: text("instance_id")
|
|
38
|
+
.notNull()
|
|
39
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
40
|
+
keyId: text("key_id").notNull(),
|
|
41
|
+
keyData: text("key_data").notNull(),
|
|
42
|
+
}, (table) => [
|
|
43
|
+
primaryKey({ columns: [table.instanceId, table.keyId] }),
|
|
44
|
+
index("idx_auth_keys_instance").on(table.instanceId),
|
|
45
|
+
]);
|
|
46
|
+
// ============================================================
|
|
47
|
+
// MESSAGES
|
|
48
|
+
// Message store for agent context and history
|
|
49
|
+
// ============================================================
|
|
50
|
+
export const messages = sqliteTable("messages", {
|
|
51
|
+
id: text("id").notNull(),
|
|
52
|
+
instanceId: text("instance_id")
|
|
53
|
+
.notNull()
|
|
54
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
55
|
+
chatId: text("chat_id").notNull(),
|
|
56
|
+
senderId: text("sender_id").notNull(),
|
|
57
|
+
type: text("type", {
|
|
58
|
+
enum: [
|
|
59
|
+
"text",
|
|
60
|
+
"image",
|
|
61
|
+
"video",
|
|
62
|
+
"audio",
|
|
63
|
+
"document",
|
|
64
|
+
"location",
|
|
65
|
+
"contact",
|
|
66
|
+
"poll",
|
|
67
|
+
"reaction",
|
|
68
|
+
"sticker",
|
|
69
|
+
],
|
|
70
|
+
}).notNull(),
|
|
71
|
+
content: text("content"),
|
|
72
|
+
mediaUrl: text("media_url"),
|
|
73
|
+
mediaLocal: text("media_local"),
|
|
74
|
+
mediaMimetype: text("media_mimetype"),
|
|
75
|
+
quotedId: text("quoted_id"),
|
|
76
|
+
isFromMe: integer("is_from_me").notNull().default(0),
|
|
77
|
+
isForwarded: integer("is_forwarded").notNull().default(0),
|
|
78
|
+
status: text("status", {
|
|
79
|
+
enum: ["received", "sent", "delivered", "read", "played"],
|
|
80
|
+
})
|
|
81
|
+
.notNull()
|
|
82
|
+
.default("received"),
|
|
83
|
+
timestamp: integer("timestamp").notNull(),
|
|
84
|
+
rawData: text("raw_data"),
|
|
85
|
+
}, (table) => [
|
|
86
|
+
primaryKey({ columns: [table.instanceId, table.id] }),
|
|
87
|
+
index("idx_messages_chat").on(table.instanceId, table.chatId, table.timestamp),
|
|
88
|
+
index("idx_messages_timestamp").on(table.instanceId, table.timestamp),
|
|
89
|
+
]);
|
|
90
|
+
// ============================================================
|
|
91
|
+
// PROCESSED MESSAGES
|
|
92
|
+
// Deduplication: track which messages have been processed
|
|
93
|
+
// ============================================================
|
|
94
|
+
export const processedMessages = sqliteTable("processed_messages", {
|
|
95
|
+
messageId: text("message_id").notNull(),
|
|
96
|
+
instanceId: text("instance_id")
|
|
97
|
+
.notNull()
|
|
98
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
99
|
+
processedAt: integer("processed_at").notNull(),
|
|
100
|
+
}, (table) => [primaryKey({ columns: [table.instanceId, table.messageId] })]);
|
|
101
|
+
// ============================================================
|
|
102
|
+
// CONTACTS
|
|
103
|
+
// Cached contact information
|
|
104
|
+
// ============================================================
|
|
105
|
+
export const contacts = sqliteTable("contacts", {
|
|
106
|
+
instanceId: text("instance_id")
|
|
107
|
+
.notNull()
|
|
108
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
109
|
+
jid: text("jid").notNull(),
|
|
110
|
+
name: text("name"),
|
|
111
|
+
notifyName: text("notify_name"),
|
|
112
|
+
phone: text("phone"),
|
|
113
|
+
lid: text("lid"),
|
|
114
|
+
profilePicUrl: text("profile_pic_url"),
|
|
115
|
+
isBusiness: integer("is_business").notNull().default(0),
|
|
116
|
+
isBlocked: integer("is_blocked").notNull().default(0),
|
|
117
|
+
updatedAt: integer("updated_at").notNull(),
|
|
118
|
+
}, (table) => [
|
|
119
|
+
primaryKey({ columns: [table.instanceId, table.jid] }),
|
|
120
|
+
index("idx_contacts_phone").on(table.instanceId, table.phone),
|
|
121
|
+
index("idx_contacts_lid").on(table.instanceId, table.lid),
|
|
122
|
+
]);
|
|
123
|
+
// ============================================================
|
|
124
|
+
// GROUPS CACHE
|
|
125
|
+
// Cached group metadata
|
|
126
|
+
// ============================================================
|
|
127
|
+
export const groupsCache = sqliteTable("groups_cache", {
|
|
128
|
+
instanceId: text("instance_id")
|
|
129
|
+
.notNull()
|
|
130
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
131
|
+
jid: text("jid").notNull(),
|
|
132
|
+
subject: text("subject"),
|
|
133
|
+
description: text("description"),
|
|
134
|
+
ownerJid: text("owner_jid"),
|
|
135
|
+
participants: text("participants").notNull().default("[]"),
|
|
136
|
+
participantCount: integer("participant_count").notNull().default(0),
|
|
137
|
+
isAnnounce: integer("is_announce").notNull().default(0),
|
|
138
|
+
isLocked: integer("is_locked").notNull().default(0),
|
|
139
|
+
ephemeralDuration: integer("ephemeral_duration"),
|
|
140
|
+
inviteCode: text("invite_code"),
|
|
141
|
+
createdAt: integer("created_at"),
|
|
142
|
+
updatedAt: integer("updated_at").notNull(),
|
|
143
|
+
}, (table) => [primaryKey({ columns: [table.instanceId, table.jid] })]);
|
|
144
|
+
// ============================================================
|
|
145
|
+
// CHATS
|
|
146
|
+
// Cached chat list with metadata
|
|
147
|
+
// ============================================================
|
|
148
|
+
export const chats = sqliteTable("chats", {
|
|
149
|
+
instanceId: text("instance_id")
|
|
150
|
+
.notNull()
|
|
151
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
152
|
+
jid: text("jid").notNull(),
|
|
153
|
+
name: text("name"),
|
|
154
|
+
isGroup: integer("is_group").notNull().default(0),
|
|
155
|
+
unreadCount: integer("unread_count").notNull().default(0),
|
|
156
|
+
isPinned: integer("is_pinned").notNull().default(0),
|
|
157
|
+
isMuted: integer("is_muted").notNull().default(0),
|
|
158
|
+
muteUntil: integer("mute_until"),
|
|
159
|
+
isArchived: integer("is_archived").notNull().default(0),
|
|
160
|
+
lastMessageId: text("last_message_id"),
|
|
161
|
+
lastMessageAt: integer("last_message_at"),
|
|
162
|
+
updatedAt: integer("updated_at").notNull(),
|
|
163
|
+
}, (table) => [
|
|
164
|
+
primaryKey({ columns: [table.instanceId, table.jid] }),
|
|
165
|
+
index("idx_chats_recent").on(table.instanceId, table.lastMessageAt),
|
|
166
|
+
]);
|
|
167
|
+
// ============================================================
|
|
168
|
+
// VERSION CACHE
|
|
169
|
+
// WhatsApp Web version auto-update (daily check)
|
|
170
|
+
// ============================================================
|
|
171
|
+
export const waVersionCache = sqliteTable("wa_version_cache", {
|
|
172
|
+
id: integer("id").primaryKey().default(1),
|
|
173
|
+
versionJson: text("version_json").notNull(),
|
|
174
|
+
fetchedAt: integer("fetched_at").notNull(),
|
|
175
|
+
isLatest: integer("is_latest").notNull().default(1),
|
|
176
|
+
});
|
|
177
|
+
// ============================================================
|
|
178
|
+
// QUEUE STATS
|
|
179
|
+
// Track outbound message queue state for monitoring
|
|
180
|
+
// ============================================================
|
|
181
|
+
export const queueStats = sqliteTable("queue_stats", {
|
|
182
|
+
instanceId: text("instance_id")
|
|
183
|
+
.primaryKey()
|
|
184
|
+
.references(() => instances.id, { onDelete: "cascade" }),
|
|
185
|
+
messagesSent: integer("messages_sent").notNull().default(0),
|
|
186
|
+
messagesFailed: integer("messages_failed").notNull().default(0),
|
|
187
|
+
lastSentAt: integer("last_sent_at"),
|
|
188
|
+
rateLimitedUntil: integer("rate_limited_until"),
|
|
189
|
+
});
|