[core] Reject Calls - WEBJS, NOWEB, GOWS

fix #608
fix #1258
This commit is contained in:
devlikepro committed 2025-12-05 00:13:12 -08:00
1 parent a8b520012f
commit c5c99136fa
11 files changed
+493 -8

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import {
Body,
Controller,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { RejectCallRequest } from '../structures/calls.dto';
@ApiSecurity('api_key')
@Controller('api/:session/calls')
@ApiTags('📞 Calls')
export class CallsController {
constructor(private manager: SessionManager) {}
@Post('reject')
@SessionApiParam
@ApiOperation({ summary: 'Reject incoming call' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
rejectCall(
@WorkingSessionParam session: WhatsappSession,
@Body() request: RejectCallRequest,
) {
return session.rejectCall(request.from, request.id);
}
}
+1
View File
@@ -79,6 +79,7 @@ export class SwaggerConfiguratorCore {
.addTag('🆔 Profile', 'Your profile information')
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('📤 Chatting', 'Chatting methods')
.addTag('📞 Calls', 'Call handling methods')
.addTag('📢 Channels', 'Channels (newsletters) methods')
.addTag('🟢 Status', 'Status (aka stories) methods')
.addTag('💬 Chats', `Chats methods`)
+4
View File
@@ -664,6 +664,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public rejectCall(from: string, id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
/**
* Chats methods
*/
+2
View File
@@ -40,6 +40,7 @@ import { join } from 'path';
import { Logger } from 'pino';
import { AuthController } from '../api/auth.controller';
import { CallsController } from '../api/calls.controller';
import { ChatsController } from '../api/chats.controller';
import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
@@ -143,6 +144,7 @@ export const CONTROLLERS = [
ProfileController,
ChattingController,
ChatsController,
CallsController,
ChannelsController,
StatusController,
LabelsController,
+129
View File
@@ -10105,6 +10105,122 @@ export namespace messages {
return GetLidsRequest.deserialize(bytes);
}
}
export class RejectCallRequest extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
session?: Session;
from?: string;
id?: string;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
if (!Array.isArray(data) && typeof data == "object") {
if ("session" in data && data.session != undefined) {
this.session = data.session;
}
if ("from" in data && data.from != undefined) {
this.from = data.from;
}
if ("id" in data && data.id != undefined) {
this.id = data.id;
}
}
}
get session() {
return pb_1.Message.getWrapperField(this, Session, 1) as Session;
}
set session(value: Session) {
pb_1.Message.setWrapperField(this, 1, value);
}
get has_session() {
return pb_1.Message.getField(this, 1) != null;
}
get from() {
return pb_1.Message.getFieldWithDefault(this, 2, "") as string;
}
set from(value: string) {
pb_1.Message.setField(this, 2, value);
}
get id() {
return pb_1.Message.getFieldWithDefault(this, 3, "") as string;
}
set id(value: string) {
pb_1.Message.setField(this, 3, value);
}
static fromObject(data: {
session?: ReturnType<typeof Session.prototype.toObject>;
from?: string;
id?: string;
}): RejectCallRequest {
const message = new RejectCallRequest({});
if (data.session != null) {
message.session = Session.fromObject(data.session);
}
if (data.from != null) {
message.from = data.from;
}
if (data.id != null) {
message.id = data.id;
}
return message;
}
toObject() {
const data: {
session?: ReturnType<typeof Session.prototype.toObject>;
from?: string;
id?: string;
} = {};
if (this.session != null) {
data.session = this.session.toObject();
}
if (this.from != null) {
data.from = this.from;
}
if (this.id != null) {
data.id = this.id;
}
return data;
}
serialize(): Uint8Array;
serialize(w: pb_1.BinaryWriter): void;
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
const writer = w || new pb_1.BinaryWriter();
if (this.has_session)
writer.writeMessage(1, this.session, () => this.session.serialize(writer));
if (this.from.length)
writer.writeString(2, this.from);
if (this.id.length)
writer.writeString(3, this.id);
if (!w)
return writer.getResultBuffer();
}
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): RejectCallRequest {
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new RejectCallRequest();
while (reader.nextField()) {
if (reader.isEndGroup())
break;
switch (reader.getFieldNumber()) {
case 1:
reader.readMessage(message.session, () => message.session = Session.deserialize(reader));
break;
case 2:
message.from = reader.readString();
break;
case 3:
message.id = reader.readString();
break;
default: reader.skipField();
}
}
return message;
}
serializeBinary(): Uint8Array {
return this.serialize();
}
static deserializeBinary(bytes: Uint8Array): RejectCallRequest {
return RejectCallRequest.deserialize(bytes);
}
}
export class PollMessage extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
@@ -10933,6 +11049,15 @@ export namespace messages {
responseSerialize: (message: DownloadMediaResponse) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => DownloadMediaResponse.deserialize(new Uint8Array(bytes))
},
RejectCall: {
path: "/messages.MessageService/RejectCall",
requestStream: false,
responseStream: false,
requestSerialize: (message: RejectCallRequest) => Buffer.from(message.serialize()),
requestDeserialize: (bytes: Buffer) => RejectCallRequest.deserialize(new Uint8Array(bytes)),
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
},
GetMessageById: {
path: "/messages.MessageService/GetMessageById",
requestStream: false,
@@ -11022,6 +11147,7 @@ export namespace messages {
abstract GetContactById(call: grpc_1.ServerUnaryCall<EntityByIdRequest, Json>, callback: grpc_1.sendUnaryData<Json>): void;
abstract CancelEventMessage(call: grpc_1.ServerUnaryCall<CancelEventMessageRequest, MessageResponse>, callback: grpc_1.sendUnaryData<MessageResponse>): void;
abstract DownloadMedia(call: grpc_1.ServerUnaryCall<DownloadMediaRequest, DownloadMediaResponse>, callback: grpc_1.sendUnaryData<DownloadMediaResponse>): void;
abstract RejectCall(call: grpc_1.ServerUnaryCall<RejectCallRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract GetMessageById(call: grpc_1.ServerUnaryCall<EntityByIdRequest, Json>, callback: grpc_1.sendUnaryData<Json>): void;
abstract GetMessages(call: grpc_1.ServerUnaryCall<GetMessagesRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
abstract GetChats(call: grpc_1.ServerUnaryCall<GetChatsRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
@@ -11210,6 +11336,9 @@ export namespace messages {
DownloadMedia: GrpcUnaryServiceInterface<DownloadMediaRequest, DownloadMediaResponse> = (message: DownloadMediaRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<DownloadMediaResponse>, options?: grpc_1.CallOptions | grpc_1.requestCallback<DownloadMediaResponse>, callback?: grpc_1.requestCallback<DownloadMediaResponse>): grpc_1.ClientUnaryCall => {
return super.DownloadMedia(message, metadata, options, callback);
};
RejectCall: GrpcUnaryServiceInterface<RejectCallRequest, Empty> = (message: RejectCallRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
return super.RejectCall(message, metadata, options, callback);
};
GetMessageById: GrpcUnaryServiceInterface<EntityByIdRequest, Json> = (message: EntityByIdRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Json>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Json>, callback?: grpc_1.requestCallback<Json>): grpc_1.ClientUnaryCall => {
return super.GetMessageById(message, metadata, options, callback);
};
@@ -543,6 +543,17 @@ function deserialize_messages_ProfileStatusRequest(buffer_arg) {
return gows_pb.ProfileStatusRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_RejectCallRequest(arg) {
if (!(arg instanceof gows_pb.RejectCallRequest)) {
throw new Error('Expected argument of type messages.RejectCallRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_RejectCallRequest(buffer_arg) {
return gows_pb.RejectCallRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_RevokeMessageRequest(arg) {
if (!(arg instanceof gows_pb.RevokeMessageRequest)) {
throw new Error('Expected argument of type messages.RevokeMessageRequest');
@@ -1391,6 +1402,20 @@ downloadMedia: {
responseDeserialize: deserialize_messages_DownloadMediaResponse,
},
//
// Calls
//
rejectCall: {
path: '/messages.MessageService/RejectCall',
requestStream: false,
responseStream: false,
requestType: gows_pb.RejectCallRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_RejectCallRequest,
requestDeserialize: deserialize_messages_RejectCallRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
//
// Storage
//
getMessageById: {
+233
View File
@@ -94,6 +94,7 @@ goog.exportSymbol('proto.messages.ProfilePictureRequest', null, global);
goog.exportSymbol('proto.messages.ProfilePictureResponse', null, global);
goog.exportSymbol('proto.messages.ProfileStatusRequest', null, global);
goog.exportSymbol('proto.messages.ReceiptType', null, global);
goog.exportSymbol('proto.messages.RejectCallRequest', null, global);
goog.exportSymbol('proto.messages.RevokeMessageRequest', null, global);
goog.exportSymbol('proto.messages.Row', null, global);
goog.exportSymbol('proto.messages.SearchNewslettersByTextRequest', null, global);
@@ -1966,6 +1967,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.messages.GetLidsRequest.displayName = 'proto.messages.GetLidsRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.messages.RejectCallRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.messages.RejectCallRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.messages.RejectCallRequest.displayName = 'proto.messages.RejectCallRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -20854,6 +20876,217 @@ proto.messages.GetLidsRequest.prototype.hasSession = function() {
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.messages.RejectCallRequest.prototype.toObject = function(opt_includeInstance) {
return proto.messages.RejectCallRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.messages.RejectCallRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.RejectCallRequest.toObject = function(includeInstance, msg) {
var f, obj = {
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
from: jspb.Message.getFieldWithDefault(msg, 2, ""),
id: jspb.Message.getFieldWithDefault(msg, 3, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.messages.RejectCallRequest}
*/
proto.messages.RejectCallRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.messages.RejectCallRequest;
return proto.messages.RejectCallRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.messages.RejectCallRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.messages.RejectCallRequest}
*/
proto.messages.RejectCallRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = new proto.messages.Session;
reader.readMessage(value,proto.messages.Session.deserializeBinaryFromReader);
msg.setSession(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setFrom(value);
break;
case 3:
var value = /** @type {string} */ (reader.readString());
msg.setId(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.messages.RejectCallRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.messages.RejectCallRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.messages.RejectCallRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.RejectCallRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getSession();
if (f != null) {
writer.writeMessage(
1,
f,
proto.messages.Session.serializeBinaryToWriter
);
}
f = message.getFrom();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getId();
if (f.length > 0) {
writer.writeString(
3,
f
);
}
};
/**
* optional Session session = 1;
* @return {?proto.messages.Session}
*/
proto.messages.RejectCallRequest.prototype.getSession = function() {
return /** @type{?proto.messages.Session} */ (
jspb.Message.getWrapperField(this, proto.messages.Session, 1));
};
/**
* @param {?proto.messages.Session|undefined} value
* @return {!proto.messages.RejectCallRequest} returns this
*/
proto.messages.RejectCallRequest.prototype.setSession = function(value) {
return jspb.Message.setWrapperField(this, 1, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.messages.RejectCallRequest} returns this
*/
proto.messages.RejectCallRequest.prototype.clearSession = function() {
return this.setSession(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.messages.RejectCallRequest.prototype.hasSession = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional string from = 2;
* @return {string}
*/
proto.messages.RejectCallRequest.prototype.getFrom = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.messages.RejectCallRequest} returns this
*/
proto.messages.RejectCallRequest.prototype.setFrom = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
/**
* optional string id = 3;
* @return {string}
*/
proto.messages.RejectCallRequest.prototype.getId = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
};
/**
* @param {string} value
* @return {!proto.messages.RejectCallRequest} returns this
*/
proto.messages.RejectCallRequest.prototype.setId = function(value) {
return jspb.Message.setProto3StringField(this, 3, value);
};
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
@@ -862,6 +862,15 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return data.id;
}
async rejectCall(from: string, id: string): Promise<void> {
const request = new messages.RejectCallRequest({
session: this.session,
from: toJID(this.ensureSuffix(from)),
id: id,
});
await promisify(this.client.RejectCall)(request);
}
@Activity()
async sendText(request: MessageTextRequest) {
const jid = toJID(this.ensureSuffix(request.chatId));
+31 -7
View File
@@ -186,9 +186,11 @@ import {
mergeMap,
Observable,
partition,
groupBy,
share,
tap,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { debounceTime, map } from 'rxjs/operators';
import { INowebStore } from './store/INowebStore';
import { NowebPersistentStore } from './store/NowebPersistentStore';
@@ -850,6 +852,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.generateMessageID();
}
async rejectCall(from: string, id: string): Promise<void> {
const jid = toJID(this.ensureSuffix(from));
await this.sock.rejectCall(id, jid);
}
@Activity()
async sendText(request: MessageTextRequest) {
const chatId = toJID(this.ensureSuffix(request.chatId));
@@ -2123,6 +2130,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
),
share(),
);
const acceptedCallIds = new Set<string>();
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
call$.pipe(
filter((call: WACallEvent) => call.status === 'offer'),
@@ -2132,18 +2141,33 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.events2.get(WAHAEvents.CALL_ACCEPTED).switch(
call$.pipe(
filter((call: WACallEvent) => call.status === 'accept'),
tap((call: WACallEvent) => acceptedCallIds.add(call.id)),
map(this.toCallData.bind(this)),
),
);
this.events2.get(WAHAEvents.CALL_REJECTED).switch(
calls$.pipe(
// Filter out if there's any "accept" events.
// Meaning it's been accepted on one device, but rejected on another
exclude((calls) => calls.some((call) => call.status === 'accept')),
mergeAll(),
filter((call: WACallEvent) => call.status === 'reject'),
call$.pipe(
filter(
(call: WACallEvent) =>
call.status === 'reject' || call.status === 'terminate',
),
// Skip rejections when the call was accepted earlier (local or other device)
exclude((call: WACallEvent) => {
const shouldSkip = acceptedCallIds.has(call.id);
if (call.status === 'terminate') {
acceptedCallIds.delete(call.id);
}
return shouldSkip;
}),
// We get two "reject" events, one with null isGroup property, ignore it
exclude((call: WACallEvent) => call.isGroup == null),
groupBy((call: WACallEvent) => call.id || 'unknown'),
mergeMap((group$) =>
group$.pipe(
debounceTime(1_000),
tap((call: WACallEvent) => acceptedCallIds.delete(call.id)),
),
),
map(this.toCallData.bind(this)),
),
);
+12 -1
View File
@@ -150,7 +150,10 @@ import {
Reaction,
WAState,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import {
Message as MessageInstance,
Call as CallInstance,
} from 'whatsapp-web.js/src/structures';
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
import {
@@ -651,6 +654,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Other methods
*/
async rejectCall(from: string, id: string): Promise<void> {
const peerJid = normalizeJid(this.ensureSuffix(from));
const call = new CallInstance(this.whatsapp, null);
call.id = id;
call.from = peerJid;
await call.reject();
}
@Activity()
sendText(request: MessageTextRequest) {
const options = this.getMessageOptions(request);
+13
View File
@@ -3,6 +3,7 @@
*/
import { ApiProperty } from '@nestjs/swagger';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { IsNotEmpty, IsString } from 'class-validator';
function CallIdProperty() {
return ApiProperty({
@@ -11,6 +12,18 @@ function CallIdProperty() {
});
}
export class RejectCallRequest {
@ChatIdProperty()
@IsString()
@IsNotEmpty()
from: string;
@CallIdProperty()
@IsString()
@IsNotEmpty()
id: string;
}
export class CallData {
@CallIdProperty()
id: string;