feat: GET /api/sessions/{name}/timelock - fix #2219

This commit is contained in:
devlikepro committed 2026-08-14 18:48:18 +07:00
1 parent 8d4eb02202
commit 841063739e
9 files changed
+153 -36

No files matched your search

+18
View File
@@ -35,6 +35,7 @@ import {
ListSessionsQuery,
MeInfo,
MessageCappingData,
ReachoutTimelockData,
SessionCreateRequest,
SessionDTO,
SessionExpand,
@@ -115,6 +116,23 @@ class SessionsController {
return session.fetchMessageCapping();
}
@Get(':session/timelock')
@SessionApiParam
@ApiOperation({
summary: 'Fetch the account reachout timelock state',
description:
'Fetch a fresh reachout timelock state from WhatsApp - the restriction ' +
'behind "server returned error 463" when messaging new contacts. ' +
'The same value is also available under me.reachoutTimelock in the session ' +
'info, and changes are pushed through the session.status event.',
})
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
fetchReachoutTimelock(
@SessionParam session: WhatsappSession,
): Promise<ReachoutTimelockData> {
return session.fetchReachoutTimelock();
}
@Post('')
@ApiOperation({
summary: 'Create a session',
+19
View File
@@ -189,4 +189,23 @@ export class SessionTools extends McpController {
url: `/api/sessions/${session}/capping`,
});
}
@Tool('sessions-timelock', {
title: 'Get reachout timelock',
description:
'Fetch the account reachout timelock state ' +
'(the restriction behind error 463 when messaging new contacts).',
inputSchema: SessionNameInput,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
})
async timelock({ session }: z.infer<typeof SessionNameInput>) {
return this.textRequest({
method: 'GET',
url: `/api/sessions/${session}/timelock`,
});
}
}
+5
View File
@@ -118,6 +118,7 @@ import {
MessageCappingData,
MessageCappingStatus,
ProxyConfig,
ReachoutTimelockData,
SessionConfig,
} from '../../structures/sessions.dto';
import {
@@ -530,6 +531,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public fetchReachoutTimelock(): Promise<ReachoutTimelockData> {
throw new NotImplementedByEngineError();
}
/**
* Auth methods
*/
+13
View File
@@ -11529,6 +11529,15 @@ export namespace messages {
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
},
FetchReachoutTimelock: {
path: "/messages.MessageService/FetchReachoutTimelock",
requestStream: false,
responseStream: false,
requestSerialize: (message: Session) => Buffer.from(message.serialize()),
requestDeserialize: (bytes: Buffer) => Session.deserialize(new Uint8Array(bytes)),
responseSerialize: (message: Json) => Buffer.from(message.serialize()),
responseDeserialize: (bytes: Buffer) => Json.deserialize(new Uint8Array(bytes))
},
FetchMessageCapping: {
path: "/messages.MessageService/FetchMessageCapping",
requestStream: false,
@@ -11855,6 +11864,7 @@ export namespace messages {
abstract SubscribePresence(call: grpc_1.ServerUnaryCall<SubscribePresenceRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract CheckPhones(call: grpc_1.ServerUnaryCall<CheckPhonesRequest, CheckPhonesResponse>, callback: grpc_1.sendUnaryData<CheckPhonesResponse>): void;
abstract MarkChatUnread(call: grpc_1.ServerUnaryCall<ChatUnreadRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
abstract FetchReachoutTimelock(call: grpc_1.ServerUnaryCall<Session, Json>, callback: grpc_1.sendUnaryData<Json>): void;
abstract FetchMessageCapping(call: grpc_1.ServerUnaryCall<Session, Json>, callback: grpc_1.sendUnaryData<Json>): void;
abstract GenerateNewMessageID(call: grpc_1.ServerUnaryCall<Session, NewMessageIDResponse>, callback: grpc_1.sendUnaryData<NewMessageIDResponse>): void;
abstract SendMessage(call: grpc_1.ServerUnaryCall<MessageRequest, MessageResponse>, callback: grpc_1.sendUnaryData<MessageResponse>): void;
@@ -12000,6 +12010,9 @@ export namespace messages {
MarkChatUnread: GrpcUnaryServiceInterface<ChatUnreadRequest, Empty> = (message: ChatUnreadRequest, 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.MarkChatUnread(message, metadata, options, callback);
};
FetchReachoutTimelock: GrpcUnaryServiceInterface<Session, Json> = (message: Session, 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.FetchReachoutTimelock(message, metadata, options, callback);
};
FetchMessageCapping: GrpcUnaryServiceInterface<Session, Json> = (message: Session, 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.FetchMessageCapping(message, metadata, options, callback);
};
+13 -1
View File
@@ -1145,7 +1145,19 @@ getProfilePicture: {
//
// Account
//
// Fetch the account's current new-chat message capping (per-cycle quota).
// Fetch the account's current reachout timelock state.
fetchReachoutTimelock: {
path: '/messages.MessageService/FetchReachoutTimelock',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
// Fetch the account's current new-chat message capping (per-cycle quota).
fetchMessageCapping: {
path: '/messages.MessageService/FetchMessageCapping',
requestStream: false,
@@ -139,6 +139,7 @@ import {
MeInfo,
MessageCappingData,
ProxyConfig,
ReachoutTimelockData,
SessionConfig,
} from '@waha/structures/sessions.dto';
import {
@@ -2299,6 +2300,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return capping;
}
@Activity()
public async fetchReachoutTimelock(): Promise<ReachoutTimelockData> {
const response = await promisify(this.client.FetchReachoutTimelock)(
this.session,
);
const timelock = parseGowsReachoutTimelock(parseJson(response));
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
this.reachoutTimelock.update(timelock);
return timelock;
}
public async getContacts(pagination: PaginationParams) {
const request = new messages.GetContactsRequest({
session: this.session,
+26 -14
View File
@@ -177,6 +177,7 @@ import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import {
MeInfo,
MessageCappingData,
ReachoutTimelockData,
ReachoutTimelockEnforcementType,
} from '@waha/structures/sessions.dto';
import { EnsureSeconds } from '@waha/utils/timehelper';
@@ -572,20 +573,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr, isNewLogin } = update;
if (update.reachoutTimeLock) {
const timelock = update.reachoutTimeLock;
const enforcementType =
timelock.enforcementType ?? ReachoutTimelockEnforcementType.DEFAULT;
let timeEnforcementEnds: number | null = null;
if (timelock.timeEnforcementEnds) {
timeEnforcementEnds = EnsureSeconds(
timelock.timeEnforcementEnds.getTime(),
);
}
this.reachoutTimelock.update({
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
isActive: timelock.isActive === true,
timeEnforcementEnds: timeEnforcementEnds,
});
this.updateReachoutTimelockFromState(update.reachoutTimeLock);
}
if (isNewLogin) {
this.restartClient();
@@ -1058,6 +1046,30 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return capping;
}
@Activity()
public async fetchReachoutTimelock(): Promise<ReachoutTimelockData> {
const state = await this.sock.fetchAccountReachoutTimelock();
return this.updateReachoutTimelockFromState(state);
}
private updateReachoutTimelockFromState(timelock: any): ReachoutTimelockData {
const enforcementType =
timelock.enforcementType ?? ReachoutTimelockEnforcementType.DEFAULT;
let timeEnforcementEnds: number | null = null;
if (timelock.timeEnforcementEnds) {
timeEnforcementEnds = EnsureSeconds(
timelock.timeEnforcementEnds.getTime(),
);
}
const data: ReachoutTimelockData = {
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
isActive: timelock.isActive === true,
timeEnforcementEnds: timeEnforcementEnds,
};
this.reachoutTimelock.update(data);
return data;
}
/**
* Other methods
*/
+45 -19
View File
@@ -130,6 +130,7 @@ import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import {
MeInfo,
MessageCappingData,
ReachoutTimelockData,
ReachoutTimelockEnforcementType,
} from '@waha/structures/sessions.dto';
import { EnsureSeconds } from '@waha/utils/timehelper';
@@ -629,25 +630,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected listenConnectionEvents() {
this.whatsapp.on(Events.REACHOUT_TIMELOCK_UPDATE, (record: any) => {
if (!record) {
// The app removes the stored record when the enforcement is lifted (or there is none)
const current = this.reachoutTimelock.value;
if (current?.isActive) {
this.reachoutTimelock.update({ ...current, isActive: false });
}
return;
}
const enforcementType =
record.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT;
this.reachoutTimelock.update({
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
// The record only exists while the enforcement is active
isActive: true,
// The app stores 'time_enforcement_ends' as unix milliseconds
timeEnforcementEnds: record.time_enforcement_ends
? EnsureSeconds(record.time_enforcement_ends)
: null,
});
this.updateReachoutTimelockFromRecord(record);
});
this.whatsapp.on(Events.MESSAGE_CAPPING_UPDATE, (record: any) => {
@@ -917,6 +900,49 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return capping;
}
@Activity()
public async fetchReachoutTimelock(): Promise<ReachoutTimelockData> {
const record = await this.whatsapp.fetchReachoutTimelock();
if (record === undefined) {
throw new NotImplementedByEngineError(
'Reachout timelock is not available in the current WhatsApp Web version',
);
}
return this.updateReachoutTimelockFromRecord(record);
}
// Applies the stored 'WAReachoutTimelockState' record (or its removal) to the tracker,
// so MeInfo and 'session.status' reflect it, and returns the resulting state
private updateReachoutTimelockFromRecord(record: any): ReachoutTimelockData {
if (!record) {
// The app removes the stored record when the enforcement is lifted (or there is none)
const current = this.reachoutTimelock.value;
if (current?.isActive) {
this.reachoutTimelock.update({ ...current, isActive: false });
}
return (
this.reachoutTimelock.value ?? {
enforcementType: ReachoutTimelockEnforcementType.DEFAULT,
isActive: false,
timeEnforcementEnds: null,
}
);
}
const enforcementType =
record.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT;
const timelock: ReachoutTimelockData = {
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
// The record only exists while the enforcement is active
isActive: true,
// The app stores 'time_enforcement_ends' as unix milliseconds
timeEnforcementEnds: record.time_enforcement_ends
? EnsureSeconds(record.time_enforcement_ends)
: null,
};
this.reachoutTimelock.update(timelock);
return timelock;
}
/**
* Groups methods
*/
+2 -2
View File
@@ -14350,7 +14350,7 @@ __metadata:
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26":
version: 1.34.7
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=d8f5e287eef6044b78e565f2900d960b5717ab3a"
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=7d4ce53562068d6610060599c8e8784dc0101b37"
dependencies:
archiver: "npm:7.0.1"
fluent-ffmpeg: "npm:2.1.3"
@@ -14367,7 +14367,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 10/1ef628f91ca447ac813937e4f0c407f662fe9ae7743f425746e972e07a6852530a644940f06aa21fedea76c1fdf4ef611d4e877c509adc26acfcbe995ea2bbfc
checksum: 10/df44673005a9aae09e14986efd4385f77e59b7a91c23bcb07ae60aed392818da7bdb404fa6d1a571da246347c6a05f34e7bd6f672a67ba15ad4bba95fa9ab0ca
languageName: node
linkType: hard