@vanzxysenpai/baileys 0.0.1 → 0.0.2
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/CHANGELOG.md +30 -0
- package/NOTICE.md +1 -1
- package/README.md +30 -0
- package/lib/VoIP/audio-feeder.d.ts +15 -0
- package/lib/VoIP/audio-feeder.js +132 -0
- package/lib/VoIP/index.d.ts +22 -0
- package/lib/VoIP/index.js +277 -0
- package/lib/VoIP/relay-transport.d.ts +16 -0
- package/lib/VoIP/relay-transport.js +559 -0
- package/lib/VoIP/signaling.d.ts +16 -0
- package/lib/VoIP/signaling.js +624 -0
- package/lib/VoIP/types.d.ts +11 -0
- package/lib/VoIP/types.js +17 -0
- package/lib/VoIP/wasm-engine.d.ts +28 -0
- package/lib/VoIP/wasm-engine.js +1214 -0
- package/lib/VoIP/worker-bootstrap.d.ts +1 -0
- package/lib/VoIP/worker-bootstrap.js +1042 -0
- package/lib/assets/wasm/loader.js +5 -0
- package/lib/assets/wasm/whatsapp.wasm +0 -0
- package/lib/assets/wasm/worker-modules.js +273 -0
- package/lib/exports-extra.d.ts +4 -0
- package/lib/exports-extra.js +4 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -0
- package/package.json +8 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Experimental audio voice calls (`VoipClient`, `ActiveCall`, `CallState`; WASM call stack + WebRTC relay ported from ourin-baileys 9.0.11). Opt-in, needs the optional `@roamhq/wrtc` peer dependency.
|
|
7
|
+
- Startup banner (`VanzxyBanner.js`): printed once per process on import and via the `prestart` script; opt out with `VANZXY_BANNER=0` (also respects `NO_COLOR`, plain text when not a TTY).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Repository links now point to `github.com/vanzxysenpai/vanzxysenpai`.
|
|
11
|
+
- Removed internal change-log style code comments (third-party attribution headers are kept).
|
|
12
|
+
- VoIP only runs when you create a `VoipClient`; there are still no connect-time account actions.
|
|
13
|
+
|
|
14
|
+
## 0.0.1
|
|
15
|
+
|
|
16
|
+
First release of `@vanzxysenpai/baileys`, built on WhiskeySockets/Baileys 7.0.0-rc14.
|
|
17
|
+
|
|
18
|
+
### Included
|
|
19
|
+
- rc14 core plus the `albumParentKey` send option.
|
|
20
|
+
- Message builders (`AIRich`, `Button`/`ButtonV2`/`ButtonV3`, `Carousel`, `Poll`, `Album`, `Event`, `A2UI`), interactive / native-flow send layer, group status, newsletter / username / status extras, stickerpacks, `Bot` framework.
|
|
21
|
+
- Merged `WAProto` containing every message class and field found in rc14 and newer community protos, with type declarations.
|
|
22
|
+
- Stability and security helpers: reconnect backoff, Bad MAC handler, connection watchdog, auto-reconnect, encrypted auth state, SQLite / Mongo / MySQL / PostgreSQL / Redis backends, anti force-close payload guard (`createAnchorGuard`).
|
|
23
|
+
- Media helpers (`resizeImage`, `convertToOpusAudio`, `convertToWhatsAppVideo`, `getVideoThumbnail`, `probeMedia`, `getMp4Duration`), `TTLCache`, social link-preview helpers.
|
|
24
|
+
|
|
25
|
+
### Behaviour notes
|
|
26
|
+
- `useMultiFileAuthState` writes `creds.json` atomically and keeps `creds.json.bak`; a corrupt file is restored from the backup or moved aside as `creds.json.corrupt-*`.
|
|
27
|
+
- No install-time scripts, no connect-time account actions, no hidden message attribution.
|
|
28
|
+
- `profilePictureUrl` defaults to `'image'`; the tc-token is opt-in (`shouldIncludeTcToken`).
|
|
29
|
+
- Android browser platform is not enabled (platform is always WEB).
|
|
30
|
+
- `Message.pollResultSnapshotMessageV3` keeps the field number used by the bundled proto.
|
package/NOTICE.md
CHANGED
|
@@ -106,7 +106,7 @@ below was MIT-licensed at its source at the time it was ported.
|
|
|
106
106
|
### ourin-baileys ports
|
|
107
107
|
- **Source:** ourin-baileys (version 9.0.11 for the VoIP stack, per this package's `lib/index.js`).
|
|
108
108
|
- **Location in this package:**
|
|
109
|
-
-
|
|
109
|
+
- `lib/VoIP/`, `lib/assets/wasm/` — audio call WASM stack + WebRTC relay (the bundled `whatsapp.wasm`, ~9.8 MB, comes with this port; its redistribution terms follow the ourin-baileys package it was taken from). Opt-in: nothing runs unless a `VoipClient` is created.
|
|
110
110
|
- `lib/Builders/AIRich.js` — the `ORich` no-op subclass of `AIRich`, kept only for drop-in compatibility with code originally written against ourin-baileys.
|
|
111
111
|
|
|
112
112
|
---
|
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
- [Carousel](#carousel)
|
|
60
60
|
- [AIRich — rich response cards](#airich--rich-response-cards)
|
|
61
61
|
- [📁 Builders Folder Map](#-builders-folder-map-libbuilders)
|
|
62
|
+
- [📞 Voice Calls (experimental)](#-voice-calls-experimental)
|
|
62
63
|
- [🔎 User Sync Queries](#-user-sync-queries)
|
|
63
64
|
- [👤 Username Management](#-username-management)
|
|
64
65
|
- [🤖 Bot Framework](#-bot-framework)
|
|
@@ -85,6 +86,14 @@
|
|
|
85
86
|
|
|
86
87
|
---
|
|
87
88
|
|
|
89
|
+
## Changelog 0.0.2
|
|
90
|
+
|
|
91
|
+
- **Voice calls (experimental, opt-in):** `VoipClient`, `ActiveCall`, `CallState` — WASM call stack + WebRTC relay ported from ourin-baileys; needs the optional `@roamhq/wrtc` peer dependency.
|
|
92
|
+
- **Startup banner:** `VanzxyBanner.js`, shown once per process; disable with `VANZXY_BANNER=0`.
|
|
93
|
+
- **Cleanup:** repository links now point to `github.com/vanzxysenpai/vanzxysenpai`; internal change-log style code comments removed.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
88
97
|
## Changelog 0.0.1
|
|
89
98
|
|
|
90
99
|
First release of **@vanzxysenpai/baileys**, built on top of WhiskeySockets/Baileys 7.0.0-rc14.
|
|
@@ -578,6 +587,27 @@ await vx.poll()
|
|
|
578
587
|
|
|
579
588
|
---
|
|
580
589
|
|
|
590
|
+
## 📞 Voice Calls (experimental)
|
|
591
|
+
|
|
592
|
+
Audio-call support (ported from ourin-baileys) via a WASM call stack + WebRTC relay. It is **opt-in**: nothing starts until you create a `VoipClient`. Requires the optional `@roamhq/wrtc` peer dependency.
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
import { VoipClient } from '@vanzxysenpai/baileys'
|
|
596
|
+
|
|
597
|
+
const voip = new VoipClient({ resourcesPath: './voip-resources' })
|
|
598
|
+
await voip.connectWithSocket(sock)
|
|
599
|
+
|
|
600
|
+
const call = await voip.call('628123456789')
|
|
601
|
+
|
|
602
|
+
call.on('ringing', () => console.log('Ringing...'))
|
|
603
|
+
call.on('connected', () => console.log('Call connected'))
|
|
604
|
+
call.on('ended', (reason) => console.log('Call ended:', reason))
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`ActiveCall` and `CallState` are exported too. Use calls responsibly: unsolicited or bulk calling is the fastest way to get a number restricted by WhatsApp.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
581
611
|
## 🔎 User Sync Queries
|
|
582
612
|
|
|
583
613
|
`WAUSync` (`USyncQuery` / `USyncUser` + protocols) lets you check things like WhatsApp registration, device lists, status, and username info for a JID before you message it — the same mechanism behind `sock.onWhatsApp()`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class AudioFeeder {
|
|
2
|
+
constructor(sampleRate: any, channels: any, framesPerChunk: any, onChunk: any, source?: string);
|
|
3
|
+
sampleRate: any;
|
|
4
|
+
channels: any;
|
|
5
|
+
framesPerChunk: any;
|
|
6
|
+
onChunk: any;
|
|
7
|
+
source: string;
|
|
8
|
+
droppedChunks: number;
|
|
9
|
+
underflowChunks: number;
|
|
10
|
+
bytesProduced: number;
|
|
11
|
+
chunksEmitted: number;
|
|
12
|
+
start: () => void;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
#private;
|
|
15
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio feeder.
|
|
3
|
+
*
|
|
4
|
+
* Spawns ffmpeg to decode `source` into f32le PCM at the requested rate, then
|
|
5
|
+
* meters frames out at chunk-cadence to the WASM uplink.
|
|
6
|
+
*
|
|
7
|
+
* @author ShellTear
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
const LOW_WATERMARK_CHUNKS = 16;
|
|
11
|
+
const MAX_QUEUED_CHUNKS = 1024;
|
|
12
|
+
const DEFAULT_WARMUP_MS = 500;
|
|
13
|
+
export class AudioFeeder {
|
|
14
|
+
sampleRate;
|
|
15
|
+
channels;
|
|
16
|
+
framesPerChunk;
|
|
17
|
+
onChunk;
|
|
18
|
+
source;
|
|
19
|
+
#proc = null;
|
|
20
|
+
#pending = Buffer.alloc(0);
|
|
21
|
+
#queue = [];
|
|
22
|
+
#emitTimer = null;
|
|
23
|
+
#nextEmitAtMs = 0;
|
|
24
|
+
#warmupUntilMs = 0;
|
|
25
|
+
droppedChunks = 0;
|
|
26
|
+
underflowChunks = 0;
|
|
27
|
+
bytesProduced = 0;
|
|
28
|
+
chunksEmitted = 0;
|
|
29
|
+
constructor(sampleRate, channels, framesPerChunk, onChunk, source = "silence") {
|
|
30
|
+
this.sampleRate = sampleRate;
|
|
31
|
+
this.channels = channels;
|
|
32
|
+
this.framesPerChunk = framesPerChunk;
|
|
33
|
+
this.onChunk = onChunk;
|
|
34
|
+
this.source = source;
|
|
35
|
+
}
|
|
36
|
+
start = () => {
|
|
37
|
+
if (this.#proc)
|
|
38
|
+
return;
|
|
39
|
+
const chunkSamples = this.framesPerChunk * this.channels;
|
|
40
|
+
const chunkBytes = chunkSamples * Float32Array.BYTES_PER_ELEMENT;
|
|
41
|
+
const chunkIntervalMs = (this.framesPerChunk / this.sampleRate) * 1000;
|
|
42
|
+
const inputArgs = this.#resolveInputArgs();
|
|
43
|
+
this.#proc = spawn("ffmpeg", [
|
|
44
|
+
"-hide_banner",
|
|
45
|
+
"-loglevel", "error",
|
|
46
|
+
"-thread_queue_size", "512",
|
|
47
|
+
...inputArgs,
|
|
48
|
+
"-f", "f32le",
|
|
49
|
+
"-ac", String(this.channels),
|
|
50
|
+
"-ar", String(this.sampleRate),
|
|
51
|
+
"pipe:1",
|
|
52
|
+
]);
|
|
53
|
+
this.#proc.stdout.on("data", (chunk) => {
|
|
54
|
+
this.#pending = Buffer.concat([this.#pending, chunk]);
|
|
55
|
+
while (this.#pending.length >= chunkBytes) {
|
|
56
|
+
if (this.#queue.length >= MAX_QUEUED_CHUNKS) {
|
|
57
|
+
this.#proc?.stdout.pause();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
const frame = this.#pending.subarray(0, chunkBytes);
|
|
61
|
+
this.#pending = this.#pending.subarray(chunkBytes);
|
|
62
|
+
const out = new Float32Array(chunkSamples);
|
|
63
|
+
out.set(new Float32Array(frame.buffer, frame.byteOffset, chunkSamples));
|
|
64
|
+
this.bytesProduced += chunkBytes;
|
|
65
|
+
this.#queue.push(out);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
this.#proc.stderr.on("data", (chunk) => {
|
|
69
|
+
process.stderr.write(`[AudioFeeder] ${chunk.toString().trim()}\n`);
|
|
70
|
+
});
|
|
71
|
+
this.#proc.on("exit", (code) => {
|
|
72
|
+
if (code !== 0 && code !== null) {
|
|
73
|
+
process.stderr.write(`[AudioFeeder] ffmpeg exited with code=${code}\n`);
|
|
74
|
+
}
|
|
75
|
+
this.#proc = null;
|
|
76
|
+
});
|
|
77
|
+
this.#nextEmitAtMs = 0;
|
|
78
|
+
this.#warmupUntilMs = Date.now() + DEFAULT_WARMUP_MS;
|
|
79
|
+
this.#scheduleNext(chunkSamples, chunkIntervalMs);
|
|
80
|
+
};
|
|
81
|
+
stop = () => {
|
|
82
|
+
if (this.#emitTimer) {
|
|
83
|
+
clearTimeout(this.#emitTimer);
|
|
84
|
+
this.#emitTimer = null;
|
|
85
|
+
}
|
|
86
|
+
this.#proc?.kill("SIGTERM");
|
|
87
|
+
this.#proc = null;
|
|
88
|
+
this.#pending = Buffer.alloc(0);
|
|
89
|
+
this.#queue = [];
|
|
90
|
+
this.#warmupUntilMs = 0;
|
|
91
|
+
};
|
|
92
|
+
#resolveInputArgs = () => {
|
|
93
|
+
if (!this.source || this.source === "silence") {
|
|
94
|
+
return ["-f", "lavfi", "-i", `aevalsrc=0:d=3600:s=${this.sampleRate}`];
|
|
95
|
+
}
|
|
96
|
+
if (this.source.startsWith("lavfi:")) {
|
|
97
|
+
return ["-f", "lavfi", "-i", this.source.slice("lavfi:".length)];
|
|
98
|
+
}
|
|
99
|
+
return ["-i", this.source];
|
|
100
|
+
};
|
|
101
|
+
#scheduleNext = (chunkSamples, chunkIntervalMs) => {
|
|
102
|
+
if (!this.#proc)
|
|
103
|
+
return;
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (this.#nextEmitAtMs === 0)
|
|
106
|
+
this.#nextEmitAtMs = now;
|
|
107
|
+
const delayMs = Math.max(0, this.#nextEmitAtMs - now);
|
|
108
|
+
this.#emitTimer = setTimeout(() => {
|
|
109
|
+
this.#emitTimer = null;
|
|
110
|
+
if (this.#queue.length < LOW_WATERMARK_CHUNKS && Date.now() < this.#warmupUntilMs) {
|
|
111
|
+
this.#nextEmitAtMs = Date.now() + 10;
|
|
112
|
+
this.#scheduleNext(chunkSamples, chunkIntervalMs);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.#flushOne(chunkSamples);
|
|
116
|
+
this.#nextEmitAtMs += chunkIntervalMs;
|
|
117
|
+
this.#scheduleNext(chunkSamples, chunkIntervalMs);
|
|
118
|
+
}, delayMs);
|
|
119
|
+
};
|
|
120
|
+
#flushOne = (chunkSamples) => {
|
|
121
|
+
let nextChunk = this.#queue.shift();
|
|
122
|
+
if (!nextChunk) {
|
|
123
|
+
nextChunk = new Float32Array(chunkSamples);
|
|
124
|
+
this.underflowChunks += 1;
|
|
125
|
+
}
|
|
126
|
+
this.chunksEmitted += 1;
|
|
127
|
+
this.onChunk(nextChunk);
|
|
128
|
+
if (this.#proc?.stdout.isPaused() && this.#queue.length <= MAX_QUEUED_CHUNKS / 4) {
|
|
129
|
+
this.#proc.stdout.resume();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { CallState } from "./types.js";
|
|
2
|
+
export class ActiveCall {
|
|
3
|
+
constructor(callId: any, engine: any, durationMs: any);
|
|
4
|
+
_audioSource: string;
|
|
5
|
+
callId: any;
|
|
6
|
+
engine: any;
|
|
7
|
+
get state(): number;
|
|
8
|
+
end: () => void;
|
|
9
|
+
mute: (muted: any) => void;
|
|
10
|
+
waitForEnd: () => Promise<any>;
|
|
11
|
+
_updateState: (state: any) => void;
|
|
12
|
+
_emitAudio: (pcm: any) => void;
|
|
13
|
+
_forceEnd: (reason: any) => void;
|
|
14
|
+
#private;
|
|
15
|
+
}
|
|
16
|
+
export class VoipClient {
|
|
17
|
+
constructor(config?: {});
|
|
18
|
+
connectWithSocket: (existingSock: any) => Promise<void>;
|
|
19
|
+
call: (phoneNumber: any, opts?: {}) => Promise<ActiveCall>;
|
|
20
|
+
disconnect: () => void;
|
|
21
|
+
#private;
|
|
22
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { randomBytes, createHmac } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import { WasmEngine } from "./wasm-engine.js";
|
|
5
|
+
import { RelayRtcTransport } from "./relay-transport.js";
|
|
6
|
+
import { SignalingBridge } from "./signaling.js";
|
|
7
|
+
import { AudioFeeder } from "./audio-feeder.js";
|
|
8
|
+
import { CallState } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export { CallState } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const SHA256_LEN = 32;
|
|
13
|
+
|
|
14
|
+
const toBareJid = (jid) => {
|
|
15
|
+
if (!jid) return jid;
|
|
16
|
+
const at = jid.indexOf("@");
|
|
17
|
+
if (at < 0) return jid;
|
|
18
|
+
const user = jid.slice(0, at).split(":")[0];
|
|
19
|
+
return `${user}@${jid.slice(at + 1)}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const computeHkdf = (key, salt, info, length) => {
|
|
23
|
+
const effectiveSalt = salt && salt.length > 0 ? Buffer.from(salt) : Buffer.alloc(SHA256_LEN, 0);
|
|
24
|
+
const prk = createHmac("sha256", effectiveSalt).update(key).digest();
|
|
25
|
+
const blocks = Math.ceil(length / SHA256_LEN);
|
|
26
|
+
const okm = Buffer.alloc(blocks * SHA256_LEN);
|
|
27
|
+
let prev = Buffer.alloc(0);
|
|
28
|
+
for (let i = 1; i <= blocks; i += 1) {
|
|
29
|
+
prev = createHmac("sha256", prk)
|
|
30
|
+
.update(prev)
|
|
31
|
+
.update(info)
|
|
32
|
+
.update(Buffer.from([i]))
|
|
33
|
+
.digest();
|
|
34
|
+
prev.copy(okm, (i - 1) * SHA256_LEN);
|
|
35
|
+
}
|
|
36
|
+
return new Uint8Array(okm.buffer, okm.byteOffset, length);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const computeHmacSha256 = (data, key) => {
|
|
40
|
+
const result = createHmac("sha256", Buffer.from(key)).update(data).digest();
|
|
41
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isCallReceiptNode = (node) => {
|
|
45
|
+
if (node?.tag !== "receipt") return false;
|
|
46
|
+
const child = Array.isArray(node.content) ? node.content[0] : null;
|
|
47
|
+
return !!(child?.attrs?.["call-id"] || child?.attrs?.call_id);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class ActiveCall extends EventEmitter {
|
|
51
|
+
#state = CallState.Idle;
|
|
52
|
+
#endResolver;
|
|
53
|
+
#endPromise;
|
|
54
|
+
#endTimer = null;
|
|
55
|
+
#ended = false;
|
|
56
|
+
_audioSource = "silence";
|
|
57
|
+
|
|
58
|
+
constructor(callId, engine, durationMs) {
|
|
59
|
+
super();
|
|
60
|
+
this.callId = callId;
|
|
61
|
+
this.engine = engine;
|
|
62
|
+
this.#endPromise = new Promise((res) => { this.#endResolver = res; });
|
|
63
|
+
if (durationMs > 0) {
|
|
64
|
+
this.#endTimer = setTimeout(() => this.end(), durationMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get state() { return this.#state; }
|
|
69
|
+
|
|
70
|
+
end = () => {
|
|
71
|
+
if (this.#ended) return;
|
|
72
|
+
this.#ended = true;
|
|
73
|
+
if (this.#endTimer) { clearTimeout(this.#endTimer); this.#endTimer = null; }
|
|
74
|
+
try { this.engine.endCall(0, true); } catch {}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
mute = (muted) => {
|
|
78
|
+
try { this.engine.setMute(muted); } catch {}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
waitForEnd = () => this.#endPromise;
|
|
82
|
+
|
|
83
|
+
_updateState = (state) => {
|
|
84
|
+
this.#state = state;
|
|
85
|
+
if (state === CallState.PreacceptReceived) this.emit("ringing");
|
|
86
|
+
else if (state === CallState.Active) this.emit("connected");
|
|
87
|
+
else if (state === CallState.Idle || state === CallState.Ending) {
|
|
88
|
+
this._forceEnd("ended");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
_emitAudio = (pcm) => { this.emit("audio", pcm); };
|
|
93
|
+
|
|
94
|
+
_forceEnd = (reason) => {
|
|
95
|
+
if (this.#ended) return;
|
|
96
|
+
this.#ended = true;
|
|
97
|
+
if (this.#endTimer) { clearTimeout(this.#endTimer); this.#endTimer = null; }
|
|
98
|
+
this.emit("ended", reason);
|
|
99
|
+
this.#endResolver(reason);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class VoipClient {
|
|
104
|
+
#config;
|
|
105
|
+
#engine = null;
|
|
106
|
+
#relay = null;
|
|
107
|
+
#signaling = null;
|
|
108
|
+
#sock = null;
|
|
109
|
+
#activeCall = null;
|
|
110
|
+
#capturePtr = 0;
|
|
111
|
+
#captureChunkBytes = 0;
|
|
112
|
+
#captureSampleRate = 16000;
|
|
113
|
+
#captureChannels = 1;
|
|
114
|
+
#captureFramesPerChunk = 320;
|
|
115
|
+
#feeder = null;
|
|
116
|
+
|
|
117
|
+
constructor(config = {}) {
|
|
118
|
+
this.#config = config;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
connectWithSocket = async (existingSock) => {
|
|
122
|
+
this.#sock = existingSock;
|
|
123
|
+
await this.#initVoipStack();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
#initVoipStack = async () => {
|
|
127
|
+
this.#signaling = new SignalingBridge({ sock: this.#sock });
|
|
128
|
+
await this.#signaling.init();
|
|
129
|
+
|
|
130
|
+
this.#relay = new RelayRtcTransport({
|
|
131
|
+
onTransportMessage: (data, ip, port) => this.#engine?.handleOnTransportMessage(data, ip, port),
|
|
132
|
+
onIceRtt: (rttMs, ip, port) => this.#engine?.updateIceRtt(rttMs, ip, port),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.#engine = new WasmEngine({
|
|
136
|
+
resourcesPath: this.#config.resourcesPath,
|
|
137
|
+
callbacks: {
|
|
138
|
+
onSignalingXmpp: (peerJid, callId, xmlPayload) =>
|
|
139
|
+
this.#signaling.sendSignaling(peerJid, callId, xmlPayload),
|
|
140
|
+
onCallEvent: (eventType, eventData) => this.#handleCallEvent(eventType, eventData),
|
|
141
|
+
sendDataToRelay: (data, ip, port) => this.#relay.send(data, ip, port),
|
|
142
|
+
onAudioCaptureInit: (config) => this.#handleAudioCaptureInit(config),
|
|
143
|
+
onAudioCaptureStart: () => this.#handleAudioCaptureStart(),
|
|
144
|
+
onAudioCaptureStop: () => this.#handleAudioCaptureStop(),
|
|
145
|
+
onAudioPlaybackData: (audioData) => this.#activeCall?._emitAudio(audioData),
|
|
146
|
+
cryptoHkdf: computeHkdf,
|
|
147
|
+
hmacSha256: computeHmacSha256,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await this.#engine.initialize();
|
|
152
|
+
this.#signaling.attachEngine(this.#engine);
|
|
153
|
+
|
|
154
|
+
const selfPnJid = this.#sock.authState.creds.me?.id;
|
|
155
|
+
const selfLidJid = this.#sock.authState.creds.me?.lid;
|
|
156
|
+
this.#engine.initVoipStack(selfPnJid, toBareJid(selfPnJid), selfLidJid);
|
|
157
|
+
await this.#engine.waitForVoipStackReady();
|
|
158
|
+
try { this.#engine.updateNetworkMedium(2, 0); } catch {}
|
|
159
|
+
|
|
160
|
+
this.#sock.ws.on("CB:call", (node) => {
|
|
161
|
+
this.#signaling.processIncomingCall(node, this.#engine, this.#activeCall?.callId ?? "");
|
|
162
|
+
});
|
|
163
|
+
this.#sock.ws.on("CB:receipt", (node) => {
|
|
164
|
+
if (!isCallReceiptNode(node)) return;
|
|
165
|
+
this.#signaling.processIncomingReceipt(node, this.#engine, this.#activeCall?.callId ?? "");
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
call = async (phoneNumber, opts = {}) => {
|
|
170
|
+
if (!this.#engine || !this.#signaling) throw new Error("Not connected. Call connectWithSocket() first.");
|
|
171
|
+
if (this.#activeCall) throw new Error("A call is already active.");
|
|
172
|
+
|
|
173
|
+
const targetNumber = phoneNumber.replace(/\D/g, "");
|
|
174
|
+
const targetPnJid = `${targetNumber}@s.whatsapp.net`;
|
|
175
|
+
const durationMs = opts.durationMs ?? 120_000;
|
|
176
|
+
const audioSource = opts.audioSource ?? "silence";
|
|
177
|
+
|
|
178
|
+
const peerLid = await this.#signaling.resolveLid(targetPnJid);
|
|
179
|
+
if (!peerLid) throw new Error(`Could not resolve LID for ${targetPnJid}`);
|
|
180
|
+
|
|
181
|
+
for (const jid of [targetPnJid, peerLid]) {
|
|
182
|
+
try { await this.#sock.presenceSubscribe(jid); } catch {}
|
|
183
|
+
}
|
|
184
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
185
|
+
|
|
186
|
+
const peerDeviceJids = await this.#signaling.discoverPeerDevices(peerLid);
|
|
187
|
+
const deviceList = peerDeviceJids.length ? peerDeviceJids : [toBareJid(peerLid)];
|
|
188
|
+
|
|
189
|
+
await this.#signaling.ensureSessionsForPeers(deviceList);
|
|
190
|
+
|
|
191
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
192
|
+
await this.#signaling.issueTcToken(peerLid);
|
|
193
|
+
const tcToken = await this.#signaling.ensureTcToken(peerLid, targetPnJid);
|
|
194
|
+
|
|
195
|
+
const callId = ("00" + randomBytes(16).toString("hex").slice(2)).toUpperCase();
|
|
196
|
+
|
|
197
|
+
const call = new ActiveCall(callId, this.#engine, durationMs);
|
|
198
|
+
call._audioSource = audioSource;
|
|
199
|
+
this.#activeCall = call;
|
|
200
|
+
|
|
201
|
+
this.#engine.startCall({
|
|
202
|
+
peerJid: peerLid,
|
|
203
|
+
peerPn: targetPnJid,
|
|
204
|
+
peerList: deviceList,
|
|
205
|
+
callId,
|
|
206
|
+
isVideo: false,
|
|
207
|
+
isLidCall: true,
|
|
208
|
+
isFromDialer: false,
|
|
209
|
+
extraData: tcToken,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return call;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
disconnect = () => {
|
|
216
|
+
this.#activeCall?._forceEnd("disconnect");
|
|
217
|
+
this.#activeCall = null;
|
|
218
|
+
this.#relay?.closeAll();
|
|
219
|
+
this.#engine?.destroy();
|
|
220
|
+
this.#engine = null;
|
|
221
|
+
this.#relay = null;
|
|
222
|
+
this.#signaling = null;
|
|
223
|
+
this.#sock = null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
#handleCallEvent = (eventType, eventData) => {
|
|
227
|
+
if (eventType === 16 && eventData) {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(eventData);
|
|
230
|
+
const info = parsed.call_info ?? parsed.callInfo ?? {};
|
|
231
|
+
const callState = Number(info.call_state ?? info.callState ?? 0);
|
|
232
|
+
this.#activeCall?._updateState(callState);
|
|
233
|
+
} catch {}
|
|
234
|
+
} else if (eventType === 156 && eventData) {
|
|
235
|
+
try {
|
|
236
|
+
const update = JSON.parse(eventData);
|
|
237
|
+
this.#relay?.updateRelayList(update);
|
|
238
|
+
} catch {}
|
|
239
|
+
} else if (eventType === 2) {
|
|
240
|
+
this.#activeCall?._forceEnd("remote_end");
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
#handleAudioCaptureInit = (config) => {
|
|
245
|
+
if (!this.#engine) return;
|
|
246
|
+
this.#captureSampleRate = config.sampleRate || 16000;
|
|
247
|
+
this.#captureChannels = config.channels || 1;
|
|
248
|
+
this.#captureFramesPerChunk = config.framesPerChunk || 320;
|
|
249
|
+
const chunkSamples = this.#captureFramesPerChunk * this.#captureChannels;
|
|
250
|
+
this.#captureChunkBytes = chunkSamples * Float32Array.BYTES_PER_ELEMENT;
|
|
251
|
+
this.#capturePtr = this.#engine.malloc(this.#captureChunkBytes);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
#handleAudioCaptureStart = () => {
|
|
255
|
+
if (!this.#engine || !this.#capturePtr) return;
|
|
256
|
+
const audioSource = this.#activeCall?._audioSource ?? "silence";
|
|
257
|
+
this.#feeder = new AudioFeeder(
|
|
258
|
+
this.#captureSampleRate,
|
|
259
|
+
this.#captureChannels,
|
|
260
|
+
this.#captureFramesPerChunk,
|
|
261
|
+
(chunk) => {
|
|
262
|
+
if (this.#engine && this.#capturePtr) this.#engine.sendAudioData(chunk, this.#capturePtr);
|
|
263
|
+
},
|
|
264
|
+
audioSource,
|
|
265
|
+
);
|
|
266
|
+
this.#feeder.start();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
#handleAudioCaptureStop = () => {
|
|
270
|
+
this.#feeder?.stop();
|
|
271
|
+
this.#feeder = null;
|
|
272
|
+
if (this.#engine && this.#capturePtr) {
|
|
273
|
+
try { this.#engine.free(this.#capturePtr); } catch {}
|
|
274
|
+
this.#capturePtr = 0;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class RelayRtcTransport {
|
|
2
|
+
constructor(config: any);
|
|
3
|
+
config: any;
|
|
4
|
+
updateRelayList: (update: any) => void;
|
|
5
|
+
send: (packet: any, ip: any, port: any) => any;
|
|
6
|
+
getStats: () => {
|
|
7
|
+
openConnections: number;
|
|
8
|
+
sentPackets: number;
|
|
9
|
+
receivedPackets: number;
|
|
10
|
+
sentBytes: number;
|
|
11
|
+
receivedBytes: number;
|
|
12
|
+
droppedPackets: number;
|
|
13
|
+
};
|
|
14
|
+
closeAll: () => Promise<void>;
|
|
15
|
+
#private;
|
|
16
|
+
}
|