@xmanrui/dsh-im 0.1.0 → 0.2.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 +32 -12
- package/THIRD_PARTY_NOTICES.md +35 -3
- package/lib/client.js +772 -68
- package/lib/index.js +255 -13339
- package/package.json +5 -4
- package/plugin-src/client/channel-logos.js +13 -0
- package/plugin-src/client/channels/shared/token-channel.js +32 -15
- package/plugin-src/client/channels/whatsapp/api.js +123 -0
- package/plugin-src/client/channels/whatsapp/index.js +433 -0
- package/plugin-src/client/channels/whatsapp/styles.js +19 -0
- package/plugin-src/client/index.js +20 -2
- package/plugin-src/client/styles.js +3 -1
- package/plugin-src/host/build.mjs +22 -2
- package/plugin-src/host/channels/whatsapp/index.mjs +35 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +121 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +140 -0
- package/plugin-src/host/index.mjs +3 -0
- package/scripts/verify-package.mjs +28 -2
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/whatsapp/config-store.mjs +165 -0
- package/src/channels/whatsapp/harness-client.mjs +3 -0
- package/src/channels/whatsapp/state-store.mjs +3 -0
- package/src/channels/whatsapp/whatsapp-bridge.mjs +15 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +388 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +299 -0
- package/src/channels/whatsapp/whatsapp-web-session.mjs +212 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { chmod, mkdir, readdir } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import makeWASocket, {
|
|
4
|
+
Browsers,
|
|
5
|
+
DisconnectReason,
|
|
6
|
+
jidNormalizedUser,
|
|
7
|
+
useMultiFileAuthState,
|
|
8
|
+
} from '@whiskeysockets/baileys';
|
|
9
|
+
|
|
10
|
+
const SILENT_LOGGER = Object.freeze({
|
|
11
|
+
level: 'silent',
|
|
12
|
+
trace() {},
|
|
13
|
+
debug() {},
|
|
14
|
+
info() {},
|
|
15
|
+
warn() {},
|
|
16
|
+
error() {},
|
|
17
|
+
fatal() {},
|
|
18
|
+
child() { return this; },
|
|
19
|
+
});
|
|
20
|
+
const APPEND_RECENT_GRACE_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
function abortError() {
|
|
23
|
+
return Object.assign(new Error('WhatsApp connection was cancelled'), { name: 'AbortError' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function disconnectStatus(error) {
|
|
27
|
+
return error?.output?.statusCode ?? error?.data?.statusCode ?? error?.statusCode ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function messageTimestampMs(value) {
|
|
31
|
+
let seconds = value;
|
|
32
|
+
if (typeof seconds === 'string') {
|
|
33
|
+
if (!/^\d+$/.test(seconds)) return null;
|
|
34
|
+
seconds = Number(seconds);
|
|
35
|
+
} else if (typeof seconds === 'bigint') {
|
|
36
|
+
seconds = Number(seconds);
|
|
37
|
+
} else if (seconds && typeof seconds === 'object') {
|
|
38
|
+
seconds = Number(seconds.valueOf());
|
|
39
|
+
}
|
|
40
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1_000 : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function hardenAuthDirectory(path) {
|
|
44
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
45
|
+
await chmod(path, 0o700);
|
|
46
|
+
const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
|
|
47
|
+
await Promise.all(entries.filter((entry) => entry.isFile())
|
|
48
|
+
.map((entry) => chmod(`${path}/${entry.name}`, 0o600).catch(() => undefined)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeIdentity(socket, authState) {
|
|
52
|
+
const source = socket.user ?? authState.creds.me;
|
|
53
|
+
const accountJid = jidNormalizedUser(source?.id);
|
|
54
|
+
if (!/^\d{5,32}@(s\.whatsapp\.net|lid)$/.test(accountJid ?? '')) {
|
|
55
|
+
throw new Error('WhatsApp did not return a valid linked account');
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
accountJid,
|
|
59
|
+
name: typeof source?.name === 'string' && source.name.trim()
|
|
60
|
+
? source.name.trim().slice(0, 100) : 'WhatsApp机器人',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function createWhatsappWebSession({
|
|
65
|
+
authDir,
|
|
66
|
+
onQr,
|
|
67
|
+
onMessage,
|
|
68
|
+
onDisconnect,
|
|
69
|
+
signal,
|
|
70
|
+
logger = console,
|
|
71
|
+
makeSocket = makeWASocket,
|
|
72
|
+
loadAuthState = useMultiFileAuthState,
|
|
73
|
+
} = {}) {
|
|
74
|
+
if (!authDir || typeof onQr !== 'function') {
|
|
75
|
+
throw new TypeError('WhatsApp Web session requires an auth directory and QR callback');
|
|
76
|
+
}
|
|
77
|
+
await hardenAuthDirectory(authDir);
|
|
78
|
+
const sessionStartedAt = Date.now();
|
|
79
|
+
const { state, saveCreds } = await loadAuthState(authDir);
|
|
80
|
+
const originalKeySet = state.keys.set.bind(state.keys);
|
|
81
|
+
state.keys.set = async (data) => {
|
|
82
|
+
await originalKeySet(data);
|
|
83
|
+
await hardenAuthDirectory(authDir);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let closed = false;
|
|
87
|
+
let readySettled = false;
|
|
88
|
+
let resolveReady;
|
|
89
|
+
let rejectReady;
|
|
90
|
+
let saveQueue = Promise.resolve();
|
|
91
|
+
let socket = null;
|
|
92
|
+
let socketGeneration = 0;
|
|
93
|
+
let restartTask = null;
|
|
94
|
+
const ready = new Promise((resolve, reject) => {
|
|
95
|
+
resolveReady = resolve;
|
|
96
|
+
rejectReady = reject;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const settleFailure = (error) => {
|
|
100
|
+
if (readySettled) return;
|
|
101
|
+
readySettled = true;
|
|
102
|
+
rejectReady(error);
|
|
103
|
+
};
|
|
104
|
+
const close = async () => {
|
|
105
|
+
if (closed) return;
|
|
106
|
+
closed = true;
|
|
107
|
+
socketGeneration += 1;
|
|
108
|
+
settleFailure(abortError());
|
|
109
|
+
await restartTask?.catch(() => undefined);
|
|
110
|
+
await saveQueue.catch(() => undefined);
|
|
111
|
+
await socket?.end(undefined).catch(() => undefined);
|
|
112
|
+
};
|
|
113
|
+
const logout = async () => {
|
|
114
|
+
if (closed) return;
|
|
115
|
+
closed = true;
|
|
116
|
+
socketGeneration += 1;
|
|
117
|
+
settleFailure(abortError());
|
|
118
|
+
await restartTask?.catch(() => undefined);
|
|
119
|
+
await saveQueue.catch(() => undefined);
|
|
120
|
+
await socket?.logout('Removed from DeepSeek Harness').catch(() => undefined);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const startSocket = () => {
|
|
124
|
+
const generation = ++socketGeneration;
|
|
125
|
+
let connectionOpen = false;
|
|
126
|
+
const nextSocket = makeSocket({
|
|
127
|
+
auth: state,
|
|
128
|
+
browser: Browsers.macOS('DeepSeek Harness'),
|
|
129
|
+
logger: SILENT_LOGGER,
|
|
130
|
+
markOnlineOnConnect: false,
|
|
131
|
+
syncFullHistory: false,
|
|
132
|
+
shouldSyncHistoryMessage: () => false,
|
|
133
|
+
getMessage: async () => undefined,
|
|
134
|
+
generateHighQualityLinkPreview: false,
|
|
135
|
+
});
|
|
136
|
+
socket = nextSocket;
|
|
137
|
+
|
|
138
|
+
const resolveWhenLinked = () => {
|
|
139
|
+
if (!connectionOpen || !state.creds.me || readySettled) return;
|
|
140
|
+
void saveQueue.then(() => {
|
|
141
|
+
if (closed || readySettled || generation !== socketGeneration
|
|
142
|
+
|| !connectionOpen || !state.creds.me) return;
|
|
143
|
+
readySettled = true;
|
|
144
|
+
resolveReady(normalizeIdentity(nextSocket, state));
|
|
145
|
+
}).catch((error) => settleFailure(error));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
nextSocket.ev.on('creds.update', () => {
|
|
149
|
+
if (closed || generation !== socketGeneration) return;
|
|
150
|
+
saveQueue = saveQueue.then(async () => {
|
|
151
|
+
await saveCreds();
|
|
152
|
+
await hardenAuthDirectory(authDir);
|
|
153
|
+
});
|
|
154
|
+
saveQueue.catch(() => logger.error?.('[dsh-im:whatsapp] failed to persist linked-device state'));
|
|
155
|
+
resolveWhenLinked();
|
|
156
|
+
});
|
|
157
|
+
nextSocket.ev.on('connection.update', (update) => {
|
|
158
|
+
if (closed || generation !== socketGeneration) return;
|
|
159
|
+
if (typeof update.qr === 'string' && update.qr) onQr(update.qr);
|
|
160
|
+
if (update.connection === 'open') {
|
|
161
|
+
connectionOpen = true;
|
|
162
|
+
resolveWhenLinked();
|
|
163
|
+
}
|
|
164
|
+
if (update.connection === 'close') {
|
|
165
|
+
const status = disconnectStatus(update.lastDisconnect?.error);
|
|
166
|
+
if (status === DisconnectReason.restartRequired) {
|
|
167
|
+
restartTask ??= saveQueue.then(async () => {
|
|
168
|
+
if (closed || generation !== socketGeneration) return;
|
|
169
|
+
await nextSocket.end(undefined).catch(() => undefined);
|
|
170
|
+
if (closed || generation !== socketGeneration) return;
|
|
171
|
+
startSocket();
|
|
172
|
+
}).catch((error) => settleFailure(error)).finally(() => {
|
|
173
|
+
restartTask = null;
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const loggedOut = status === DisconnectReason.loggedOut;
|
|
178
|
+
const error = Object.assign(new Error(loggedOut
|
|
179
|
+
? 'WhatsApp linked device was removed from the phone'
|
|
180
|
+
: 'WhatsApp Web connection closed'), { code: loggedOut ? 'logged-out' : 'connection-closed' });
|
|
181
|
+
if (!readySettled) settleFailure(error);
|
|
182
|
+
else onDisconnect?.({ error, loggedOut });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
nextSocket.ev.on('messages.upsert', ({ messages, type }) => {
|
|
186
|
+
if (closed || generation !== socketGeneration
|
|
187
|
+
|| (type !== 'notify' && type !== 'append') || typeof onMessage !== 'function') return;
|
|
188
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
189
|
+
if (type === 'append') {
|
|
190
|
+
const timestamp = messageTimestampMs(message?.messageTimestamp);
|
|
191
|
+
if (timestamp === null || timestamp < sessionStartedAt - APPEND_RECENT_GRACE_MS) continue;
|
|
192
|
+
}
|
|
193
|
+
Promise.resolve(onMessage(message)).catch(() => {
|
|
194
|
+
logger.error?.('[dsh-im:whatsapp] failed to process an inbound WhatsApp message');
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
startSocket();
|
|
201
|
+
|
|
202
|
+
if (signal) {
|
|
203
|
+
if (signal.aborted) await close();
|
|
204
|
+
else signal.addEventListener('abort', () => void close(), { once: true });
|
|
205
|
+
}
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
get socket() { return socket; },
|
|
208
|
+
ready,
|
|
209
|
+
close,
|
|
210
|
+
logout,
|
|
211
|
+
});
|
|
212
|
+
}
|