feat(groups): membership approval - settings, pending requests API and group.v2.participants.join-request event

Closes #2200

Co-authored-by: icecubex300 <83597812+icecubex300@users.noreply.github.com>
This commit is contained in:
devlikeproandicecubex300 committed 2026-08-31 10:44:22 +07:00
1 parent b5e223a333
commit cc12380e48
21 files changed
+1553 -1

No files matched your search

+85
View File
@@ -32,6 +32,8 @@ import {
CreateGroupRequest,
DescriptionRequest,
GroupField,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupsListFields,
GroupsPaginationParams,
@@ -39,6 +41,7 @@ import {
JoinGroupResponse,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
SubjectRequest,
} from '../structures/groups.dto';
@@ -343,6 +346,88 @@ export class GroupsController {
return session.getMemberAddMode(id);
}
@Put(':id/settings/security/membership-approval')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Update settings - approve new members',
description:
'Enables or disables admin approval for users requesting to join the group.',
})
setMembershipApprovalMode(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: SettingsMembershipApproval,
): Promise<boolean> {
return session.setMembershipApprovalMode(
id,
request.newMembersApprovalRequired,
);
}
@Get(':id/settings/security/membership-approval')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary: 'Get settings - approve new members',
description:
'Returns whether admin approval is required for users requesting to join the group.',
})
getMembershipApprovalMode(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<SettingsMembershipApproval> {
return session.getMembershipApprovalMode(id);
}
@Get(':id/participants/join-requests')
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
@ApiOperation({
summary: 'Get pending requests to join the group',
})
getGroupJoinRequests(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<GroupJoinRequest[]> {
return session.getGroupJoinRequests(id);
}
@Post(':id/participants/join-requests/approve')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Approve pending requests to join the group',
})
approveGroupJoinRequests(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
return session.approveGroupJoinRequests(id, request);
}
@Post(':id/participants/join-requests/reject')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
@ApiOperation({
summary: 'Reject pending requests to join the group',
})
rejectGroupJoinRequests(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
return session.rejectGroupJoinRequests(id, request);
}
@Get(':id/invite-code')
@SessionApiParam
@GroupIdApiParam
+103
View File
@@ -8,6 +8,7 @@ import {
GroupDescriptionInput,
GroupIdInput,
GroupJoinInput,
GroupMembershipApprovalInput,
GroupParticipantsInput,
GroupPictureInput,
GroupsListInput,
@@ -363,6 +364,108 @@ export class GroupTools extends McpController {
});
}
@Tool('groups-get-membership-approval', {
title: 'Get membership approval setting',
description:
'Get whether admin approval is required for users requesting to join the group',
inputSchema: GroupIdInput,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
})
async getMembershipApproval({ session, id }: z.infer<typeof GroupIdInput>) {
return this.textRequest({
method: 'GET',
url: `/api/${session}/groups/${id}/settings/security/membership-approval`,
});
}
@Tool('groups-set-membership-approval', {
title: 'Set membership approval setting',
description:
'Enable or disable admin approval for users requesting to join the group',
inputSchema: GroupMembershipApprovalInput,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
})
async setMembershipApproval({
session,
id,
...body
}: z.infer<typeof GroupMembershipApprovalInput>) {
return this.textRequest({
method: 'PUT',
url: `/api/${session}/groups/${id}/settings/security/membership-approval`,
data: body,
});
}
@Tool('groups-get-join-requests', {
title: 'Get pending requests to join the group',
description: 'Get pending requests to join the group',
inputSchema: GroupIdInput,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
})
async getJoinRequests({ session, id }: z.infer<typeof GroupIdInput>) {
return this.textRequest({
method: 'GET',
url: `/api/${session}/groups/${id}/participants/join-requests`,
});
}
@Tool('groups-approve-join-requests', {
title: 'Approve pending requests to join the group',
description: 'Approve pending requests to join the group',
inputSchema: GroupParticipantsInput,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
})
async approveJoinRequests({
session,
id,
...body
}: z.infer<typeof GroupParticipantsInput>) {
return this.textRequest({
method: 'POST',
url: `/api/${session}/groups/${id}/participants/join-requests/approve`,
data: body,
});
}
@Tool('groups-reject-join-requests', {
title: 'Reject pending requests to join the group',
description: 'Reject pending requests to join the group',
inputSchema: GroupParticipantsInput,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
})
async rejectJoinRequests({
session,
id,
...body
}: z.infer<typeof GroupParticipantsInput>) {
return this.textRequest({
method: 'POST',
url: `/api/${session}/groups/${id}/participants/join-requests/reject`,
data: body,
});
}
@Tool('groups-get-invite-code', {
title: 'Get group invite code',
description: 'Get the invite code for a group',
+8
View File
@@ -7,6 +7,7 @@ import {
GroupsPaginationParams,
JoinGroupRequest,
ParticipantsRequest,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
SubjectRequest,
} from '@waha/structures/groups.dto';
@@ -64,3 +65,10 @@ export const GroupParticipantsInput = DtoToZod(ParticipantsRequest).extend({
session: SessionField,
id: GroupIdField,
});
export const GroupMembershipApprovalInput = DtoToZod(
SettingsMembershipApproval,
).extend({
session: SessionField,
id: GroupIdField,
});
+34
View File
@@ -106,10 +106,13 @@ import { EventMessageRequest } from '../../structures/events.dto';
import {
CreateGroupRequest,
GroupField,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupsListFields,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
} from '../../structures/groups.dto';
import { WAHAChatPresences } from '../../structures/presence.dto';
@@ -1069,6 +1072,37 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getMembershipApprovalMode(
id: string,
): Promise<SettingsMembershipApproval> {
throw new NotImplementedByEngineError();
}
public setMembershipApprovalMode(
id: string,
value: boolean,
): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public getGroupJoinRequests(id: string): Promise<GroupJoinRequest[]> {
throw new NotImplementedByEngineError();
}
public approveGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
throw new NotImplementedByEngineError();
}
public rejectGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
throw new NotImplementedByEngineError();
}
public deleteGroup(id) {
throw new NotImplementedByEngineError();
}
+76
View File
@@ -1,20 +1,28 @@
import {
GOWSGroupJoinRequest,
GOWSGroupParticipant,
GOWSGroupParticipantRequest,
GroupInfoEvent,
GroupInfoFull,
GroupJoinRequestEvent,
JoinedGroupEvent,
} from '@waha/core/engines/gows/types.group';
import { isPnUser, toCusFormat, toJID } from '@waha/core/utils/jids';
import {
GroupId,
GroupInfo,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupParticipantRole,
NormalizeJoinRequestMethod,
} from '@waha/structures/groups.dto';
import {
GroupParticipantsJoinRequestAction,
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
@@ -171,6 +179,74 @@ function ToGroupInfoPartial(group: GroupInfoEvent): GroupInfo | null {
return null;
}
export function ToGroupJoinRequest(
request: GOWSGroupParticipantRequest,
): GroupJoinRequest {
const requestedAt = Date.parse(request.RequestedAt);
return {
requesterId: toCusFormat(request.JID),
addedById: null,
parentGroupId: null,
requestMethod: null,
timestamp: requestedAt ? Math.floor(requestedAt / 1000) : 0,
};
}
export function ToGroupJoinRequestResult(
participant: GOWSGroupParticipant,
): GroupJoinRequestResult {
return {
requesterId: toCusFormat(participant.JID),
success: !participant.Error,
error: participant.Error || undefined,
};
}
function ToGroupParticipantsJoinRequestAction(
event: GroupJoinRequestEvent,
request: GOWSGroupJoinRequest,
): GroupParticipantsJoinRequestAction | null {
if (event.Action === 'created') {
return GroupParticipantsJoinRequestAction.CREATED;
}
if (event.Action !== 'revoked') {
return null;
}
// The wire has one 'revoked' change for both cases - tell them apart by who sent it:
// the requester cancelled their own request, anyone else (an admin) rejected it
const self =
event.Sender === request.JID ||
(!!event.SenderPN && event.SenderPN === request.PhoneNumber);
if (self) {
return GroupParticipantsJoinRequestAction.REVOKED;
}
return GroupParticipantsJoinRequestAction.REJECTED;
}
export function ToGroupV2ParticipantsJoinRequestEvents(
event: GroupJoinRequestEvent,
): GroupV2ParticipantsJoinRequestEvent[] {
const events: GroupV2ParticipantsJoinRequestEvent[] = [];
for (const request of event.Requests || []) {
const action = ToGroupParticipantsJoinRequestAction(event, request);
if (!action) {
continue;
}
events.push({
group: {
id: event.JID,
},
action: action,
requesterId: toCusFormat(request.JID),
requesterPn: toCusFormat(request.PhoneNumber) || null,
requestMethod: NormalizeJoinRequestMethod(request.RequestMethod),
timestamp: Date.now(),
_data: event,
});
}
return events;
}
export function ToGroupParticipants(
participants: GOWSGroupParticipant[],
): GroupParticipant[] {
+182
View File
@@ -19,6 +19,10 @@ export namespace messages {
PROMOTE = 2,
DEMOTE = 3
}
export enum ParticipantRequestAction {
APPROVE = 0,
REJECT = 1
}
export enum MediaType {
IMAGE = 0,
AUDIO = 1,
@@ -2978,6 +2982,145 @@ export namespace messages {
return UpdateParticipantsRequest.deserialize(bytes);
}
}
export class UpdateRequestParticipantsRequest extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
session?: Session;
jid?: string;
participants?: string[];
action?: ParticipantRequestAction;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [3], this.#one_of_decls);
if (!Array.isArray(data) && typeof data == "object") {
if ("session" in data && data.session != undefined) {
this.session = data.session;
}
if ("jid" in data && data.jid != undefined) {
this.jid = data.jid;
}
if ("participants" in data && data.participants != undefined) {
this.participants = data.participants;
}
if ("action" in data && data.action != undefined) {
this.action = data.action;
}
}
}
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 jid() {
return pb_1.Message.getFieldWithDefault(this, 2, "") as string;
}
set jid(value: string) {
pb_1.Message.setField(this, 2, value);
}
get participants() {
return pb_1.Message.getFieldWithDefault(this, 3, []) as string[];
}
set participants(value: string[]) {
pb_1.Message.setField(this, 3, value);
}
get action() {
return pb_1.Message.getFieldWithDefault(this, 4, ParticipantRequestAction.APPROVE) as ParticipantRequestAction;
}
set action(value: ParticipantRequestAction) {
pb_1.Message.setField(this, 4, value);
}
static fromObject(data: {
session?: ReturnType<typeof Session.prototype.toObject>;
jid?: string;
participants?: string[];
action?: ParticipantRequestAction;
}): UpdateRequestParticipantsRequest {
const message = new UpdateRequestParticipantsRequest({});
if (data.session != null) {
message.session = Session.fromObject(data.session);
}
if (data.jid != null) {
message.jid = data.jid;
}
if (data.participants != null) {
message.participants = data.participants;
}
if (data.action != null) {
message.action = data.action;
}
return message;
}
toObject() {
const data: {
session?: ReturnType<typeof Session.prototype.toObject>;
jid?: string;
participants?: string[];
action?: ParticipantRequestAction;
} = {};
if (this.session != null) {
data.session = this.session.toObject();
}
if (this.jid != null) {
data.jid = this.jid;
}
if (this.participants != null) {
data.participants = this.participants;
}
if (this.action != null) {
data.action = this.action;
}
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.jid.length)
writer.writeString(2, this.jid);
if (this.participants.length)
writer.writeRepeatedString(3, this.participants);
if (this.action != ParticipantRequestAction.APPROVE)
writer.writeEnum(4, this.action);
if (!w)
return writer.getResultBuffer();
}
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): UpdateRequestParticipantsRequest {
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new UpdateRequestParticipantsRequest();
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.jid = reader.readString();
break;
case 3:
pb_1.Message.addToRepeatedField(message, 3, reader.readString());
break;
case 4:
message.action = reader.readEnum();
break;
default: reader.skipField();
}
}
return message;
}
serializeBinary(): Uint8Array {
return this.serialize();
}
static deserializeBinary(bytes: Uint8Array): UpdateRequestParticipantsRequest {
return UpdateRequestParticipantsRequest.deserialize(bytes);
}
}
export class GroupCodeRequest extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
@@ -11475,6 +11618,33 @@ export namespace messages {
responseSerialize: (message: JsonList) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => JsonList.deserialize(new Uint8Array(bytes))
},
SetGroupJoinApprovalMode: {
path: "/messages.MessageService/SetGroupJoinApprovalMode",
requestStream: false,
responseStream: false,
requestSerialize: (message: JidBoolRequest) => Buffer.from(message.serialize()),
requestDeserialize: (bytes: Buffer) => JidBoolRequest.deserialize(new Uint8Array(bytes)),
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
},
GetGroupRequestParticipants: {
path: "/messages.MessageService/GetGroupRequestParticipants",
requestStream: false,
responseStream: false,
requestSerialize: (message: JidRequest) => Buffer.from(message.serialize()),
requestDeserialize: (bytes: Buffer) => JidRequest.deserialize(new Uint8Array(bytes)),
responseSerialize: (message: JsonList) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => JsonList.deserialize(new Uint8Array(bytes))
},
UpdateGroupRequestParticipants: {
path: "/messages.MessageService/UpdateGroupRequestParticipants",
requestStream: false,
responseStream: false,
requestSerialize: (message: UpdateRequestParticipantsRequest) => Buffer.from(message.serialize()),
requestDeserialize: (bytes: Buffer) => UpdateRequestParticipantsRequest.deserialize(new Uint8Array(bytes)),
responseSerialize: (message: JsonList) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => JsonList.deserialize(new Uint8Array(bytes))
},
GetProfilePicture: {
path: "/messages.MessageService/GetProfilePicture",
requestStream: false,
@@ -11858,6 +12028,9 @@ export namespace messages {
abstract SetGroupAnnounce(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract SetGroupMemberAddMode(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract UpdateGroupParticipants(call: grpc_1.ServerUnaryCall<UpdateParticipantsRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
abstract SetGroupJoinApprovalMode(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract GetGroupRequestParticipants(call: grpc_1.ServerUnaryCall<JidRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
abstract UpdateGroupRequestParticipants(call: grpc_1.ServerUnaryCall<UpdateRequestParticipantsRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
abstract GetProfilePicture(call: grpc_1.ServerUnaryCall<ProfilePictureRequest, ProfilePictureResponse>, callback: grpc_1.sendUnaryData<ProfilePictureResponse>): void;
abstract SendPresence(call: grpc_1.ServerUnaryCall<PresenceRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract SendChatPresence(call: grpc_1.ServerUnaryCall<ChatPresenceRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
@@ -11992,6 +12165,15 @@ export namespace messages {
UpdateGroupParticipants: GrpcUnaryServiceInterface<UpdateParticipantsRequest, JsonList> = (message: UpdateParticipantsRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, options?: grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, callback?: grpc_1.requestCallback<JsonList>): grpc_1.ClientUnaryCall => {
return super.UpdateGroupParticipants(message, metadata, options, callback);
};
SetGroupJoinApprovalMode: GrpcUnaryServiceInterface<JidBoolRequest, Empty> = (message: JidBoolRequest, 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.SetGroupJoinApprovalMode(message, metadata, options, callback);
};
GetGroupRequestParticipants: GrpcUnaryServiceInterface<JidRequest, JsonList> = (message: JidRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, options?: grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, callback?: grpc_1.requestCallback<JsonList>): grpc_1.ClientUnaryCall => {
return super.GetGroupRequestParticipants(message, metadata, options, callback);
};
UpdateGroupRequestParticipants: GrpcUnaryServiceInterface<UpdateRequestParticipantsRequest, JsonList> = (message: UpdateRequestParticipantsRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, options?: grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, callback?: grpc_1.requestCallback<JsonList>): grpc_1.ClientUnaryCall => {
return super.UpdateGroupRequestParticipants(message, metadata, options, callback);
};
GetProfilePicture: GrpcUnaryServiceInterface<ProfilePictureRequest, ProfilePictureResponse> = (message: ProfilePictureRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<ProfilePictureResponse>, options?: grpc_1.CallOptions | grpc_1.requestCallback<ProfilePictureResponse>, callback?: grpc_1.requestCallback<ProfilePictureResponse>): grpc_1.ClientUnaryCall => {
return super.GetProfilePicture(message, metadata, options, callback);
};
@@ -697,6 +697,17 @@ function deserialize_messages_UpdateParticipantsRequest(buffer_arg) {
return gows_pb.UpdateParticipantsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpdateRequestParticipantsRequest(arg) {
if (!(arg instanceof gows_pb.UpdateRequestParticipantsRequest)) {
throw new Error('Expected argument of type messages.UpdateRequestParticipantsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_UpdateRequestParticipantsRequest(buffer_arg) {
return gows_pb.UpdateRequestParticipantsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpsertLabelRequest(arg) {
if (!(arg instanceof gows_pb.UpsertLabelRequest)) {
throw new Error('Expected argument of type messages.UpsertLabelRequest');
@@ -1073,6 +1084,39 @@ updateGroupParticipants: {
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
setGroupJoinApprovalMode: {
path: '/messages.MessageService/SetGroupJoinApprovalMode',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidBoolRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidBoolRequest,
requestDeserialize: deserialize_messages_JidBoolRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getGroupRequestParticipants: {
path: '/messages.MessageService/GetGroupRequestParticipants',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_JidRequest,
requestDeserialize: deserialize_messages_JidRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
updateGroupRequestParticipants: {
path: '/messages.MessageService/UpdateGroupRequestParticipants',
requestStream: false,
responseStream: false,
requestType: gows_pb.UpdateRequestParticipantsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_UpdateRequestParticipantsRequest,
requestDeserialize: deserialize_messages_UpdateRequestParticipantsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
//
// Actions
//
+298
View File
@@ -84,6 +84,7 @@ goog.exportSymbol('proto.messages.Pagination', null, global);
goog.exportSymbol('proto.messages.PairCodeRequest', null, global);
goog.exportSymbol('proto.messages.PairCodeResponse', null, global);
goog.exportSymbol('proto.messages.ParticipantAction', null, global);
goog.exportSymbol('proto.messages.ParticipantRequestAction', null, global);
goog.exportSymbol('proto.messages.PasskeyResponseRequest', null, global);
goog.exportSymbol('proto.messages.PhoneInfo', null, global);
goog.exportSymbol('proto.messages.PollMessage', null, global);
@@ -120,6 +121,7 @@ goog.exportSymbol('proto.messages.StreamEventsRequest', null, global);
goog.exportSymbol('proto.messages.SubscribePresenceRequest', null, global);
goog.exportSymbol('proto.messages.UpdateContactRequest', null, global);
goog.exportSymbol('proto.messages.UpdateParticipantsRequest', null, global);
goog.exportSymbol('proto.messages.UpdateRequestParticipantsRequest', null, global);
goog.exportSymbol('proto.messages.UpsertLabelRequest', null, global);
goog.exportSymbol('proto.messages.VideoInfo', null, global);
goog.exportSymbol('proto.messages.vCardContact', null, global);
@@ -753,6 +755,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.messages.UpdateParticipantsRequest.displayName = 'proto.messages.UpdateParticipantsRequest';
}
/**
* 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.UpdateRequestParticipantsRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, proto.messages.UpdateRequestParticipantsRequest.repeatedFields_, null);
};
goog.inherits(proto.messages.UpdateRequestParticipantsRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.messages.UpdateRequestParticipantsRequest.displayName = 'proto.messages.UpdateRequestParticipantsRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -7652,6 +7675,273 @@ proto.messages.UpdateParticipantsRequest.prototype.setAction = function(value) {
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.messages.UpdateRequestParticipantsRequest.repeatedFields_ = [3];
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.UpdateRequestParticipantsRequest.prototype.toObject = function(opt_includeInstance) {
return proto.messages.UpdateRequestParticipantsRequest.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.UpdateRequestParticipantsRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.UpdateRequestParticipantsRequest.toObject = function(includeInstance, msg) {
var f, obj = {
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
jid: jspb.Message.getFieldWithDefault(msg, 2, ""),
participantsList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f,
action: jspb.Message.getFieldWithDefault(msg, 4, 0)
};
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.UpdateRequestParticipantsRequest}
*/
proto.messages.UpdateRequestParticipantsRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.messages.UpdateRequestParticipantsRequest;
return proto.messages.UpdateRequestParticipantsRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.messages.UpdateRequestParticipantsRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.messages.UpdateRequestParticipantsRequest}
*/
proto.messages.UpdateRequestParticipantsRequest.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.setJid(value);
break;
case 3:
var value = /** @type {string} */ (reader.readString());
msg.addParticipants(value);
break;
case 4:
var value = /** @type {!proto.messages.ParticipantRequestAction} */ (reader.readEnum());
msg.setAction(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.messages.UpdateRequestParticipantsRequest.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.UpdateRequestParticipantsRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.UpdateRequestParticipantsRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getSession();
if (f != null) {
writer.writeMessage(
1,
f,
proto.messages.Session.serializeBinaryToWriter
);
}
f = message.getJid();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getParticipantsList();
if (f.length > 0) {
writer.writeRepeatedString(
3,
f
);
}
f = message.getAction();
if (f !== 0.0) {
writer.writeEnum(
4,
f
);
}
};
/**
* optional Session session = 1;
* @return {?proto.messages.Session}
*/
proto.messages.UpdateRequestParticipantsRequest.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.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.setSession = function(value) {
return jspb.Message.setWrapperField(this, 1, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.clearSession = function() {
return this.setSession(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.hasSession = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional string jid = 2;
* @return {string}
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.getJid = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.setJid = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
/**
* repeated string participants = 3;
* @return {!Array<string>}
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.getParticipantsList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 3));
};
/**
* @param {!Array<string>} value
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.setParticipantsList = function(value) {
return jspb.Message.setField(this, 3, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.addParticipants = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 3, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.clearParticipantsList = function() {
return this.setParticipantsList([]);
};
/**
* optional ParticipantRequestAction action = 4;
* @return {!proto.messages.ParticipantRequestAction}
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.getAction = function() {
return /** @type {!proto.messages.ParticipantRequestAction} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
};
/**
* @param {!proto.messages.ParticipantRequestAction} value
* @return {!proto.messages.UpdateRequestParticipantsRequest} returns this
*/
proto.messages.UpdateRequestParticipantsRequest.prototype.setAction = function(value) {
return jspb.Message.setProto3EnumField(this, 4, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -22845,6 +23135,14 @@ proto.messages.ParticipantAction = {
DEMOTE: 3
};
/**
* @enum {number}
*/
proto.messages.ParticipantRequestAction = {
APPROVE: 0,
REJECT: 1
};
/**
* @enum {number}
*/
@@ -15,12 +15,19 @@ import { Jid } from '@waha/core/engines/const';
import { EventsFromObservable } from '@waha/core/engines/gows/EventsFromObservable';
import { GowsEventStreamObservable } from '@waha/core/engines/gows/GowsEventStreamObservable';
import {
ToGroupJoinRequest,
ToGroupJoinRequestResult,
ToGroupParticipants,
ToGroupV2JoinEvent,
ToGroupV2LeaveEvent,
ToGroupV2ParticipantsJoinRequestEvents,
ToGroupV2ParticipantsEvents,
ToGroupV2UpdateEvent,
} from '@waha/core/engines/gows/groups.gows';
import {
GOWSGroupParticipant,
GOWSGroupParticipantRequest,
} from '@waha/core/engines/gows/types.group';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import {
optional,
@@ -116,11 +123,14 @@ import {
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupSortField,
Participant,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
@@ -247,6 +257,7 @@ enum WhatsMeowEvent {
// Groups
GROUP_INFO = 'events.GroupInfo',
JOINED_GROUP = 'events.JoinedGroup',
GROUP_JOIN_REQUEST = 'gows.GroupJoinRequestEvent',
// Labels
LABEL_EDIT = 'events.LabelEdit',
LABEL_ASSOCIATION_CHAT = 'events.LabelAssociationChat',
@@ -753,6 +764,15 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.GROUP_V2_UPDATE).switch(groupV2Update$);
const groupV2ParticipantsJoinRequest$ = all$.pipe(
onlyEvent(WhatsMeowEvent.GROUP_JOIN_REQUEST),
map(ToGroupV2ParticipantsJoinRequestEvents),
mergeMap((events) => events),
);
this.events2
.get(WAHAEvents.GROUP_V2_PARTICIPANTS_JOIN_REQUEST)
.switch(groupV2ParticipantsJoinRequest$);
// Label Events
// First, create streams for the raw label events
const labelEditEvents$ = all$.pipe(
@@ -1683,6 +1703,79 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return;
}
public async getMembershipApprovalMode(
id: string,
): Promise<SettingsMembershipApproval> {
const group = await this.getGroup(id);
return {
newMembersApprovalRequired: !!group.IsJoinApprovalRequired,
};
}
@Activity()
public async setMembershipApprovalMode(
id: string,
value: boolean,
): Promise<boolean> {
const req = new messages.JidBoolRequest({
session: this.session,
jid: id,
value: value,
});
await promisify(this.client.SetGroupJoinApprovalMode)(req);
return true;
}
@Activity()
public async getGroupJoinRequests(id: string): Promise<GroupJoinRequest[]> {
const req = new messages.JidRequest({
session: this.session,
jid: id,
});
const response = await promisify(this.client.GetGroupRequestParticipants)(
req,
);
const requests: GOWSGroupParticipantRequest[] = parseJsonList(response);
return requests.map(ToGroupJoinRequest);
}
@Activity()
public approveGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const action = messages.ParticipantRequestAction.APPROVE;
return this.updateRequestParticipants(id, request.participants, action);
}
@Activity()
public rejectGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const action = messages.ParticipantRequestAction.REJECT;
return this.updateRequestParticipants(id, request.participants, action);
}
private async updateRequestParticipants(
id: string,
participants: Array<Participant>,
action: messages.ParticipantRequestAction,
): Promise<GroupJoinRequestResult[]> {
const jids = participants.map((p) => normalizeJid(toJID(p.id)));
const req = new messages.UpdateRequestParticipantsRequest({
session: this.session,
jid: id,
participants: jids,
action: action,
});
const response = await promisify(
this.client.UpdateGroupRequestParticipants,
)(req);
const results: GOWSGroupParticipant[] = parseJsonList(response);
return results.map(ToGroupJoinRequestResult);
}
public deleteGroup(id) {
throw new NotImplementedByEngineError();
}
+22
View File
@@ -89,6 +89,7 @@ export interface GOWSGroupParticipant {
PhoneNumber: JID;
IsAdmin: boolean;
IsSuperAdmin: boolean;
Error?: number; // set when the participant action failed
}
export interface GroupInfoFull {
@@ -107,3 +108,24 @@ export interface JoinedGroupEvent extends GroupInfoFull {
Reason: string;
Type: string;
}
export interface GOWSGroupJoinRequest {
JID: JID;
PhoneNumber: JID; // empty string if not provided
RequestMethod: string; // invite_link | linked_group_join | non_admin_add
}
export interface GroupJoinRequestEvent {
JID: JID;
Sender?: JID;
SenderPN?: JID;
Timestamp: string;
Action: string; // created | revoked
Requests: GOWSGroupJoinRequest[];
}
// GetGroupRequestParticipants RPC result item
export interface GOWSGroupParticipantRequest {
JID: JID;
RequestedAt: string;
}
+88
View File
@@ -7,13 +7,18 @@ import type {
import { getGroupInviteLink } from '@waha/core/abc/session.abc';
import {
GroupInfo,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupParticipantRole,
NormalizeJoinRequestMethod,
} from '@waha/structures/groups.dto';
import {
GroupParticipantsJoinRequestAction,
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
@@ -164,6 +169,89 @@ export function ToGroupV2UpdateEvent(
};
}
// Raw <membership_approval_request> node attrs from groupRequestParticipantsList
interface MembershipApprovalRequestAttrs {
jid?: string;
phone_number?: string;
request_method?: string;
request_time?: string;
t?: string;
}
export function ToGroupJoinRequest(
attrs: MembershipApprovalRequestAttrs,
): GroupJoinRequest {
return {
requesterId: toCusFormat(attrs.jid),
requesterPn: toCusFormat(attrs.phone_number) || null,
addedById: null,
parentGroupId: null,
requestMethod: NormalizeJoinRequestMethod(attrs.request_method),
timestamp: Number(attrs.request_time || attrs.t) || 0,
};
}
export function ToGroupJoinRequestResult(result: {
status: string;
jid: string | undefined;
}): GroupJoinRequestResult {
const success = result.status === '200';
let error: number | undefined;
if (!success) {
error = Number(result.status) || undefined;
}
return {
requesterId: toCusFormat(result.jid) || null,
success: success,
error: error,
};
}
interface GroupJoinRequestUpdate {
id: string;
author: string;
authorPn?: string;
participant: string;
participantPn?: string;
action: string;
method?: string;
}
function ToGroupParticipantsJoinRequestAction(
action: string,
): GroupParticipantsJoinRequestAction | null {
switch (action) {
case 'created':
return GroupParticipantsJoinRequestAction.CREATED;
case 'rejected':
return GroupParticipantsJoinRequestAction.REJECTED;
case 'revoked':
return GroupParticipantsJoinRequestAction.REVOKED;
default:
return null;
}
}
export function ToGroupV2ParticipantsJoinRequestEvent(
update: GroupJoinRequestUpdate,
): GroupV2ParticipantsJoinRequestEvent | null {
const action = ToGroupParticipantsJoinRequestAction(update.action);
if (!action) {
return null;
}
return {
group: {
id: toCusFormat(update.id),
},
action: action,
requesterId: toCusFormat(update.participant),
requesterPn: toCusFormat(update.participantPn) || null,
requestMethod: NormalizeJoinRequestMethod(update.method),
timestamp: Date.now(),
_data: update,
};
}
export function ToGroupV2LeaveEvent(
me: Contact,
update: GroupParticipantUpdate,
@@ -51,9 +51,12 @@ import {
WhatsappSession,
} from '@waha/core/abc/session.abc';
import {
ToGroupJoinRequest,
ToGroupJoinRequestResult,
ToGroupParticipant,
ToGroupV2JoinEvent,
ToGroupV2LeaveEvent,
ToGroupV2ParticipantsJoinRequestEvent,
ToGroupV2Participants,
ToGroupV2UpdateEvent,
} from '@waha/core/engines/noweb/groups.noweb';
@@ -154,9 +157,12 @@ import {
import { BinaryFile, FileType, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import {
@@ -2086,6 +2092,59 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return await this.sock.groupMemberAddMode(id, mode);
}
public async getMembershipApprovalMode(
id: string,
): Promise<SettingsMembershipApproval> {
const group = await this.getGroup(id);
return { newMembersApprovalRequired: !!group.joinApprovalMode };
}
@Activity()
public async setMembershipApprovalMode(
id: string,
value: boolean,
): Promise<boolean> {
const mode = value ? 'on' : 'off';
await this.sock.groupJoinApprovalMode(id, mode);
return true;
}
@Activity()
public async getGroupJoinRequests(id: string): Promise<GroupJoinRequest[]> {
const requests = await this.sock.groupRequestParticipantsList(id);
return requests.map(ToGroupJoinRequest);
}
@Activity()
public async approveGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
return await this.updateGroupJoinRequests(id, request, 'approve');
}
@Activity()
public async rejectGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
return await this.updateGroupJoinRequests(id, request, 'reject');
}
private async updateGroupJoinRequests(
id: string,
request: ParticipantsRequest,
action: 'approve' | 'reject',
): Promise<GroupJoinRequestResult[]> {
const participants = request.participants.map(getId);
const results = await this.sock.groupRequestParticipantsUpdate(
id,
participants,
action,
);
return results.map(ToGroupJoinRequestResult);
}
@Activity()
public async leaveGroup(id) {
return this.sock.groupLeave(id);
@@ -2781,6 +2840,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.GROUP_V2_LEAVE).switch(groupV2Leave$);
const groupV2ParticipantsJoinRequest$: Observable<any> = fromEvent(
this.sock.ev,
'group.join-request',
).pipe(
map((event: any) => ToGroupV2ParticipantsJoinRequestEvent(event)),
filter(Boolean),
);
this.events2
.get(WAHAEvents.GROUP_V2_PARTICIPANTS_JOIN_REQUEST)
.switch(groupV2ParticipantsJoinRequest$);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
filter((presence: any) => this.jids.include(presence.id)),
+64 -1
View File
@@ -2,21 +2,28 @@ import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
GroupId,
GroupInfo,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupParticipantRole,
NormalizeJoinRequestMethod,
} from '@waha/structures/groups.dto';
import {
GroupParticipantsJoinRequestAction,
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
import {
GroupChat,
GroupMembershipRequest as WEBJSGroupJoinRequest,
GroupNotification,
GroupNotificationTypes,
GroupParticipant as WEBJSGroupParticipant,
MembershipRequestActionResult as WEBJSMembershipRequestActionResult,
} from 'whatsapp-web.js';
import { isPnUser } from '@waha/core/utils/jids';
import { GetSerialized } from '@waha/core/utils/serialized';
@@ -36,12 +43,68 @@ function ToGroupInfo(
invite: invite,
membersCanAddNewMember: groupMetadata.memberAddMode === 'all_member_add',
membersCanSendMessages: groupMetadata.announce,
newMembersApprovalRequired: groupMetadata.membershipApprovalMode,
newMembersApprovalRequired: ToGroupMembershipApprovalRequired(
groupMetadata.membershipApprovalMode,
),
participants: participants,
};
return info;
}
export function ToGroupMembershipApprovalRequired(value: unknown): boolean {
return value === true || value === 1;
}
export function ToGroupJoinRequest(
request: WEBJSGroupJoinRequest,
): GroupJoinRequest {
const requesterId = GetSerialized(request.id);
if (!requesterId) {
throw new Error('Unable to serialize group membership requester ID');
}
return {
requesterId: requesterId,
addedById: GetSerialized(request.addedBy),
parentGroupId: GetSerialized(request.parentGroupId),
requestMethod: NormalizeJoinRequestMethod(request.requestMethod),
timestamp: request.t,
};
}
export function ToGroupJoinRequestResults(
result: WEBJSMembershipRequestActionResult,
): GroupJoinRequestResult[] {
// whatsapp-web.js may report one result for multiple requesters - flatten to one entry per requester
let requesterIds: (string | null)[];
if (Array.isArray(result.requesterId)) {
requesterIds = result.requesterId;
} else {
requesterIds = [result.requesterId ?? null];
}
return requesterIds.map((requesterId) => ({
requesterId: requesterId,
success: !result.error,
error: result.error,
}));
}
export function ToGroupV2ParticipantsJoinRequestEvent(
notification: GroupNotification,
): GroupV2ParticipantsJoinRequestEvent | null {
if (!notification.chatId || !notification.author) {
return null;
}
return {
group: {
id: notification.chatId,
},
action: GroupParticipantsJoinRequestAction.CREATED,
requesterId: notification.author,
timestamp: notification.timestamp,
_data: notification,
};
}
export async function ToGroupV2JoinEvent(
client: WebjsClientCore,
me: string,
@@ -15,8 +15,12 @@ import {
} from '@waha/core/engines/webjs/ack.webjs';
import {
getParticipants,
ToGroupMembershipApprovalRequired,
ToGroupJoinRequest,
ToGroupJoinRequestResults,
ToGroupV2JoinEvent,
ToGroupV2LeaveEvent,
ToGroupV2ParticipantsJoinRequestEvent,
ToGroupV2ParticipantsEvent,
ToGroupV2UpdateEvent,
} from '@waha/core/engines/webjs/groups.webjs';
@@ -106,10 +110,13 @@ import {
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupSortField,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
@@ -1691,6 +1698,101 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return groupChat.setAddMembersAdminsOnly(!value);
}
@Activity()
public async getMembershipApprovalMode(
id: string,
): Promise<SettingsMembershipApproval> {
const membershipApprovalMode = await this.whatsapp.pupPage.evaluate(
async (groupId) => {
await window
.require('WAWebGroupQueryJob')
.queryAndUpdateGroupMetadataById({ id: groupId });
const groupWid = window.require('WAWebWidFactory').createWid(groupId);
const group = await window
.require('WAWebCollections')
.Chat.find(groupWid);
return group.groupMetadata.membershipApprovalMode;
},
id,
);
return {
newMembersApprovalRequired: ToGroupMembershipApprovalRequired(
membershipApprovalMode,
),
};
}
@Activity()
public async setMembershipApprovalMode(
id: string,
value: boolean,
): Promise<boolean> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
const success = await this.whatsapp.pupPage.evaluate(
async (groupId, enabled) => {
// @ts-ignore
const chat = await window.WWebJS.getChat(groupId, {
getAsModel: false,
});
try {
await window
.require('WAWebSetPropertyGroupAction')
.setGroupProperty(
chat,
'membership_approval_mode',
enabled ? 1 : 0,
);
return true;
} catch (error) {
if (error.name === 'ServerStatusCodeError') {
return false;
}
throw error;
}
},
id,
value,
);
if (success) {
(groupChat as any).groupMetadata.membershipApprovalMode = value;
}
return success;
}
@Activity()
public async getGroupJoinRequests(id: string): Promise<GroupJoinRequest[]> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
const requests = await groupChat.getGroupMembershipRequests();
return requests.map(ToGroupJoinRequest);
}
@Activity()
public async approveGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
const results = await groupChat.approveGroupMembershipRequests({
requesterIds: request.participants.map((participant) => participant.id),
sleep: [250, 500],
});
return results.flatMap(ToGroupJoinRequestResults);
}
@Activity()
public async rejectGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
const results = await groupChat.rejectGroupMembershipRequests({
requesterIds: request.participants.map((participant) => participant.id),
sleep: [250, 500],
});
return results.flatMap(ToGroupJoinRequestResults);
}
public async getGroups(pagination: PaginationParams) {
const chats = await this.whatsapp.getChats();
const groups = lodash.filter(chats, (chat) => chat.isGroup);
@@ -2373,6 +2475,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
.get(WAHAEvents.GROUP_V2_PARTICIPANTS)
.switch(groupV2Participants);
const groupV2ParticipantsJoinRequest$ = fromEvent<GroupNotification>(
this.whatsapp,
Events.GROUP_MEMBERSHIP_REQUEST,
).pipe(map(ToGroupV2ParticipantsJoinRequestEvent), filter(Boolean));
this.events2
.get(WAHAEvents.GROUP_V2_PARTICIPANTS_JOIN_REQUEST)
.switch(groupV2ParticipantsJoinRequest$);
const groupUpdate$ = fromEvent<GroupNotification>(
this.whatsapp,
Events.GROUP_UPDATE,
+23
View File
@@ -8,8 +8,10 @@ import {
GroupParticipantRole,
} from '@waha/structures/groups.dto';
import {
GroupParticipantsJoinRequestAction,
GroupParticipantType,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
@@ -203,6 +205,27 @@ export function WppGp2ToGroupV2Update(msg: any): GroupV2UpdateEvent {
};
}
export function WppGp2ToGroupV2ParticipantsJoinRequest(
msg: any,
): GroupV2ParticipantsJoinRequestEvent | null {
// Same subtype whatsapp-web.js emits 'group_membership_request' for
if (msg.subtype !== 'membership_approval_request') {
return null;
}
if (!msg.chatId || !msg.author) {
return null;
}
return {
group: {
id: toCusFormat(msg.chatId),
},
action: GroupParticipantsJoinRequestAction.CREATED,
requesterId: toCusFormat(msg.author),
timestamp: msg.timestamp,
_data: msg,
};
}
export function WppUpdateLabelToAssociations(
data: WppUpdateLabelEvent,
): LabelChatAssociation[] {
+93
View File
@@ -70,6 +70,7 @@ import { CallData } from '@waha/structures/calls.dto';
import {
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
@@ -110,6 +111,7 @@ import {
} from '@wppconnect-team/wppconnect';
import type { Logger as WinstonLogger } from 'winston';
import {
WppGp2ToGroupV2ParticipantsJoinRequest,
WppGp2ToGroupV2Update,
WppParticipantsIsMyJoin,
WppParticipantsIsMyLeave,
@@ -138,6 +140,7 @@ import { getSessionNamespace } from '@waha/config';
import { killProcessesByPatterns } from '@waha/core/utils/processes';
import { DistinctAck } from '@waha/core/utils/reactive';
import { isJidGroup, toCusFormat } from '@waha/core/utils/jids';
import { GetSerialized } from '@waha/core/utils/serialized';
import { WAMedia } from '@waha/structures/media.dto';
import * as lodash from 'lodash';
import {
@@ -161,11 +164,15 @@ import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupInfo,
GroupJoinRequest,
GroupJoinRequestResult,
GroupParticipant,
GroupParticipantRole,
GroupSortField,
NormalizeJoinRequestMethod,
ParticipantsRequest,
SettingsMemberAddMode,
SettingsMembershipApproval,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
@@ -1433,6 +1440,67 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
);
}
public async getMembershipApprovalMode(
id: string,
): Promise<SettingsMembershipApproval> {
const group = await this.wpp!.getChatById(this.ensureSuffix(id));
return {
newMembersApprovalRequired:
!!group?.groupMetadata?.membershipApprovalMode,
};
}
@Activity()
public setMembershipApprovalMode(
id: string,
value: boolean,
): Promise<boolean> {
// Not in the GroupProperty enum, but wa-js passes the property through
return this.wpp!.setGroupProperty(
this.ensureSuffix(id),
'membership_approval_mode' as GroupProperty,
value,
);
}
@Activity()
public async getGroupJoinRequests(id: string): Promise<GroupJoinRequest[]> {
const requests = await this.wpp!.getGroupMembershipRequests(
this.ensureSuffix(id),
);
return requests.map((request) => ({
requesterId: GetSerialized(request.id),
addedById: GetSerialized(request.addedBy),
parentGroupId: GetSerialized(request.parentGroupId),
requestMethod: NormalizeJoinRequestMethod(request.requestMethod),
timestamp: request.t,
}));
}
@Activity()
public async approveGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const results = await this.wpp!.approveGroupMembershipRequest(
this.ensureSuffix(id),
request.participants.map((participant) => participant.id),
);
return results.map(WppToGroupJoinRequestResult);
}
@Activity()
public async rejectGroupJoinRequests(
id: string,
request: ParticipantsRequest,
): Promise<GroupJoinRequestResult[]> {
const results = await this.wpp!.rejectGroupMembershipRequest(
this.ensureSuffix(id),
request.participants.map((participant) => participant.id),
);
return results.map(WppToGroupJoinRequestResult);
}
@Activity()
public deleteGroup(id: string): Promise<boolean> {
return this.wpp!.deleteChat(this.ensureSuffix(id));
@@ -1927,6 +1995,20 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.GROUP_V2_UPDATE).switch(groupV2Update$);
const groupV2ParticipantsJoinRequest$ = streams.onAnyMessage.pipe(
map((p) => p.data),
filter((msg) => msg?.type === MessageType.GP2),
distinct((msg) => msg?.id, interval(60_000)),
map(
(msg): GroupV2ParticipantsJoinRequestEvent =>
WppGp2ToGroupV2ParticipantsJoinRequest(msg),
),
filter(Boolean),
);
this.events2
.get(WAHAEvents.GROUP_V2_PARTICIPANTS_JOIN_REQUEST)
.switch(groupV2ParticipantsJoinRequest$);
//
// Calls
//
@@ -2738,6 +2820,17 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
}
}
function WppToGroupJoinRequestResult(result: {
error: any;
wid: any;
}): GroupJoinRequestResult {
return {
requesterId: GetSerialized(result.wid),
success: !result.error,
error: Number(result.error) || undefined,
};
}
function extractWppMessageId(response: unknown): string | null {
if (!response || typeof response !== 'object') {
return null;
+1
View File
@@ -17,6 +17,7 @@ export enum WAHAEvents {
GROUP_V2_LEAVE = 'group.v2.leave',
GROUP_V2_UPDATE = 'group.v2.update',
GROUP_V2_PARTICIPANTS = 'group.v2.participants',
GROUP_V2_PARTICIPANTS_JOIN_REQUEST = 'group.v2.participants.join-request',
PRESENCE_UPDATE = 'presence.update',
POLL_VOTE = 'poll.vote',
POLL_VOTE_FAILED = 'poll.vote.failed',
+96
View File
@@ -30,6 +30,102 @@ export class SettingsMemberAddMode {
membersCanAddNewMember: boolean = true;
}
export class SettingsMembershipApproval {
@IsBoolean()
newMembersApprovalRequired: boolean = false;
}
export enum GroupJoinRequestMethod {
INVITE_LINK = 'invite_link',
LINKED_GROUP_JOIN = 'linked_group_join',
NON_ADMIN_ADD = 'non_admin_add',
}
export function NormalizeJoinRequestMethod(
value: string | null | undefined,
): GroupJoinRequestMethod | null {
// Engines report the method in different formats
// NOWEB/GOWS use 'invite_link'
// WEBJS/WPP use 'InviteLink'
switch (value) {
case 'InviteLink':
case GroupJoinRequestMethod.INVITE_LINK:
return GroupJoinRequestMethod.INVITE_LINK;
case 'LinkedGroupJoin':
case GroupJoinRequestMethod.LINKED_GROUP_JOIN:
return GroupJoinRequestMethod.LINKED_GROUP_JOIN;
case 'NonAdminAdd':
case GroupJoinRequestMethod.NON_ADMIN_ADD:
return GroupJoinRequestMethod.NON_ADMIN_ADD;
default:
return null;
}
}
export class GroupJoinRequest {
@ApiProperty({
description: 'ID of the user requesting to join the group',
example: '123456789@c.us',
})
requesterId: string;
@ApiProperty({
description: 'Phone number ID of the requester, if known',
example: '123456789@c.us',
nullable: true,
required: false,
})
requesterPn?: string | null;
@ApiProperty({
description: 'ID of the user who created the request',
example: '123456789@c.us',
nullable: true,
})
addedById: string | null;
@ApiProperty({
description: 'ID of the parent community group, if present',
example: '123456789@g.us',
nullable: true,
})
parentGroupId: string | null;
@ApiProperty({
description:
'How the request was created, for example non_admin_add, invite_link, or linked_group_join',
example: 'invite_link',
nullable: true,
})
requestMethod: string | null;
@ApiProperty({
description: 'Unix timestamp when the request was created',
example: 1666943582,
})
timestamp: number;
}
export class GroupJoinRequestResult {
@ApiProperty({
description: 'ID of the participant the action was applied to',
example: '123456789@c.us',
nullable: true,
})
requesterId: string | null;
@ApiProperty({
example: true,
})
success: boolean;
@ApiProperty({
required: false,
example: 404,
})
error?: number;
}
/**
* Queries
*/
+51
View File
@@ -69,3 +69,54 @@ export class GroupV2ParticipantsEvent {
_data: any;
}
export enum GroupParticipantsJoinRequestAction {
// A user requested to join the group
CREATED = 'created',
// An admin rejected the request
REJECTED = 'rejected',
// The requester cancelled their own request
REVOKED = 'revoked',
}
export class GroupV2ParticipantsJoinRequestEvent {
group: GroupId;
@ApiProperty({
description: 'What happened to the request to join the group',
enum: GroupParticipantsJoinRequestAction,
example: GroupParticipantsJoinRequestAction.CREATED,
})
action: GroupParticipantsJoinRequestAction;
@ApiProperty({
description: 'ID of the user requesting to join the group',
example: '123456789@lid',
})
requesterId: string;
@ApiProperty({
description: 'Phone number ID of the requester, if known',
example: '123456789@c.us',
nullable: true,
required: false,
})
requesterPn?: string | null;
@ApiProperty({
description:
'How the request was created, for example non_admin_add, invite_link, or linked_group_join',
example: 'invite_link',
nullable: true,
required: false,
})
requestMethod?: string | null;
@ApiProperty({
description: 'Unix timestamp',
example: 1666943582,
})
timestamp: number;
_data: any;
}
+10
View File
@@ -3,6 +3,7 @@ import { WAHAEvents } from '@waha/structures/enums.dto';
import {
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsJoinRequestEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
@@ -43,3 +44,12 @@ export class WebhookGroupV2Participants extends WAHAWebhook {
payload: GroupV2ParticipantsEvent;
}
export class WebhookGroupV2ParticipantsJoinRequest extends WAHAWebhook {
@ApiProperty({
description: 'When a user requests to join a group',
})
event = WAHAEvents.GROUP_V2_PARTICIPANTS_JOIN_REQUEST;
payload: GroupV2ParticipantsJoinRequestEvent;
}
+2
View File
@@ -1,6 +1,7 @@
import {
WebhookGroupV2Join,
WebhookGroupV2Leave,
WebhookGroupV2ParticipantsJoinRequest,
WebhookGroupV2Participants,
WebhookGroupV2Update,
} from '@waha/structures/groups.webhooks.dto';
@@ -45,6 +46,7 @@ const WAHA_WEBHOOKS = [
WebhookGroupV2Leave,
WebhookGroupV2Update,
WebhookGroupV2Participants,
WebhookGroupV2ParticipantsJoinRequest,
WAHAWebhookPresenceUpdate,
WAHAWebhookPollVote,
WAHAWebhookPollVoteFailed,