Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions example.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,25 @@ client.on('call', async (call) => {
call.from,
`[${call.fromMe ? 'Outgoing' : 'Incoming'}] Phone call from ${call.from}, type ${call.isGroup ? 'group' : ''} ${call.isVideo ? 'video' : 'audio'} call. ${rejectCalls ? 'This call was automatically rejected by the script.' : ''}`,
);

// Instead of rejecting, a call can also be answered and an audio clip
// (e.g. a text-to-speech file you generated) can be played into it:
// await call.accept(); // answers with audio only, even for video calls
// await call.playAudio(MessageMedia.fromFilePath('./welcome.mp3'));
// await call.end();
});

// Placing an outgoing call and speaking once the other party answers:
// const call = await client.call('1234567890', { waitForAnswer: true });
// if (await call.isConnected()) {
// await call.playAudio(MessageMedia.fromFilePath('./message.mp3'));
// await call.end();
// }

// Checking for an ongoing call and hanging it up:
// const activeCall = await client.getActiveCall();
// if (activeCall) await activeCall.end();

client.on('disconnected', (reason) => {
console.log('Client was logged out', reason);
});
Expand Down
30 changes: 26 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,7 @@ declare namespace WAWebJS {
): Promise<Message>;

/** Send a reaction to a specific messageId */
sendReaction(
messageId: string,
reaction: string,
): Promise<void>;
sendReaction(messageId: string, reaction: string): Promise<void>;

/** Sends a channel admin invitation to a user, allowing them to become an admin of the channel */
sendChannelAdminInvite(
Expand Down Expand Up @@ -345,6 +342,19 @@ declare namespace WAWebJS {
/** Generates a WhatsApp call link (video call or voice call) */
createCallLink(startTime: Date, callType: string): Promise<string>;

/** Places an outgoing call to the provided chat or phone number */
call(
chatId: string,
options?: {
video?: boolean;
waitForAnswer?: boolean;
answerTimeout?: number;
},
): Promise<Call>;

/** Gets the call that is currently ongoing (ringing, being placed or connected), if any */
getActiveCall(): Promise<Call | null>;

/**
* Sends a response to the scheduled event message, indicating whether a user is going to attend the event or not
* @param response The response code to the event message. Valid values are: `0` for NONE response (removes a previous response) | `1` for GOING | `2` for NOT GOING | `3` for MAYBE going
Expand Down Expand Up @@ -2444,6 +2454,18 @@ declare namespace WAWebJS {

/** Reject the call */
reject: () => Promise<void>;

/** Accept the call (audio only by default, even for incoming video calls) */
accept: (options?: { video?: boolean }) => Promise<boolean>;

/** End an ongoing call */
end: () => Promise<boolean>;

/** Play an audio clip into the ongoing call so the other party can hear it */
playAudio: (media: MessageMedia | string) => Promise<number>;

/** Indicates whether the call is currently connected (the other party has answered) */
isConnected: () => Promise<boolean>;
}

/** Message type List */
Expand Down
113 changes: 96 additions & 17 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,19 @@ class Client extends EventEmitter {
// navigator.webdriver fix
browserArgs.push('--disable-blink-features=AutomationControlled');

// Required for the calling feature: allow the audio graph to run
// without a user gesture and auto-grant the microphone permission.
const callArgs = [
'--autoplay-policy=no-user-gesture-required',
'--use-fake-ui-for-media-stream',
];
for (const arg of callArgs) {
const flag = arg.split('=')[0];
if (!browserArgs.find((a) => a.startsWith(flag))) {
browserArgs.push(arg);
}
}

browser = await puppeteer.launch({
...puppeteerOpts,
args: browserArgs,
Expand Down Expand Up @@ -1131,26 +1144,52 @@ class Client extends EventEmitter {
WAWebCallCollection &&
typeof WAWebCallCollection.on === 'function'
) {
const mapKey = Object.keys(WAWebCallCollection).find(
(k) => WAWebCallCollection[k] instanceof Map,
);
const internalCallMap = WAWebCallCollection[mapKey];
const originalMapSet =
internalCallMap.set.bind(internalCallMap);

internalCallMap.set = function (key, value) {
const emitCall = (call) => {
if (
!call ||
!call.id ||
window._wwjsLastCallId === call.id
) {
return;
}
window._wwjsLastCallId = call.id;
window.onIncomingCall({
id: value.id,
peerJid: value.peerJid,
isVideo: value.isVideo,
isGroup: value.isGroup,
canHandleLocally: value.canHandleLocally,
outgoing: value.outgoing,
webClientShouldHandle: value.webClientShouldHandle,
participants: value.participants,
id: call.id,
peerJid: call.peerJid,
isVideo: call.isVideo,
isGroup: call.isGroup,
canHandleLocally: call.canHandleLocally,
outgoing: call.outgoing,
webClientShouldHandle: call.webClientShouldHandle,
participants: call.participants,
});
return originalMapSet(key, value);
};

// Newer WhatsApp Web surfaces calls through the collection's
// activeCall attribute instead of inserting them into the map.
// The changed call is passed as the handler argument; the
// lastActiveCall getter is not yet updated at this point.
if (!window._wwjsCallListener) {
window._wwjsCallListener = true;
WAWebCallCollection.on('change:activeCall', (call) => {
emitCall(call);
});
}

// Fallback for versions that still populate the internal map.
const mapKey = Object.keys(WAWebCallCollection).find(
(k) => WAWebCallCollection[k] instanceof Map,
);
if (mapKey) {
const internalCallMap = WAWebCallCollection[mapKey];
const originalMapSet =
internalCallMap.set.bind(internalCallMap);

internalCallMap.set = function (key, value) {
emitCall(value);
return originalMapSet(key, value);
};
}
}
Chat.on('remove', async (chat) => {
window.onRemoveChatEvent(
Expand Down Expand Up @@ -3279,6 +3318,46 @@ class Client extends EventEmitter {
);
}

/**
* Places an outgoing call to the provided chat or phone number
* @param {string} chatId The chat ID ("@c.us" is automatically appended) or phone number to call
* @param {object} [options] Call options
* @param {boolean} [options.video=false] Whether to place a video call instead of a voice call
* @param {boolean} [options.waitForAnswer=false] If true, waits until the callee answers (or the timeout elapses) before resolving
* @param {number} [options.answerTimeout=60000] Maximum time to wait for an answer, in milliseconds, when waitForAnswer is true
* @returns {Promise<Call>} The placed call
*/
async call(chatId, options = {}) {
const callData = await this.pupPage.evaluate(
(id, isVideo, waitForAnswer, answerTimeout) => {
return window.WWebJS.startCall(
id,
isVideo,
waitForAnswer,
answerTimeout,
);
},
chatId,
options.video ?? false,
options.waitForAnswer ?? false,
options.answerTimeout ?? 60000,
);

return new Call(this, callData);
}

/**
* Gets the call that is currently ongoing (ringing, being placed or connected), if any
* @returns {Promise<Call|null>} The current call, or null if there is no ongoing call
*/
async getActiveCall() {
const callData = await this.pupPage.evaluate(() => {
return window.WWebJS.getActiveCall();
});

return callData ? new Call(this, callData) : null;
}

/**
* Sends a response to the scheduled event message, indicating whether a user is going to attend the event or not
* @param {number} response The response code to the scheduled event message. Valid values are: `0` for NONE response (removes a previous response) | `1` for GOING | `2` for NOT GOING | `3` for MAYBE going
Expand Down
48 changes: 48 additions & 0 deletions src/structures/Call.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,54 @@ class Call extends Base {
this.id,
);
}

/**
* Accept the call
* @param {object} [options] Accept options
* @param {boolean} [options.video=false] Whether to answer with video (requires a camera). Defaults to audio only, even for incoming video calls
* @returns {Promise<boolean>}
*/
async accept(options = {}) {
return this.client.pupPage.evaluate(
(id, isVideo) => {
return window.WWebJS.acceptCall(id, isVideo);
},
this.id,
options.video ?? false,
);
}

/**
* End an ongoing call
* @returns {Promise<boolean>}
*/
async end() {
return this.client.pupPage.evaluate((id) => {
return window.WWebJS.endCall(id);
}, this.id);
}

/**
* Play an audio clip into the ongoing call so the other party can hear it
* @param {MessageMedia|string} media A MessageMedia instance or a base64 encoded audio string
* @returns {Promise<number>} The duration of the played audio in seconds
*/
async playAudio(media) {
const data = typeof media === 'string' ? media : media.data;
return this.client.pupPage.evaluate((base64) => {
return window.WWebJS.playCallAudio(base64);
}, data);
}

/**
* Indicates whether the call is currently connected (the other party has answered)
* @returns {Promise<boolean>}
*/
async isConnected() {
return this.client.pupPage.evaluate((id) => {
return window.WWebJS.isCallConnected(id);
}, this.id);
}
}

module.exports = Call;
Loading