@c4t4/heyamigo 0.1.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/.gitignore +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/config/access.example.json +88 -0
- package/config/config.example.json +72 -0
- package/config/import-instructions.HOWTO.md +58 -0
- package/config/import-instructions.md +67 -0
- package/config/memory-instructions.md +40 -0
- package/config/personalities/casual.md +24 -0
- package/config/personalities/professional.md +25 -0
- package/config/personalities/sharp.md +45 -0
- package/dist/ai/claude.js +153 -0
- package/dist/ai/sessions.js +63 -0
- package/dist/cli/import.js +17 -0
- package/dist/cli/index.js +70 -0
- package/dist/cli/service.js +105 -0
- package/dist/cli/setup.js +701 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/supervisor.js +37 -0
- package/dist/config.js +104 -0
- package/dist/gateway/bootstrap.js +56 -0
- package/dist/gateway/commands.js +58 -0
- package/dist/gateway/incoming.js +239 -0
- package/dist/gateway/outgoing.js +168 -0
- package/dist/gateway/triggers.js +75 -0
- package/dist/index.js +30 -0
- package/dist/logger.js +7 -0
- package/dist/memory/digest-flag.js +8 -0
- package/dist/memory/digest.js +211 -0
- package/dist/memory/frontmatter.js +100 -0
- package/dist/memory/importer.js +103 -0
- package/dist/memory/paths.js +26 -0
- package/dist/memory/preamble.js +98 -0
- package/dist/memory/router.js +90 -0
- package/dist/memory/scheduler.js +85 -0
- package/dist/memory/store.js +183 -0
- package/dist/promptlog.js +52 -0
- package/dist/queue/persistence.js +68 -0
- package/dist/queue/queue.js +49 -0
- package/dist/queue/types.js +1 -0
- package/dist/queue/worker.js +51 -0
- package/dist/store/media.js +108 -0
- package/dist/store/messages.js +33 -0
- package/dist/wa/auth.js +9 -0
- package/dist/wa/sender.js +79 -0
- package/dist/wa/socket.js +84 -0
- package/dist/wa/whitelist.js +213 -0
- package/package.json +63 -0
- package/scripts/start-browser.sh +158 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
const QUEUE_DIR = resolve(process.cwd(), 'storage/queue');
|
|
5
|
+
const PENDING_FILE = resolve(QUEUE_DIR, 'pending.jsonl');
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
mkdirSync(QUEUE_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
export function persistJob(job) {
|
|
10
|
+
ensureDir();
|
|
11
|
+
const line = JSON.stringify({ ...job, enqueuedAt: Date.now() }) + '\n';
|
|
12
|
+
writeFileSync(PENDING_FILE, line, { flag: 'a', encoding: 'utf-8' });
|
|
13
|
+
}
|
|
14
|
+
export function removeJob(job) {
|
|
15
|
+
if (!existsSync(PENDING_FILE))
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
const lines = readFileSync(PENDING_FILE, 'utf-8')
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
const remaining = lines.filter((line) => {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(line);
|
|
24
|
+
return !(parsed.jid === job.jid && parsed.text === job.text);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
if (remaining.length === 0) {
|
|
31
|
+
unlinkSync(PENDING_FILE);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
writeFileSync(PENDING_FILE, remaining.join('\n') + '\n', 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// best-effort cleanup
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function loadPendingJobs() {
|
|
42
|
+
if (!existsSync(PENDING_FILE))
|
|
43
|
+
return [];
|
|
44
|
+
try {
|
|
45
|
+
const lines = readFileSync(PENDING_FILE, 'utf-8')
|
|
46
|
+
.split('\n')
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
const jobs = [];
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(line);
|
|
52
|
+
jobs.push(parsed);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
logger.warn({ line: line.slice(0, 100) }, 'skipping malformed pending job');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
logger.info({ count: jobs.length }, 'loaded pending jobs from disk');
|
|
59
|
+
return jobs;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function clearPending() {
|
|
66
|
+
if (existsSync(PENDING_FILE))
|
|
67
|
+
unlinkSync(PENDING_FILE);
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fastq from 'fastq';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { loadPendingJobs, persistJob, removeJob } from './persistence.js';
|
|
4
|
+
import { processJob } from './worker.js';
|
|
5
|
+
const queues = new Map();
|
|
6
|
+
function getQueue(jid) {
|
|
7
|
+
let q = queues.get(jid);
|
|
8
|
+
if (!q) {
|
|
9
|
+
q = fastq.promise(async (job) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await processJob(job);
|
|
12
|
+
removeJob(job);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
removeJob(job);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}, 1);
|
|
20
|
+
queues.set(jid, q);
|
|
21
|
+
}
|
|
22
|
+
return q;
|
|
23
|
+
}
|
|
24
|
+
export async function enqueue(job) {
|
|
25
|
+
persistJob(job);
|
|
26
|
+
return getQueue(job.jid).push(job);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* On boot, replay any jobs that were persisted but never completed
|
|
30
|
+
* (process crashed mid-queue). Returns a promise that resolves when
|
|
31
|
+
* all replayed jobs finish or fail. Caller provides a handler for
|
|
32
|
+
* results since the original WAMessage context is gone.
|
|
33
|
+
*/
|
|
34
|
+
export async function replayPending(onResult) {
|
|
35
|
+
const pending = loadPendingJobs();
|
|
36
|
+
if (!pending.length)
|
|
37
|
+
return;
|
|
38
|
+
logger.info({ count: pending.length }, 'replaying pending jobs from last session');
|
|
39
|
+
const promises = pending.map(async (job) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await getQueue(job.jid).push(job);
|
|
42
|
+
await onResult(job, result);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
logger.error({ err, jid: job.jid }, 'replayed job failed');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
await Promise.allSettled(promises);
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { askClaude } from '../ai/claude.js';
|
|
2
|
+
import { clearSession, setSession, setUsage } from '../ai/sessions.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { extractDigestFlag } from '../memory/digest-flag.js';
|
|
5
|
+
import { scheduleDigest } from '../memory/scheduler.js';
|
|
6
|
+
function isStaleSessionError(err) {
|
|
7
|
+
return (err instanceof Error &&
|
|
8
|
+
err.message.includes('No conversation found'));
|
|
9
|
+
}
|
|
10
|
+
async function callClaude(job) {
|
|
11
|
+
const { reply, sessionId, usage } = await askClaude({
|
|
12
|
+
input: job.input,
|
|
13
|
+
sessionId: job.sessionId,
|
|
14
|
+
allowedTools: job.allowedTools,
|
|
15
|
+
});
|
|
16
|
+
if (!job.sessionId) {
|
|
17
|
+
setSession(job.jid, sessionId);
|
|
18
|
+
}
|
|
19
|
+
const totalContextTokens = usage.inputTokens +
|
|
20
|
+
usage.cacheReadTokens +
|
|
21
|
+
usage.cacheCreationTokens +
|
|
22
|
+
usage.outputTokens;
|
|
23
|
+
setUsage(job.jid, {
|
|
24
|
+
...usage,
|
|
25
|
+
totalContextTokens,
|
|
26
|
+
updatedAt: Math.floor(Date.now() / 1000),
|
|
27
|
+
});
|
|
28
|
+
const { clean, flag } = extractDigestFlag(reply);
|
|
29
|
+
if (flag) {
|
|
30
|
+
logger.info({ jid: job.jid, number: job.senderNumber, reason: flag }, 'DIGEST flag raised, scheduling');
|
|
31
|
+
scheduleDigest({
|
|
32
|
+
jid: job.jid,
|
|
33
|
+
number: job.senderNumber,
|
|
34
|
+
reason: flag,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { reply: clean };
|
|
38
|
+
}
|
|
39
|
+
export async function processJob(job) {
|
|
40
|
+
try {
|
|
41
|
+
return await callClaude(job);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (job.sessionId && isStaleSessionError(err)) {
|
|
45
|
+
logger.warn({ jid: job.jid, staleId: job.sessionId }, 'stale session detected, clearing and retrying with fresh bootstrap');
|
|
46
|
+
clearSession(job.jid);
|
|
47
|
+
return callClaude({ ...job, sessionId: undefined, allowedTools: job.allowedTools });
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, readdir, stat, unlink } from 'fs/promises';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { downloadMediaMessage, extensionForMediaMessage, getContentType, } from 'baileys';
|
|
5
|
+
import { config } from '../config.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
const MEDIA_TYPES = {
|
|
8
|
+
imageMessage: 'image',
|
|
9
|
+
videoMessage: 'video',
|
|
10
|
+
audioMessage: 'audio',
|
|
11
|
+
documentMessage: 'document',
|
|
12
|
+
documentWithCaptionMessage: 'document',
|
|
13
|
+
stickerMessage: 'sticker',
|
|
14
|
+
};
|
|
15
|
+
function mediaDir(jid) {
|
|
16
|
+
return resolve(process.cwd(), config.storage.mediaDir, jid);
|
|
17
|
+
}
|
|
18
|
+
export function detectMediaType(msg) {
|
|
19
|
+
const content = msg.message;
|
|
20
|
+
if (!content)
|
|
21
|
+
return null;
|
|
22
|
+
const type = getContentType(content);
|
|
23
|
+
if (!type)
|
|
24
|
+
return null;
|
|
25
|
+
return MEDIA_TYPES[type] ?? null;
|
|
26
|
+
}
|
|
27
|
+
export async function downloadAndSave(msg, jid) {
|
|
28
|
+
const mediaType = detectMediaType(msg);
|
|
29
|
+
if (!mediaType)
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
|
33
|
+
const ext = extensionForMediaMessage(msg.message) || 'bin';
|
|
34
|
+
const id = msg.key.id || `${Date.now()}`;
|
|
35
|
+
const dir = mediaDir(jid);
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
const filename = `${id}.${ext}`;
|
|
38
|
+
const filePath = resolve(dir, filename);
|
|
39
|
+
writeFileSync(filePath, buffer);
|
|
40
|
+
const content = msg.message;
|
|
41
|
+
const messageType = getContentType(content);
|
|
42
|
+
const mediaMsg = content[messageType];
|
|
43
|
+
const mimetype = mediaMsg?.mimetype ?? `application/octet-stream`;
|
|
44
|
+
logger.debug({ jid, mediaType, filename, bytes: buffer.length }, 'media saved');
|
|
45
|
+
return {
|
|
46
|
+
mediaType,
|
|
47
|
+
mediaPath: filePath,
|
|
48
|
+
mediaMime: mimetype,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
logger.error({ err, jid, msgId: msg.key.id }, 'media download failed');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function mediaPromptTag(info, caption) {
|
|
57
|
+
const label = info.mediaType === 'image'
|
|
58
|
+
? 'an image'
|
|
59
|
+
: info.mediaType === 'video'
|
|
60
|
+
? 'a video'
|
|
61
|
+
: info.mediaType === 'audio'
|
|
62
|
+
? 'a voice message'
|
|
63
|
+
: info.mediaType === 'document'
|
|
64
|
+
? 'a document'
|
|
65
|
+
: 'a sticker';
|
|
66
|
+
const lines = [
|
|
67
|
+
`[User sent ${label}: ${info.mediaPath}]`,
|
|
68
|
+
`Read this file to see what the user sent.`,
|
|
69
|
+
];
|
|
70
|
+
if (caption)
|
|
71
|
+
lines.push(`Caption: "${caption}"`);
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Delete media files older than retention period.
|
|
76
|
+
*/
|
|
77
|
+
export async function pruneMedia() {
|
|
78
|
+
const days = config.storage.mediaRetentionDays;
|
|
79
|
+
if (days <= 0)
|
|
80
|
+
return;
|
|
81
|
+
const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
82
|
+
const baseDir = resolve(process.cwd(), config.storage.mediaDir);
|
|
83
|
+
try {
|
|
84
|
+
const jids = await readdir(baseDir, { withFileTypes: true });
|
|
85
|
+
for (const jidEntry of jids) {
|
|
86
|
+
if (!jidEntry.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
const jidDir = resolve(baseDir, jidEntry.name);
|
|
89
|
+
const files = await readdir(jidDir);
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const fp = resolve(jidDir, file);
|
|
92
|
+
try {
|
|
93
|
+
const s = await stat(fp);
|
|
94
|
+
if (s.mtimeMs < cutoffMs) {
|
|
95
|
+
await unlink(fp);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
100
|
+
// remove empty jid dirs
|
|
101
|
+
const remaining = await readdir(jidDir);
|
|
102
|
+
if (remaining.length === 0) {
|
|
103
|
+
await readdir(jidDir).then(() => import('fs/promises').then((fs) => fs.rmdir(jidDir)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from 'fs/promises';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
let dirReady = false;
|
|
5
|
+
async function ensureDir() {
|
|
6
|
+
if (dirReady)
|
|
7
|
+
return;
|
|
8
|
+
await mkdir(resolve(process.cwd(), config.storage.messagesDir), {
|
|
9
|
+
recursive: true,
|
|
10
|
+
});
|
|
11
|
+
dirReady = true;
|
|
12
|
+
}
|
|
13
|
+
function fileFor(jid) {
|
|
14
|
+
return resolve(process.cwd(), config.storage.messagesDir, `${jid}.jsonl`);
|
|
15
|
+
}
|
|
16
|
+
export async function append(msg) {
|
|
17
|
+
await ensureDir();
|
|
18
|
+
const line = JSON.stringify(msg) + '\n';
|
|
19
|
+
await appendFile(fileFor(msg.jid), line, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
export async function readLast(jid, n) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(fileFor(jid), 'utf-8');
|
|
24
|
+
const lines = content.trimEnd().split('\n').filter(Boolean);
|
|
25
|
+
const tail = lines.slice(-n);
|
|
26
|
+
return tail.map((l) => JSON.parse(l));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT')
|
|
30
|
+
return [];
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/wa/auth.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mkdirSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { useMultiFileAuthState } from 'baileys';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
export async function initAuth() {
|
|
6
|
+
const authDir = resolve(process.cwd(), config.whatsapp.authDir);
|
|
7
|
+
mkdirSync(authDir, { recursive: true });
|
|
8
|
+
return useMultiFileAuthState(authDir);
|
|
9
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { basename, extname } from 'path';
|
|
3
|
+
export async function sendText(sock, jid, text, quoted) {
|
|
4
|
+
return sock.sendMessage(jid, { text }, quoted ? { quoted } : undefined);
|
|
5
|
+
}
|
|
6
|
+
const EXT_MAP = {
|
|
7
|
+
'.png': 'image',
|
|
8
|
+
'.jpg': 'image',
|
|
9
|
+
'.jpeg': 'image',
|
|
10
|
+
'.gif': 'image',
|
|
11
|
+
'.webp': 'image',
|
|
12
|
+
'.mp4': 'video',
|
|
13
|
+
'.avi': 'video',
|
|
14
|
+
'.mov': 'video',
|
|
15
|
+
'.mkv': 'video',
|
|
16
|
+
'.mp3': 'audio',
|
|
17
|
+
'.ogg': 'audio',
|
|
18
|
+
'.opus': 'audio',
|
|
19
|
+
'.m4a': 'audio',
|
|
20
|
+
'.wav': 'audio',
|
|
21
|
+
'.pdf': 'document',
|
|
22
|
+
'.doc': 'document',
|
|
23
|
+
'.docx': 'document',
|
|
24
|
+
'.xls': 'document',
|
|
25
|
+
'.xlsx': 'document',
|
|
26
|
+
'.csv': 'document',
|
|
27
|
+
'.txt': 'document',
|
|
28
|
+
'.zip': 'document',
|
|
29
|
+
};
|
|
30
|
+
const MIME_MAP = {
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
'.jpg': 'image/jpeg',
|
|
33
|
+
'.jpeg': 'image/jpeg',
|
|
34
|
+
'.gif': 'image/gif',
|
|
35
|
+
'.webp': 'image/webp',
|
|
36
|
+
'.mp4': 'video/mp4',
|
|
37
|
+
'.avi': 'video/avi',
|
|
38
|
+
'.mov': 'video/quicktime',
|
|
39
|
+
'.mkv': 'video/x-matroska',
|
|
40
|
+
'.mp3': 'audio/mpeg',
|
|
41
|
+
'.ogg': 'audio/ogg',
|
|
42
|
+
'.opus': 'audio/opus',
|
|
43
|
+
'.m4a': 'audio/mp4',
|
|
44
|
+
'.wav': 'audio/wav',
|
|
45
|
+
'.pdf': 'application/pdf',
|
|
46
|
+
'.doc': 'application/msword',
|
|
47
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
48
|
+
'.xls': 'application/vnd.ms-excel',
|
|
49
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
50
|
+
'.csv': 'text/csv',
|
|
51
|
+
'.txt': 'text/plain',
|
|
52
|
+
'.zip': 'application/zip',
|
|
53
|
+
};
|
|
54
|
+
export function detectMediaType(filePath) {
|
|
55
|
+
const ext = extname(filePath).toLowerCase();
|
|
56
|
+
return EXT_MAP[ext] ?? 'document';
|
|
57
|
+
}
|
|
58
|
+
export async function sendFile(sock, jid, filePath, caption, quoted) {
|
|
59
|
+
const buffer = readFileSync(filePath);
|
|
60
|
+
const ext = extname(filePath).toLowerCase();
|
|
61
|
+
const mime = MIME_MAP[ext] ?? 'application/octet-stream';
|
|
62
|
+
const type = detectMediaType(filePath);
|
|
63
|
+
const opts = quoted ? { quoted } : undefined;
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'image':
|
|
66
|
+
return sock.sendMessage(jid, { image: buffer, caption: caption || undefined }, opts);
|
|
67
|
+
case 'video':
|
|
68
|
+
return sock.sendMessage(jid, { video: buffer, caption: caption || undefined, mimetype: mime }, opts);
|
|
69
|
+
case 'audio':
|
|
70
|
+
return sock.sendMessage(jid, { audio: buffer, mimetype: mime }, opts);
|
|
71
|
+
case 'document':
|
|
72
|
+
return sock.sendMessage(jid, {
|
|
73
|
+
document: buffer,
|
|
74
|
+
mimetype: mime,
|
|
75
|
+
fileName: basename(filePath),
|
|
76
|
+
caption: caption || undefined,
|
|
77
|
+
}, opts);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import makeWASocket, { Browsers, DisconnectReason, fetchLatestWaWebVersion, } from 'baileys';
|
|
2
|
+
import QRCode from 'qrcode';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
import { initAuth } from './auth.js';
|
|
6
|
+
let currentSocket = null;
|
|
7
|
+
let currentQr = null;
|
|
8
|
+
let pairingCodeShown = false;
|
|
9
|
+
let reconnectAttempts = 0;
|
|
10
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
11
|
+
let onNewSocket = null;
|
|
12
|
+
export function getSocket() {
|
|
13
|
+
return currentSocket;
|
|
14
|
+
}
|
|
15
|
+
export function getCurrentQr() {
|
|
16
|
+
return currentQr;
|
|
17
|
+
}
|
|
18
|
+
export async function startSocket(listener) {
|
|
19
|
+
if (listener)
|
|
20
|
+
onNewSocket = listener;
|
|
21
|
+
const { state, saveCreds } = await initAuth();
|
|
22
|
+
const { version, isLatest } = await fetchLatestWaWebVersion({});
|
|
23
|
+
logger.info({ version, isLatest }, 'using WA Web version');
|
|
24
|
+
const sock = makeWASocket({
|
|
25
|
+
auth: state,
|
|
26
|
+
version,
|
|
27
|
+
browser: Browsers.macOS(config.whatsapp.browserName),
|
|
28
|
+
logger: logger.child({ module: 'baileys' }),
|
|
29
|
+
// Don't emit events for messages we send ourselves — eliminates echo
|
|
30
|
+
// loops and avoids double-storing outbound messages (outgoing.ts
|
|
31
|
+
// persists them explicitly).
|
|
32
|
+
emitOwnEvents: false,
|
|
33
|
+
});
|
|
34
|
+
currentSocket = sock;
|
|
35
|
+
onNewSocket?.(sock);
|
|
36
|
+
sock.ev.on('creds.update', saveCreds);
|
|
37
|
+
sock.ev.on('connection.update', async (update) => {
|
|
38
|
+
const { connection, lastDisconnect, qr } = update;
|
|
39
|
+
if (qr) {
|
|
40
|
+
currentQr = qr;
|
|
41
|
+
const ascii = await QRCode.toString(qr, { type: 'utf8', margin: 2 });
|
|
42
|
+
process.stdout.write('\nScan this QR in WhatsApp → Settings → Linked Devices → Link a device\n');
|
|
43
|
+
process.stdout.write(ascii + '\n');
|
|
44
|
+
// Also show pairing code (works on any terminal, even broken QR rendering)
|
|
45
|
+
if (!pairingCodeShown && config.owner.number) {
|
|
46
|
+
pairingCodeShown = true;
|
|
47
|
+
sock
|
|
48
|
+
.requestPairingCode(config.owner.number)
|
|
49
|
+
.then((code) => {
|
|
50
|
+
process.stdout.write(`\nOr enter pairing code: ${code}\n` +
|
|
51
|
+
`WhatsApp → Linked Devices → Link with phone number\n\n`);
|
|
52
|
+
})
|
|
53
|
+
.catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (connection === 'open') {
|
|
57
|
+
logger.info({ user: sock.user }, 'connection open');
|
|
58
|
+
reconnectAttempts = 0;
|
|
59
|
+
currentQr = null;
|
|
60
|
+
pairingCodeShown = false;
|
|
61
|
+
}
|
|
62
|
+
if (connection === 'close') {
|
|
63
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
64
|
+
const loggedOut = statusCode === DisconnectReason.loggedOut;
|
|
65
|
+
logger.warn({ statusCode, loggedOut }, 'connection closed');
|
|
66
|
+
currentSocket = null;
|
|
67
|
+
if (loggedOut) {
|
|
68
|
+
logger.error('logged out — delete storage/auth and restart to re-pair');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
reconnectAttempts += 1;
|
|
72
|
+
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
|
73
|
+
logger.fatal({ attempts: reconnectAttempts }, 'max reconnect attempts reached, giving up');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const delayMs = Math.min(2000 * reconnectAttempts, 30000);
|
|
77
|
+
logger.info({ attempt: reconnectAttempts, delayMs }, 'reconnecting');
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
void startSocket().catch((err) => logger.error({ err }, 'reconnect failed'));
|
|
80
|
+
}, delayMs);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return sock;
|
|
84
|
+
}
|