wingman-ai 1.0.0__py3-none-any.whl
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.
- share/wingman/node_listener/package-lock.json +1785 -0
- share/wingman/node_listener/package.json +50 -0
- share/wingman/node_listener/src/index.ts +108 -0
- share/wingman/node_listener/src/ipc.ts +70 -0
- share/wingman/node_listener/src/messageHandler.ts +135 -0
- share/wingman/node_listener/src/socket.ts +244 -0
- share/wingman/node_listener/src/types.d.ts +13 -0
- share/wingman/node_listener/tsconfig.json +19 -0
- wingman/__init__.py +4 -0
- wingman/__main__.py +6 -0
- wingman/cli/__init__.py +5 -0
- wingman/cli/commands/__init__.py +1 -0
- wingman/cli/commands/auth.py +90 -0
- wingman/cli/commands/config.py +109 -0
- wingman/cli/commands/init.py +71 -0
- wingman/cli/commands/logs.py +84 -0
- wingman/cli/commands/start.py +111 -0
- wingman/cli/commands/status.py +84 -0
- wingman/cli/commands/stop.py +33 -0
- wingman/cli/commands/uninstall.py +113 -0
- wingman/cli/main.py +50 -0
- wingman/cli/wizard.py +356 -0
- wingman/config/__init__.py +31 -0
- wingman/config/paths.py +153 -0
- wingman/config/personality.py +155 -0
- wingman/config/registry.py +343 -0
- wingman/config/settings.py +294 -0
- wingman/core/__init__.py +16 -0
- wingman/core/agent.py +257 -0
- wingman/core/ipc_handler.py +124 -0
- wingman/core/llm/__init__.py +5 -0
- wingman/core/llm/client.py +77 -0
- wingman/core/memory/__init__.py +6 -0
- wingman/core/memory/context.py +109 -0
- wingman/core/memory/models.py +213 -0
- wingman/core/message_processor.py +277 -0
- wingman/core/policy/__init__.py +5 -0
- wingman/core/policy/evaluator.py +265 -0
- wingman/core/process_manager.py +135 -0
- wingman/core/safety/__init__.py +8 -0
- wingman/core/safety/cooldown.py +63 -0
- wingman/core/safety/quiet_hours.py +75 -0
- wingman/core/safety/rate_limiter.py +58 -0
- wingman/core/safety/triggers.py +117 -0
- wingman/core/transports/__init__.py +14 -0
- wingman/core/transports/base.py +106 -0
- wingman/core/transports/imessage/__init__.py +5 -0
- wingman/core/transports/imessage/db_listener.py +280 -0
- wingman/core/transports/imessage/sender.py +162 -0
- wingman/core/transports/imessage/transport.py +140 -0
- wingman/core/transports/whatsapp.py +180 -0
- wingman/daemon/__init__.py +5 -0
- wingman/daemon/manager.py +303 -0
- wingman/installer/__init__.py +5 -0
- wingman/installer/node_installer.py +253 -0
- wingman_ai-1.0.0.dist-info/METADATA +553 -0
- wingman_ai-1.0.0.dist-info/RECORD +60 -0
- wingman_ai-1.0.0.dist-info/WHEEL +4 -0
- wingman_ai-1.0.0.dist-info/entry_points.txt +2 -0
- wingman_ai-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wingman-ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WhatsApp listener component for Wingman AI - connects to WhatsApp Web using Baileys",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "tsc && node dist/index.js",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/metanoia-oss/wingman.git",
|
|
16
|
+
"directory": "node_listener"
|
|
17
|
+
},
|
|
18
|
+
"author": "Wingman Contributors",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/metanoia-oss/wingman/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/metanoia-oss/wingman#readme",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"whatsapp",
|
|
26
|
+
"baileys",
|
|
27
|
+
"chatbot",
|
|
28
|
+
"ai",
|
|
29
|
+
"wingman",
|
|
30
|
+
"whatsapp-web",
|
|
31
|
+
"messaging"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/**/*",
|
|
38
|
+
"package.json",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@whiskeysockets/baileys": "^6.7.16",
|
|
43
|
+
"pino": "^8.16.0",
|
|
44
|
+
"qrcode-terminal": "^0.12.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.10.0",
|
|
48
|
+
"typescript": "^5.3.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createSocket, sendMessage, closeSocket } from './socket';
|
|
2
|
+
import { setupStdinListener, sendToPython, log, IPCCommand } from './ipc';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle commands from Python orchestrator
|
|
6
|
+
*/
|
|
7
|
+
async function handleCommand(cmd: IPCCommand): Promise<void> {
|
|
8
|
+
log('info', 'Received command', { action: cmd.action });
|
|
9
|
+
|
|
10
|
+
switch (cmd.action) {
|
|
11
|
+
case 'send_message':
|
|
12
|
+
if (cmd.payload?.jid && cmd.payload?.text) {
|
|
13
|
+
const success = await sendMessage(cmd.payload.jid, cmd.payload.text);
|
|
14
|
+
sendToPython({
|
|
15
|
+
type: 'send_result',
|
|
16
|
+
data: {
|
|
17
|
+
success,
|
|
18
|
+
jid: cmd.payload.jid,
|
|
19
|
+
messageId: cmd.payload.messageId
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} else {
|
|
23
|
+
sendToPython({
|
|
24
|
+
type: 'error',
|
|
25
|
+
data: { message: 'send_message requires jid and text in payload' }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case 'ping':
|
|
31
|
+
sendToPython({ type: 'pong' });
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case 'shutdown':
|
|
35
|
+
log('info', 'Shutdown requested');
|
|
36
|
+
sendToPython({ type: 'shutting_down' });
|
|
37
|
+
await closeSocket();
|
|
38
|
+
process.exit(0);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
default:
|
|
42
|
+
log('warn', 'Unknown command', { action: cmd.action });
|
|
43
|
+
sendToPython({
|
|
44
|
+
type: 'error',
|
|
45
|
+
data: { message: `Unknown action: ${cmd.action}` }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Main entry point
|
|
52
|
+
*/
|
|
53
|
+
async function main(): Promise<void> {
|
|
54
|
+
log('info', 'Starting WhatsApp listener...');
|
|
55
|
+
sendToPython({ type: 'starting' });
|
|
56
|
+
|
|
57
|
+
// Set up stdin listener for commands from Python
|
|
58
|
+
setupStdinListener(handleCommand);
|
|
59
|
+
|
|
60
|
+
// Handle graceful shutdown
|
|
61
|
+
process.on('SIGINT', async () => {
|
|
62
|
+
log('info', 'SIGINT received, shutting down');
|
|
63
|
+
sendToPython({ type: 'shutting_down' });
|
|
64
|
+
await closeSocket();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
process.on('SIGTERM', async () => {
|
|
69
|
+
log('info', 'SIGTERM received, shutting down');
|
|
70
|
+
sendToPython({ type: 'shutting_down' });
|
|
71
|
+
await closeSocket();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Handle uncaught errors
|
|
76
|
+
process.on('uncaughtException', (err) => {
|
|
77
|
+
log('error', 'Uncaught exception', { error: String(err), stack: err.stack });
|
|
78
|
+
sendToPython({
|
|
79
|
+
type: 'error',
|
|
80
|
+
data: { message: `Uncaught exception: ${err.message}` }
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
process.on('unhandledRejection', (reason) => {
|
|
85
|
+
log('error', 'Unhandled rejection', { reason: String(reason) });
|
|
86
|
+
sendToPython({
|
|
87
|
+
type: 'error',
|
|
88
|
+
data: { message: `Unhandled rejection: ${reason}` }
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await createSocket();
|
|
94
|
+
log('info', 'Socket created, waiting for connection...');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log('error', 'Failed to create socket', { error: String(err) });
|
|
97
|
+
sendToPython({
|
|
98
|
+
type: 'error',
|
|
99
|
+
data: { message: `Failed to create socket: ${err}` }
|
|
100
|
+
});
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main().catch((err) => {
|
|
106
|
+
log('error', 'Fatal error in main', { error: String(err) });
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
|
|
3
|
+
export interface IPCMessage {
|
|
4
|
+
type: string;
|
|
5
|
+
data?: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface IPCCommand {
|
|
9
|
+
action: string;
|
|
10
|
+
payload?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const NULL_CHAR = '\0';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a message to Python via stdout using NULL-delimited JSON
|
|
17
|
+
*/
|
|
18
|
+
export function sendToPython(message: IPCMessage): void {
|
|
19
|
+
const json = JSON.stringify(message);
|
|
20
|
+
process.stdout.write(json + NULL_CHAR);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set up stdin listener for commands from Python
|
|
25
|
+
*/
|
|
26
|
+
export function setupStdinListener(onCommand: (cmd: IPCCommand) => void): void {
|
|
27
|
+
let buffer = '';
|
|
28
|
+
|
|
29
|
+
process.stdin.setEncoding('utf8');
|
|
30
|
+
process.stdin.on('data', (chunk: string) => {
|
|
31
|
+
buffer += chunk;
|
|
32
|
+
|
|
33
|
+
// Process all complete messages (NULL-delimited)
|
|
34
|
+
let nullIndex: number;
|
|
35
|
+
while ((nullIndex = buffer.indexOf(NULL_CHAR)) !== -1) {
|
|
36
|
+
const jsonStr = buffer.slice(0, nullIndex);
|
|
37
|
+
buffer = buffer.slice(nullIndex + 1);
|
|
38
|
+
|
|
39
|
+
if (jsonStr.trim()) {
|
|
40
|
+
try {
|
|
41
|
+
const command = JSON.parse(jsonStr) as IPCCommand;
|
|
42
|
+
onCommand(command);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
sendToPython({
|
|
45
|
+
type: 'error',
|
|
46
|
+
data: { message: `Failed to parse command: ${err}` }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
process.stdin.on('end', () => {
|
|
54
|
+
sendToPython({ type: 'stdin_closed' });
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Log a message (goes to stderr to not interfere with IPC)
|
|
61
|
+
*/
|
|
62
|
+
export function log(level: 'info' | 'warn' | 'error', message: string, data?: any): void {
|
|
63
|
+
const logEntry = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
level,
|
|
66
|
+
message,
|
|
67
|
+
...(data && { data })
|
|
68
|
+
};
|
|
69
|
+
process.stderr.write(JSON.stringify(logEntry) + '\n');
|
|
70
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { WAMessage, WASocket, proto } from '@whiskeysockets/baileys';
|
|
2
|
+
import { sendToPython, log } from './ipc';
|
|
3
|
+
|
|
4
|
+
export interface ProcessedMessage {
|
|
5
|
+
messageId: string;
|
|
6
|
+
chatId: string;
|
|
7
|
+
senderId: string;
|
|
8
|
+
senderName: string | null;
|
|
9
|
+
text: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
isGroup: boolean;
|
|
12
|
+
isSelf: boolean;
|
|
13
|
+
quotedMessage?: {
|
|
14
|
+
text: string;
|
|
15
|
+
senderId: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract text content from various message types
|
|
21
|
+
*/
|
|
22
|
+
function extractText(message: proto.IMessage | null | undefined): string | null {
|
|
23
|
+
if (!message) return null;
|
|
24
|
+
|
|
25
|
+
// Direct text message
|
|
26
|
+
if (message.conversation) {
|
|
27
|
+
return message.conversation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extended text (with mentions, links, etc.)
|
|
31
|
+
if (message.extendedTextMessage?.text) {
|
|
32
|
+
return message.extendedTextMessage.text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Image/video with caption
|
|
36
|
+
if (message.imageMessage?.caption) {
|
|
37
|
+
return message.imageMessage.caption;
|
|
38
|
+
}
|
|
39
|
+
if (message.videoMessage?.caption) {
|
|
40
|
+
return message.videoMessage.caption;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Document with caption
|
|
44
|
+
if (message.documentMessage?.caption) {
|
|
45
|
+
return message.documentMessage.caption;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Process incoming WhatsApp message
|
|
53
|
+
*/
|
|
54
|
+
export function processMessage(
|
|
55
|
+
msg: WAMessage,
|
|
56
|
+
sock: WASocket
|
|
57
|
+
): ProcessedMessage | null {
|
|
58
|
+
try {
|
|
59
|
+
const key = msg.key;
|
|
60
|
+
if (!key || !key.remoteJid) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const chatId = key.remoteJid;
|
|
65
|
+
const isGroup = chatId.endsWith('@g.us');
|
|
66
|
+
const isSelf = key.fromMe || false;
|
|
67
|
+
|
|
68
|
+
// Get sender ID
|
|
69
|
+
let senderId: string;
|
|
70
|
+
if (isGroup) {
|
|
71
|
+
senderId = key.participant || chatId;
|
|
72
|
+
} else {
|
|
73
|
+
senderId = isSelf ? (sock.user?.id || 'self') : chatId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract text
|
|
77
|
+
const text = extractText(msg.message);
|
|
78
|
+
if (!text) {
|
|
79
|
+
return null; // Skip non-text messages
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get sender name
|
|
83
|
+
const senderName = msg.pushName || null;
|
|
84
|
+
|
|
85
|
+
// Get timestamp
|
|
86
|
+
const timestamp = msg.messageTimestamp
|
|
87
|
+
? (typeof msg.messageTimestamp === 'number'
|
|
88
|
+
? msg.messageTimestamp
|
|
89
|
+
: Number(msg.messageTimestamp))
|
|
90
|
+
: Date.now() / 1000;
|
|
91
|
+
|
|
92
|
+
// Check for quoted message
|
|
93
|
+
let quotedMessage: ProcessedMessage['quotedMessage'];
|
|
94
|
+
const contextInfo = msg.message?.extendedTextMessage?.contextInfo;
|
|
95
|
+
if (contextInfo?.quotedMessage) {
|
|
96
|
+
const quotedText = extractText(contextInfo.quotedMessage);
|
|
97
|
+
if (quotedText) {
|
|
98
|
+
quotedMessage = {
|
|
99
|
+
text: quotedText,
|
|
100
|
+
senderId: contextInfo.participant || ''
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
messageId: key.id || '',
|
|
107
|
+
chatId,
|
|
108
|
+
senderId,
|
|
109
|
+
senderName,
|
|
110
|
+
text,
|
|
111
|
+
timestamp,
|
|
112
|
+
isGroup,
|
|
113
|
+
isSelf,
|
|
114
|
+
quotedMessage
|
|
115
|
+
};
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log('error', 'Failed to process message', { error: String(err) });
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle incoming messages and emit to Python
|
|
124
|
+
*/
|
|
125
|
+
export function handleMessages(messages: WAMessage[], sock: WASocket): void {
|
|
126
|
+
for (const msg of messages) {
|
|
127
|
+
const processed = processMessage(msg, sock);
|
|
128
|
+
if (processed) {
|
|
129
|
+
sendToPython({
|
|
130
|
+
type: 'message',
|
|
131
|
+
data: processed
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import makeWASocket, {
|
|
2
|
+
DisconnectReason,
|
|
3
|
+
useMultiFileAuthState,
|
|
4
|
+
WASocket,
|
|
5
|
+
ConnectionState,
|
|
6
|
+
fetchLatestBaileysVersion
|
|
7
|
+
} from '@whiskeysockets/baileys';
|
|
8
|
+
import { Boom } from '@hapi/boom';
|
|
9
|
+
import * as qrcode from 'qrcode-terminal';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import { sendToPython, log } from './ipc';
|
|
13
|
+
import { handleMessages } from './messageHandler';
|
|
14
|
+
import pino from 'pino';
|
|
15
|
+
|
|
16
|
+
const AUTH_DIR = path.join(__dirname, '..', '..', 'auth_state');
|
|
17
|
+
const STARTUP_DELAY_FILE = path.join(__dirname, '..', '..', '.last_disconnect');
|
|
18
|
+
|
|
19
|
+
let sock: WASocket | null = null;
|
|
20
|
+
let reconnectAttempts = 0;
|
|
21
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
22
|
+
let isConnecting = false; // Prevent concurrent connection attempts
|
|
23
|
+
const MIN_RESTART_DELAY_MS = 5000; // Minimum time between sessions
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Close existing socket connection gracefully
|
|
27
|
+
*/
|
|
28
|
+
async function closeExistingSocket(): Promise<void> {
|
|
29
|
+
if (sock) {
|
|
30
|
+
log('info', 'Closing existing socket connection');
|
|
31
|
+
try {
|
|
32
|
+
sock.ev.removeAllListeners('connection.update');
|
|
33
|
+
sock.ev.removeAllListeners('creds.update');
|
|
34
|
+
sock.ev.removeAllListeners('messages.upsert');
|
|
35
|
+
await sock.logout().catch(() => {}); // Ignore logout errors
|
|
36
|
+
sock.end(undefined);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
log('warn', 'Error closing socket', { error: String(err) });
|
|
39
|
+
}
|
|
40
|
+
sock = null;
|
|
41
|
+
// Give WhatsApp servers time to register the disconnect
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if we need to wait before connecting (to avoid 440 errors)
|
|
48
|
+
*/
|
|
49
|
+
async function enforceStartupDelay(): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(STARTUP_DELAY_FILE)) {
|
|
52
|
+
const lastDisconnect = parseInt(fs.readFileSync(STARTUP_DELAY_FILE, 'utf-8'), 10);
|
|
53
|
+
const elapsed = Date.now() - lastDisconnect;
|
|
54
|
+
const waitTime = MIN_RESTART_DELAY_MS - elapsed;
|
|
55
|
+
|
|
56
|
+
if (waitTime > 0) {
|
|
57
|
+
log('info', `Waiting ${waitTime}ms before connecting (anti-440 delay)`);
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Ignore errors reading the file
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Record disconnect time for anti-440 protection
|
|
68
|
+
*/
|
|
69
|
+
function recordDisconnectTime(): void {
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(STARTUP_DELAY_FILE, Date.now().toString());
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Ignore errors writing the file
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create and initialize WhatsApp socket connection
|
|
79
|
+
*/
|
|
80
|
+
export async function createSocket(): Promise<WASocket> {
|
|
81
|
+
// Prevent concurrent connection attempts
|
|
82
|
+
if (isConnecting) {
|
|
83
|
+
log('warn', 'Connection already in progress, skipping');
|
|
84
|
+
return sock!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isConnecting = true;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Wait if we disconnected recently (anti-440 protection)
|
|
91
|
+
await enforceStartupDelay();
|
|
92
|
+
|
|
93
|
+
// Close any existing connection first
|
|
94
|
+
await closeExistingSocket();
|
|
95
|
+
|
|
96
|
+
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
|
97
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
98
|
+
|
|
99
|
+
// Create silent logger for Baileys
|
|
100
|
+
const logger = pino({ level: 'silent' });
|
|
101
|
+
|
|
102
|
+
log('info', 'Creating socket with auth state', { authDir: AUTH_DIR });
|
|
103
|
+
|
|
104
|
+
sock = makeWASocket({
|
|
105
|
+
version,
|
|
106
|
+
auth: state,
|
|
107
|
+
logger,
|
|
108
|
+
browser: ['WhatsApp Agent', 'Chrome', '120.0.0'],
|
|
109
|
+
connectTimeoutMs: 60000,
|
|
110
|
+
keepAliveIntervalMs: 30000,
|
|
111
|
+
retryRequestDelayMs: 2000,
|
|
112
|
+
markOnlineOnConnect: false, // Don't mark online to reduce conflicts
|
|
113
|
+
syncFullHistory: false, // Don't sync history to reduce footprint
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Handle connection updates
|
|
117
|
+
sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
|
|
118
|
+
handleConnectionUpdate(update, saveCreds);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Save credentials on update
|
|
122
|
+
sock.ev.on('creds.update', saveCreds);
|
|
123
|
+
|
|
124
|
+
// Handle incoming messages
|
|
125
|
+
sock.ev.on('messages.upsert', async (m) => {
|
|
126
|
+
if (m.type === 'notify' && sock) {
|
|
127
|
+
handleMessages(m.messages, sock);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return sock;
|
|
132
|
+
} finally {
|
|
133
|
+
isConnecting = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle connection state changes
|
|
139
|
+
*/
|
|
140
|
+
function handleConnectionUpdate(
|
|
141
|
+
update: Partial<ConnectionState>,
|
|
142
|
+
saveCreds: () => Promise<void>
|
|
143
|
+
): void {
|
|
144
|
+
const { connection, lastDisconnect, qr } = update;
|
|
145
|
+
|
|
146
|
+
// Display QR code for scanning
|
|
147
|
+
if (qr) {
|
|
148
|
+
log('info', 'QR Code received, display in terminal');
|
|
149
|
+
process.stderr.write('\n=== Scan this QR code with WhatsApp ===\n\n');
|
|
150
|
+
qrcode.generate(qr, { small: true }, (qrString) => {
|
|
151
|
+
process.stderr.write(qrString + '\n');
|
|
152
|
+
});
|
|
153
|
+
sendToPython({ type: 'qr_code', data: { qr } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (connection === 'close') {
|
|
157
|
+
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
|
158
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
159
|
+
|
|
160
|
+
log('warn', 'Connection closed', { statusCode, shouldReconnect });
|
|
161
|
+
sendToPython({
|
|
162
|
+
type: 'disconnected',
|
|
163
|
+
data: { statusCode, shouldReconnect }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
167
|
+
log('info', 'Logged out, reconnecting for new QR code...');
|
|
168
|
+
sendToPython({ type: 'logged_out' });
|
|
169
|
+
// Reconnect to get a new QR code
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
createSocket().catch((err) => {
|
|
172
|
+
log('error', 'Reconnection failed', { error: String(err) });
|
|
173
|
+
});
|
|
174
|
+
}, 2000);
|
|
175
|
+
} else if (shouldReconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
176
|
+
reconnectAttempts++;
|
|
177
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
178
|
+
log('info', `Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
|
|
179
|
+
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
createSocket().catch((err) => {
|
|
182
|
+
log('error', 'Reconnection failed', { error: String(err) });
|
|
183
|
+
});
|
|
184
|
+
}, delay);
|
|
185
|
+
} else {
|
|
186
|
+
log('error', 'Max reconnection attempts reached');
|
|
187
|
+
sendToPython({ type: 'max_reconnect_reached' });
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
} else if (connection === 'open') {
|
|
191
|
+
reconnectAttempts = 0;
|
|
192
|
+
log('info', 'Connection established');
|
|
193
|
+
sendToPython({
|
|
194
|
+
type: 'connected',
|
|
195
|
+
data: { user: sock?.user }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Send a text message
|
|
202
|
+
*/
|
|
203
|
+
export async function sendMessage(jid: string, text: string): Promise<boolean> {
|
|
204
|
+
if (!sock) {
|
|
205
|
+
log('error', 'Cannot send message: socket not initialized');
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await sock.sendMessage(jid, { text });
|
|
211
|
+
log('info', 'Message sent', { jid, textLength: text.length });
|
|
212
|
+
return true;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
log('error', 'Failed to send message', { jid, error: String(err) });
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the current socket instance
|
|
221
|
+
*/
|
|
222
|
+
export function getSocket(): WASocket | null {
|
|
223
|
+
return sock;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gracefully close the socket connection
|
|
228
|
+
*/
|
|
229
|
+
export async function closeSocket(): Promise<void> {
|
|
230
|
+
if (sock) {
|
|
231
|
+
log('info', 'Gracefully closing socket');
|
|
232
|
+
try {
|
|
233
|
+
sock.ev.removeAllListeners('connection.update');
|
|
234
|
+
sock.ev.removeAllListeners('creds.update');
|
|
235
|
+
sock.ev.removeAllListeners('messages.upsert');
|
|
236
|
+
sock.end(undefined);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log('warn', 'Error during socket close', { error: String(err) });
|
|
239
|
+
}
|
|
240
|
+
sock = null;
|
|
241
|
+
// Record disconnect time for anti-440 protection on next startup
|
|
242
|
+
recordDisconnectTime();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare module 'qrcode-terminal' {
|
|
2
|
+
interface QRCodeOptions {
|
|
3
|
+
small?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function generate(
|
|
7
|
+
text: string,
|
|
8
|
+
options?: QRCodeOptions,
|
|
9
|
+
callback?: (qrcode: string) => void
|
|
10
|
+
): void;
|
|
11
|
+
|
|
12
|
+
export function setErrorLevel(level: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
wingman/__init__.py
ADDED
wingman/__main__.py
ADDED
wingman/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands for Wingman."""
|