Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382c1a7e94 | ||
|
|
d7fb3091a6 | ||
|
|
1fbe60e646 | ||
|
|
841063739e | ||
|
|
8d4eb02202 | ||
|
|
f6ed147acc | ||
|
|
96a106cff7 | ||
|
|
98854e2da3 | ||
|
|
48c34cbc17 | ||
|
|
926b5c1b66 | ||
|
|
608bad07a9 | ||
|
|
64c6a32786 | ||
|
|
f33ca787a5 | ||
|
|
78aea9aa51 | ||
|
|
5e75d47149 | ||
|
|
c5a53d36bb | ||
|
|
0213692705 | ||
|
|
a112fde248 | ||
|
|
c207f3c08c | ||
|
|
1b8027473e | ||
|
|
976066b216 | ||
|
|
af229a1d40 | ||
|
|
d14126d8a6 | ||
|
|
d4ce0e6541 | ||
|
|
5ddbeb995e | ||
|
|
f26d43b299 | ||
|
|
ff998579f0 | ||
|
|
3cae921fba | ||
|
|
4682dddb4e | ||
|
|
2e6683d26f | ||
|
|
e97107ba1a | ||
|
|
2d96a57f72 | ||
|
|
9adcd3b133 | ||
|
|
79233e09e3 | ||
|
|
a96a4b2161 | ||
|
|
f736105873 | ||
|
|
2c1ea2c7c1 | ||
|
|
216e00cc25 | ||
|
|
ac1a015046 | ||
|
|
8f4af6bb08 | ||
|
|
acee1b7d79 | ||
|
|
99241c3089 | ||
|
|
8e764372d2 | ||
|
|
fa21a60cf3 | ||
|
|
02fa0ea06b | ||
|
|
8d1ad04625 | ||
|
|
a639baad8d | ||
|
|
2e24107018 | ||
|
|
750ce208b3 | ||
|
|
057e0a0e70 | ||
|
|
255f84a12d | ||
|
|
ac6d14394a | ||
|
|
cb77c2fc49 | ||
|
|
5c279c5240 |
No files matched your search
@@ -35,7 +35,7 @@ jobs:
|
||||
# run: yarn test:unit
|
||||
|
||||
docker-build:
|
||||
if: github.repository == 'devlikeapro/waha-plus'
|
||||
if: github.repository == 'devlikeapro/waha'
|
||||
# needs: unit-tests
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name:
|
||||
@@ -73,6 +73,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check recent changes
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
approvedGitRepositories:
|
||||
- '**'
|
||||
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableScripts: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 0
|
||||
|
||||
supportedArchitectures:
|
||||
cpu:
|
||||
- arm
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@ RUN apt-get update && \
|
||||
WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
|
||||
RUN npm install -g corepack && corepack enable
|
||||
RUN yarn set version 4.9.2
|
||||
RUN yarn install
|
||||
|
||||
# App
|
||||
@@ -179,7 +179,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="140.0.7339.80-1"
|
||||
ARG CHROME_VERSION="142.0.7444.134-1"
|
||||
ARG OPUSTAGS_VERSION="1.10.1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
|
||||
@@ -44,8 +44,8 @@ up-webjs:
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26
|
||||
|
||||
up-wpp:
|
||||
yarn up @wppconnect-team/wppconnect@github:wppconnect-team/wppconnect#master
|
||||
yarn up @wppconnect/wa-js@github:wppconnect-team/wa-js#main
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=1 yarn up @wppconnect-team/wppconnect@github:wppconnect-team/wppconnect#master
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=1 yarn up @wppconnect/wa-js@github:wppconnect-team/wa-js#main
|
||||
|
||||
up-rust-bridge:
|
||||
yarn up -R whatsapp-rust-bridge
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"watchPathIgnorePatterns": ["node_modules", "src/dashboard", "dist"],
|
||||
"assets": [
|
||||
{ "include": "dashboard/**", "watchAssets": false },
|
||||
{ "include": "core/engines/webjs/*", "watchAssets": true },
|
||||
|
||||
+1
-1
@@ -205,5 +205,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
"packageManager": "yarn@4.17.1"
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
JoinGroupRequest,
|
||||
JoinGroupResponse,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
@@ -309,6 +310,39 @@ export class GroupsController {
|
||||
return session.getMessagesAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/member-add-mode')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
|
||||
@ApiOperation({
|
||||
summary: 'Update settings - who can add new members',
|
||||
description:
|
||||
'Updates the group settings for who can add new members to the group - all members or admins only.',
|
||||
})
|
||||
setMemberAddMode(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsMemberAddMode,
|
||||
) {
|
||||
return session.setMemberAddMode(id, request.membersCanAddNewMember);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/member-add-mode')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
|
||||
@ApiOperation({
|
||||
summary: 'Get settings - who can add new members',
|
||||
description:
|
||||
'The group settings for who can add new members to the group - all members or admins only.',
|
||||
})
|
||||
getMemberAddMode(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsMemberAddMode> {
|
||||
return session.getMemberAddMode(id);
|
||||
}
|
||||
|
||||
@Get(':id/invite-code')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
|
||||
@@ -34,6 +34,8 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
ListSessionsQuery,
|
||||
MeInfo,
|
||||
MessageCappingData,
|
||||
ReachoutTimelockData,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionExpand,
|
||||
@@ -98,6 +100,39 @@ class SessionsController {
|
||||
return this.sessionService.getSessionMe(session);
|
||||
}
|
||||
|
||||
@Get(':session/capping')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Fetch the account new-chat message capping (per-cycle quota)',
|
||||
description:
|
||||
'Fetch a fresh new-chat message capping (quota) state from WhatsApp. ' +
|
||||
'The same value is also available under me.messageCapping in the session ' +
|
||||
'info, and changes are pushed through the session.status event.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
|
||||
fetchMessageCapping(
|
||||
@SessionParam session: WhatsappSession,
|
||||
): Promise<MessageCappingData> {
|
||||
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',
|
||||
|
||||
@@ -221,10 +221,23 @@ export class MessageHandler {
|
||||
mentions: mentions,
|
||||
};
|
||||
const session = this.session;
|
||||
await session.readMessages(chatId);
|
||||
await session.startTyping({ chatId: chatId, session: '' });
|
||||
// Best effort - do not block sending on read-receipt failure
|
||||
await session.readMessages(chatId).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error reading messages in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
await session.startTyping({ chatId: chatId, session: '' }).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error starting typing in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
await sleep(2000);
|
||||
await session.stopTyping({ chatId: chatId, session: '' });
|
||||
await session.stopTyping({ chatId: chatId, session: '' }).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error stopping typing in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
const msg = await session.sendText(request);
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export function clearContent(attachments) {
|
||||
/**
|
||||
* Remove actual base64 "content" from attachment
|
||||
*/
|
||||
if (!attachments) {
|
||||
return attachments;
|
||||
}
|
||||
return attachments.map((attachment) => {
|
||||
attachment = { ...attachment };
|
||||
attachment.content = '';
|
||||
return attachment;
|
||||
});
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
ChatWootAppConfig,
|
||||
ChatWootConfig,
|
||||
} from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { clearContent } from '@waha/apps/chatwoot/consumers/utils';
|
||||
|
||||
export function ListenEventsForChatWoot(config: ChatWootConfig) {
|
||||
const events = [
|
||||
@@ -319,6 +320,7 @@ export abstract class MessageBaseHandler<
|
||||
this.session,
|
||||
EngineHelper.ChatID(payload as any),
|
||||
this.l,
|
||||
EngineHelper.PhoneNumber(payload as any),
|
||||
);
|
||||
const conversation = await this.repo.ConversationByContact(contactInfo);
|
||||
this.info.onConversationId(conversation.conversationId);
|
||||
@@ -329,6 +331,8 @@ export abstract class MessageBaseHandler<
|
||||
`Created message as '${message.message_type}' from WhatsApp: ${response.id}`,
|
||||
);
|
||||
await this.saveMapping(response, payload);
|
||||
// Clear attachments content to avoid saving it in the task result
|
||||
message.attachments = clearContent(message.attachments);
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,21 @@ class JidContactInfo extends ChatContactInfo {
|
||||
* LID contact info
|
||||
*/
|
||||
class LidContactInfo extends ChatContactInfo {
|
||||
constructor(
|
||||
session: WAHASessionAPI,
|
||||
chatId: string,
|
||||
locale: Locale,
|
||||
private pn: string | null = null,
|
||||
) {
|
||||
super(session, chatId, locale);
|
||||
}
|
||||
|
||||
@CacheAsync()
|
||||
async jid() {
|
||||
const pn = await this.session.findPNByLid(this.chatId);
|
||||
let pn = this.pn;
|
||||
if (!pn) {
|
||||
pn = await this.session.findPNByLid(this.chatId);
|
||||
}
|
||||
if (!pn) {
|
||||
return null;
|
||||
}
|
||||
@@ -261,6 +273,7 @@ export function WhatsAppContactInfo(
|
||||
session: WAHASessionAPI,
|
||||
chatId: string,
|
||||
locale: Locale,
|
||||
pn: string | null = null,
|
||||
): ContactInfo {
|
||||
if (isJidGroup(chatId)) {
|
||||
return new GroupContactInfo(session, chatId, locale);
|
||||
@@ -271,7 +284,7 @@ export function WhatsAppContactInfo(
|
||||
} else if (isJidBroadcast(chatId)) {
|
||||
return new BroadcastContactInfo(session, chatId, locale);
|
||||
} else if (isLidUser(chatId)) {
|
||||
return new LidContactInfo(session, normalizeJid(chatId), locale);
|
||||
return new LidContactInfo(session, normalizeJid(chatId), locale, pn);
|
||||
} else if (isPnUser(chatId)) {
|
||||
return new JidContactInfo(session, chatId, locale);
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,12 @@ import { WAHAEngine } from '@waha/structures/enums.dto';
|
||||
import { getEngineName } from '@waha/version';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
import { Jid } from '@waha/core/engines/const';
|
||||
import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||
import {
|
||||
isLidUser,
|
||||
isPnUser,
|
||||
jidsFromKey,
|
||||
toCusFormat,
|
||||
} from '@waha/core/utils/jids';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
@@ -22,6 +27,12 @@ export interface QuotedMedia {
|
||||
interface IEngineHelper {
|
||||
ChatID(message: WAMessage | any): string;
|
||||
|
||||
/**
|
||||
* Phone number chat id (@c.us) of the DM contact extracted from the message payload
|
||||
* (alt jid fields), when the engine provides it. Null otherwise.
|
||||
*/
|
||||
PhoneNumber(message: WAMessage | any): string | null;
|
||||
|
||||
CallChatID(call: CallData | any): string;
|
||||
|
||||
IsReplyToStatus(
|
||||
@@ -49,6 +60,15 @@ class NOWEBHelper implements IEngineHelper {
|
||||
return message.from;
|
||||
}
|
||||
|
||||
PhoneNumber(message: WAMessage | any): string | null {
|
||||
const key = message._data?.key;
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
const jids = jidsFromKey(key);
|
||||
return pnChatIdOrNull(jids?.pn);
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
@@ -100,6 +120,21 @@ class GOWSHelper implements IEngineHelper {
|
||||
return message.from;
|
||||
}
|
||||
|
||||
PhoneNumber(message: WAMessage | any): string | null {
|
||||
const info = message._data?.Info;
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
// SenderAlt / RecipientAlt carry the phone number for @lid-addressed chats
|
||||
let pn: string | null;
|
||||
if (info.IsFromMe) {
|
||||
pn = info.RecipientAlt || null;
|
||||
} else {
|
||||
pn = info.SenderAlt || null;
|
||||
}
|
||||
return pnChatIdOrNull(pn);
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call._data?.CallCreator || call.from;
|
||||
}
|
||||
@@ -156,6 +191,11 @@ class WEBJSHelper implements IEngineHelper {
|
||||
return message._data?.id?.remote || message.from;
|
||||
}
|
||||
|
||||
PhoneNumber(message: WAMessage | any): string | null {
|
||||
void message;
|
||||
return null;
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
@@ -222,6 +262,11 @@ class WPPHelper implements IEngineHelper {
|
||||
return toCusFormat(parseMessageIdSerialized(message.id as any).remoteJid);
|
||||
}
|
||||
|
||||
PhoneNumber(message: WAMessage | any): string | null {
|
||||
void message;
|
||||
return null;
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
@@ -367,6 +412,17 @@ function extractBrowserQuotedMedia(replyData: any): QuotedMedia | null {
|
||||
};
|
||||
}
|
||||
|
||||
function pnChatIdOrNull(pn: string | null | undefined): string | null {
|
||||
if (!pn) {
|
||||
return null;
|
||||
}
|
||||
const chatId = toCusFormat(pn);
|
||||
if (isPnUser(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function preferPnChats(chats: string[]): string[] {
|
||||
const unique = lodash.uniq(chats ?? []);
|
||||
const hasPn = unique.some(isPnUser);
|
||||
|
||||
@@ -171,4 +171,41 @@ export class SessionTools extends McpController {
|
||||
url: `/api/sessions/${session}/restart`,
|
||||
});
|
||||
}
|
||||
|
||||
@Tool('sessions-capping', {
|
||||
title: 'Get message capping',
|
||||
description:
|
||||
'Fetch the account new-chat message capping (per-cycle quota).',
|
||||
inputSchema: SessionNameInput,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
},
|
||||
})
|
||||
async capping({ session }: z.infer<typeof SessionNameInput>) {
|
||||
return this.textRequest({
|
||||
method: 'GET',
|
||||
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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MessageCappingData } from '@waha/structures/sessions.dto';
|
||||
import * as lodash from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* WhatsApp new-chat message capping (per-cycle quota).
|
||||
* The volume counterpart of the reachout timelock: it caps how many new chats
|
||||
* the account may start per cycle. Keeps the latest known state and emits
|
||||
* changes. Unlike the timelock there is no client-side expiry - the cycle
|
||||
* resets on the server and the state is refreshed by re-fetching.
|
||||
*/
|
||||
export class MessageCappingTracker {
|
||||
private capping: MessageCappingData | null = null;
|
||||
private changes: Subject<MessageCappingData | null> = new Subject();
|
||||
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
get value(): MessageCappingData | null {
|
||||
return this.capping;
|
||||
}
|
||||
|
||||
get changes$(): Observable<MessageCappingData | null> {
|
||||
return this.changes;
|
||||
}
|
||||
|
||||
update(capping: MessageCappingData | null) {
|
||||
if (lodash.isEqual(this.capping, capping)) {
|
||||
return;
|
||||
}
|
||||
this.apply(capping);
|
||||
}
|
||||
|
||||
private apply(capping: MessageCappingData | null) {
|
||||
this.capping = capping;
|
||||
this.logger.info({ messageCapping: capping }, 'Message capping updated');
|
||||
this.changes.next(capping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ReachoutTimelockData } from '@waha/structures/sessions.dto';
|
||||
import { LongTimeout, setLongTimeout } from '@waha/utils/promiseTimeout';
|
||||
import { EnsureMilliseconds } from '@waha/utils/timehelper';
|
||||
import * as lodash from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* WhatsApp "reachout timelock"
|
||||
* The account is restricted from messaging new contacts for a period of time (the cause of 463 errors on send).
|
||||
* Keeps the latest known state, expires it when the enforcement ends and emits changes.
|
||||
*/
|
||||
export class ReachoutTimelockTracker {
|
||||
private timelock: ReachoutTimelockData | null = null;
|
||||
private timeout?: LongTimeout;
|
||||
private changes: Subject<ReachoutTimelockData | null> = new Subject();
|
||||
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
get value(): ReachoutTimelockData | null {
|
||||
return this.timelock;
|
||||
}
|
||||
|
||||
get changes$(): Observable<ReachoutTimelockData | null> {
|
||||
return this.changes;
|
||||
}
|
||||
|
||||
update(timelock: ReachoutTimelockData | null) {
|
||||
if (timelock && !timelock.isActive && !this.timelock) {
|
||||
// No enforcement has been seen for the account - keep it null instead of storing an inactive record
|
||||
// (startup fetch on a clean account lands here)
|
||||
return;
|
||||
}
|
||||
if (lodash.isEqual(this.timelock, timelock)) {
|
||||
return;
|
||||
}
|
||||
this.timeout?.clear();
|
||||
if (timelock?.isActive && timelock.timeEnforcementEnds) {
|
||||
const endsAtMs = EnsureMilliseconds(timelock.timeEnforcementEnds);
|
||||
const delay = Math.max(endsAtMs - Date.now(), 0);
|
||||
this.timeout = setLongTimeout(() => this.expire(), delay).unref();
|
||||
}
|
||||
this.apply(timelock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the expiry timer, keeping the last known state.
|
||||
*/
|
||||
stop() {
|
||||
this.timeout?.clear();
|
||||
}
|
||||
|
||||
private expire() {
|
||||
if (!this.timelock?.isActive) {
|
||||
return;
|
||||
}
|
||||
this.apply({ ...this.timelock, isActive: false });
|
||||
}
|
||||
|
||||
private apply(timelock: ReachoutTimelockData | null) {
|
||||
this.timelock = timelock;
|
||||
this.logger.info(
|
||||
{ reachoutTimelock: timelock },
|
||||
'Reachout timelock updated',
|
||||
);
|
||||
this.changes.next(timelock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
MessageCappingData,
|
||||
MessageCappingStatus,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
|
||||
function parseCappingTimestamp(value: any): number | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? null : EnsureSeconds(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the message capping payload (xwa2_message_capping_info) - the shape is the same
|
||||
* across engines: GOWS gRPC event, NOWEB Baileys fetch/push, WEBJS injected fetch/local read.
|
||||
*/
|
||||
export function parseMessageCapping(data: any): MessageCappingData {
|
||||
const cappingStatus = (data?.capping_status ??
|
||||
MessageCappingStatus.NONE) as MessageCappingStatus;
|
||||
return {
|
||||
cappingStatus: cappingStatus,
|
||||
// -1 total means "no cap"
|
||||
totalQuota: typeof data?.total_quota === 'number' ? data.total_quota : -1,
|
||||
usedQuota: typeof data?.used_quota === 'number' ? data.used_quota : 0,
|
||||
// cycle timestamps are unix seconds strings (or ms numbers from WEBJS local reads)
|
||||
cycleStart: parseCappingTimestamp(data?.cycle_start_timestamp),
|
||||
cycleEnd: parseCappingTimestamp(data?.cycle_end_timestamp),
|
||||
mvStatus: data?.mv_status ?? null,
|
||||
oteStatus: data?.ote_status ?? null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { WAHAEvents, WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { WASessionStatusBody } from '@waha/structures/webhooks.dto';
|
||||
|
||||
const logger: any = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
logger.child = () => logger;
|
||||
|
||||
const BaseSession = WhatsappSession as unknown as new (params: any) => any;
|
||||
|
||||
class TestSession extends BaseSession {
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
// pushName and id must be set so the SESSION_STATUS pipeline does not delay WORKING statuses
|
||||
return { id: '123@c.us', pushName: 'Test' };
|
||||
}
|
||||
|
||||
public setStatusPublic(status: WAHASessionStatus) {
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
public updateReachoutTimelockPublic(timelock: ReachoutTimelockData | null) {
|
||||
this.reachoutTimelock.update(timelock);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSession(): TestSession {
|
||||
return new TestSession({
|
||||
name: 'test',
|
||||
printQR: false,
|
||||
loggerBuilder: { child: () => logger },
|
||||
sessionStore: null,
|
||||
mediaManager: null,
|
||||
sessionConfig: null,
|
||||
engineConfig: null,
|
||||
ignore: {},
|
||||
});
|
||||
}
|
||||
|
||||
const ACTIVE_TIMELOCK: ReachoutTimelockData = {
|
||||
enforcementType: ReachoutTimelockEnforcementType.RESTRICT_ALL_COMPANIONS,
|
||||
isActive: true,
|
||||
timeEnforcementEnds: Math.floor(Date.now() / 1000) + 3600,
|
||||
};
|
||||
|
||||
describe('WhatsappSession reachout timelock', () => {
|
||||
let session: TestSession;
|
||||
let statuses: WASessionStatusBody[];
|
||||
|
||||
beforeEach(() => {
|
||||
session = buildSession();
|
||||
statuses = [];
|
||||
session
|
||||
.getEventObservable(WAHAEvents.SESSION_STATUS)
|
||||
.subscribe((body: WASessionStatusBody) => statuses.push(body));
|
||||
});
|
||||
|
||||
it('re-issues WORKING with data when a timelock arrives', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
expect(statuses[1].status).toEqual(WAHASessionStatus.WORKING);
|
||||
expect(statuses[1].data).toEqual({ reachoutTimelock: ACTIVE_TIMELOCK });
|
||||
});
|
||||
|
||||
it('keeps attaching the timelock on plain WORKING assignments', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
// Reconnect flows assign 'status = WORKING' with no data
|
||||
session.setStatusPublic(WAHASessionStatus.STARTING);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
|
||||
const last = statuses.at(-1);
|
||||
expect(last.status).toEqual(WAHASessionStatus.WORKING);
|
||||
expect(last.data).toEqual({ reachoutTimelock: ACTIVE_TIMELOCK });
|
||||
});
|
||||
|
||||
it('collapses consecutive WORKING statuses with the same data', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ignores duplicate timelock updates', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
session.updateReachoutTimelockPublic({ ...ACTIVE_TIMELOCK });
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ignores an inactive timelock when none has been seen', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
enforcementType: ReachoutTimelockEnforcementType.DEFAULT,
|
||||
isActive: false,
|
||||
timeEnforcementEnds: null,
|
||||
});
|
||||
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0].data).toBeNull();
|
||||
});
|
||||
|
||||
it('re-issues WORKING when the timelock is lifted by an event', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
const lifted = { ...ACTIVE_TIMELOCK, isActive: false };
|
||||
session.updateReachoutTimelockPublic(lifted);
|
||||
|
||||
expect(statuses).toHaveLength(3);
|
||||
expect(statuses[2].data).toEqual({ reachoutTimelock: lifted });
|
||||
});
|
||||
|
||||
it('marks the timelock inactive when the enforcement expires', () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const endsAt = Math.floor(Date.now() / 1000) + 600;
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
...ACTIVE_TIMELOCK,
|
||||
timeEnforcementEnds: endsAt,
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(601 * 1000);
|
||||
|
||||
const last = statuses.at(-1);
|
||||
expect(last.data.reachoutTimelock.isActive).toBe(false);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not fire the expiry timer after the session stops', () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
...ACTIVE_TIMELOCK,
|
||||
timeEnforcementEnds: Math.floor(Date.now() / 1000) + 600,
|
||||
});
|
||||
session.setStatusPublic(WAHASessionStatus.STOPPED);
|
||||
const count = statuses.length;
|
||||
|
||||
jest.advanceTimersByTime(601 * 1000);
|
||||
|
||||
expect(statuses).toHaveLength(count);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MessageCappingTracker } from '@waha/core/abc/MessageCappingTracker';
|
||||
import { ReachoutTimelockTracker } from '@waha/core/abc/ReachoutTimelockTracker';
|
||||
import { getBrowserExecutablePath as getBrowserExecutablePathAutodetect } from '@waha/core/abc/session.browser';
|
||||
import { IMediaConverter } from '@waha/core/media/IConverter';
|
||||
import { Ffmpeg } from '@waha/core/utils/ffmpeg';
|
||||
@@ -107,12 +109,16 @@ import {
|
||||
GroupParticipant,
|
||||
GroupsListFields,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '../../structures/groups.dto';
|
||||
import { WAHAChatPresences } from '../../structures/presence.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
MessageCappingData,
|
||||
MessageCappingStatus,
|
||||
ProxyConfig,
|
||||
ReachoutTimelockData,
|
||||
SessionConfig,
|
||||
} from '../../structures/sessions.dto';
|
||||
import {
|
||||
@@ -196,6 +202,8 @@ export abstract class WhatsappSession {
|
||||
|
||||
private _status: WAHASessionStatus;
|
||||
private _statusData: any = null;
|
||||
protected reachoutTimelock: ReachoutTimelockTracker;
|
||||
protected messageCapping: MessageCappingTracker;
|
||||
private _presence:
|
||||
| WAHAPresenceStatus.ONLINE
|
||||
| WAHAPresenceStatus.OFFLINE
|
||||
@@ -242,6 +250,24 @@ export abstract class WhatsappSession {
|
||||
this.loggerBuilder = loggerBuilder;
|
||||
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
|
||||
this.mediaConverter = new Ffmpeg(this.name, this.logger);
|
||||
this.reachoutTimelock = new ReachoutTimelockTracker(this.logger);
|
||||
this.reachoutTimelock.changes$.subscribe((timelock) => {
|
||||
if (this.status === WAHASessionStatus.WORKING) {
|
||||
// Re-issue WORKING so 'session.status' consumers get the update
|
||||
this.setStatus(WAHASessionStatus.WORKING, {
|
||||
reachoutTimelock: timelock,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.messageCapping = new MessageCappingTracker(this.logger);
|
||||
this.messageCapping.changes$.subscribe((capping) => {
|
||||
if (this.status === WAHASessionStatus.WORKING) {
|
||||
// Re-issue WORKING so 'session.status' consumers get the update
|
||||
this.setStatus(WAHASessionStatus.WORKING, {
|
||||
messageCapping: capping,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
|
||||
(key) =>
|
||||
new SwitchObservable((obs$) => {
|
||||
@@ -280,11 +306,12 @@ export abstract class WhatsappSession {
|
||||
}
|
||||
return of(update);
|
||||
}),
|
||||
// Remove consecutive duplicate WORKING statuses
|
||||
// Remove consecutive duplicate WORKING statuses, but let through WORKING re-issued with new data
|
||||
distinctUntilChanged(
|
||||
(prev, curr) =>
|
||||
prev.status === curr.status &&
|
||||
curr.status === WAHASessionStatus.WORKING,
|
||||
curr.status === WAHASessionStatus.WORKING &&
|
||||
lodash.isEqual(prev.data, curr.data),
|
||||
),
|
||||
// attach current time (ms)
|
||||
timestamp(),
|
||||
@@ -349,6 +376,27 @@ export abstract class WhatsappSession {
|
||||
// wait for STOPPED event, ignore the rest
|
||||
return;
|
||||
}
|
||||
if (status === WAHASessionStatus.WORKING && data == null) {
|
||||
// Plain 'status = WORKING' assignments (reconnects) must keep carrying the
|
||||
// active account-restriction info so 'session.status' consumers do not lose it
|
||||
const carry: any = {};
|
||||
if (this.reachoutTimelock.value?.isActive) {
|
||||
carry.reachoutTimelock = this.reachoutTimelock.value;
|
||||
}
|
||||
const capping = this.messageCapping.value;
|
||||
if (capping && capping.cappingStatus !== MessageCappingStatus.NONE) {
|
||||
carry.messageCapping = capping;
|
||||
}
|
||||
if (Object.keys(carry).length > 0) {
|
||||
data = carry;
|
||||
}
|
||||
}
|
||||
if (
|
||||
status === WAHASessionStatus.STOPPED ||
|
||||
status === WAHASessionStatus.FAILED
|
||||
) {
|
||||
this.reachoutTimelock?.stop();
|
||||
}
|
||||
this._status = status;
|
||||
this._statusData = data;
|
||||
this.status$.next({ status: status, data: data });
|
||||
@@ -479,6 +527,14 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public fetchMessageCapping(): Promise<MessageCappingData> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public fetchReachoutTimelock(): Promise<ReachoutTimelockData> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth methods
|
||||
*/
|
||||
@@ -1005,6 +1061,14 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public setMemberAddMode(id, value) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteGroup(id) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ApiKeyAuthMiddleware } from '@waha/core/auth/api-key-auth.middleware';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { NowebEngineConfigService } from '@waha/core/config/NowebEngineConfigService';
|
||||
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
@@ -205,6 +206,7 @@ export const PROVIDERS_BASE: Provider[] = [
|
||||
WebJSEngineConfigService,
|
||||
WPPEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
NowebEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { WAVersion } from '@adiwajshing/baileys';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NowebConfig } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import {
|
||||
formatWaVersion,
|
||||
isWaVersionHigher,
|
||||
parseWaVersion,
|
||||
} from '@waha/core/engines/noweb/waversion';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
export enum WAVersionAutoMode {
|
||||
WEB = 'auto-web',
|
||||
BAILEYS = 'auto-baileys',
|
||||
}
|
||||
|
||||
const FETCH_WA_VERSION_TIMEOUT_MS = 15_000;
|
||||
|
||||
@Injectable()
|
||||
export class NowebEngineConfigService {
|
||||
private config: NowebConfig | null = null;
|
||||
|
||||
constructor(
|
||||
protected configService: ConfigService,
|
||||
@InjectPinoLogger('NowebEngineConfigService')
|
||||
protected logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
async getConfig(): Promise<NowebConfig> {
|
||||
if (!this.config) {
|
||||
const version = await this.resolveWaVersion();
|
||||
this.config = { waVersion: version };
|
||||
}
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the WhatsApp Web version to use for NOWEB engine.
|
||||
* Returns the built-in Baileys version when WAHA_NOWEB_WA_VERSION is not set, invalid, can not be fetched
|
||||
* or is lower than the built-in one (unless WAHA_NOWEB_WA_VERSION_FORCE is set).
|
||||
*/
|
||||
private async resolveWaVersion(): Promise<WAVersion> {
|
||||
const env = this.configService.get<string>('WAHA_NOWEB_WA_VERSION', '');
|
||||
const wahaversion = esm.b.DEFAULT_CONNECTION_CONFIG.version as WAVersion;
|
||||
const force = parseBool(
|
||||
this.configService.get('WAHA_NOWEB_WA_VERSION_FORCE', 'false'),
|
||||
);
|
||||
const options = {
|
||||
signal: AbortSignal.timeout(FETCH_WA_VERSION_TIMEOUT_MS),
|
||||
};
|
||||
|
||||
let version: WAVersion | null = null;
|
||||
switch (env) {
|
||||
case WAVersionAutoMode.WEB: {
|
||||
const value = await esm.b.fetchLatestWaWebVersion(options);
|
||||
if (!value.isLatest) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION='${env}' - failed to fetch the latest WhatsApp Web version: ${value.error}`,
|
||||
);
|
||||
return wahaversion;
|
||||
}
|
||||
this.logger.info(
|
||||
`WAHA_NOWEB_WA_VERSION='${env}' - fetched the latest WhatsApp Web version: ${formatWaVersion(
|
||||
value.version,
|
||||
)}`,
|
||||
);
|
||||
version = value.version;
|
||||
break;
|
||||
}
|
||||
case WAVersionAutoMode.BAILEYS: {
|
||||
const value = await esm.b.fetchLatestBaileysVersion(options);
|
||||
if (!value.isLatest) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION='${env}' - failed to fetch the latest WhatsApp Web version: ${value.error}`,
|
||||
);
|
||||
return wahaversion;
|
||||
}
|
||||
this.logger.info(
|
||||
`WAHA_NOWEB_WA_VERSION='${env}' - fetched the latest WhatsApp Web version: ${formatWaVersion(
|
||||
value.version,
|
||||
)}`,
|
||||
);
|
||||
version = value.version;
|
||||
break;
|
||||
}
|
||||
case '':
|
||||
break;
|
||||
default:
|
||||
version = parseWaVersion(env);
|
||||
}
|
||||
|
||||
if (!version && env) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION='${env}' - invalid format, expected 'x.y.z', '${WAVersionAutoMode.WEB}' or '${WAVersionAutoMode.BAILEYS}'.`,
|
||||
);
|
||||
return wahaversion;
|
||||
}
|
||||
|
||||
if (!version && force) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION_FORCE is set, but WAHA_NOWEB_WA_VERSION is not.`,
|
||||
);
|
||||
return wahaversion;
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
return wahaversion;
|
||||
}
|
||||
|
||||
if (force && version) {
|
||||
this.logger.debug(
|
||||
`Using WAHA_NOWEB_WA_VERSION because of WAHA_NOWEB_WA_VERSION_FORCE.`,
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
if (isWaVersionHigher(wahaversion, version)) {
|
||||
this.logger.debug(
|
||||
`Built-in wa.version is higher than WAHA_NOWEB_WA_VERSION, using built-in version.`,
|
||||
);
|
||||
return wahaversion;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { promiseTimeout, sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { spawn } from 'child_process';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export class GowsSubprocess {
|
||||
private checkIntervalMs: number = 100;
|
||||
private readyDelayMs: number = 1_000;
|
||||
private stopTimeoutMs: number = 3_000;
|
||||
private readyText = 'gRPC server started!';
|
||||
|
||||
private child: any;
|
||||
@@ -96,7 +97,19 @@ export class GowsSubprocess {
|
||||
|
||||
async stop() {
|
||||
this.logger.info('Stopping GOWS subprocess...');
|
||||
this.child?.kill('SIGTERM');
|
||||
const child = this.child;
|
||||
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
const exited = new Promise<void>((resolve) => child.once('exit', resolve));
|
||||
child.kill('SIGTERM');
|
||||
// the process is detached, so if it ignores SIGTERM nothing else kills it - escalate to SIGKILL
|
||||
await promiseTimeout(this.stopTimeoutMs, exited).catch(() => {
|
||||
this.logger.warn(
|
||||
`GOWS did not exit in ${this.stopTimeoutMs}ms, sending SIGKILL`,
|
||||
);
|
||||
child.kill('SIGKILL');
|
||||
});
|
||||
this.logger.info('GOWS subprocess stopped');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GroupInfoFull,
|
||||
JoinedGroupEvent,
|
||||
} from '@waha/core/engines/gows/types.group';
|
||||
import { toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
import { isPnUser, toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
GroupId,
|
||||
GroupInfo,
|
||||
@@ -115,8 +115,10 @@ function getParticipants(jids: string[] | null, type: GroupParticipantType) {
|
||||
}
|
||||
const participants: GroupParticipant[] = [];
|
||||
for (const jid of jids) {
|
||||
const id = toCusFormat(jid);
|
||||
participants.push({
|
||||
id: toCusFormat(jid),
|
||||
id: id,
|
||||
pn: isPnUser(id) ? id : null,
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1423,7 +1423,7 @@ export namespace messages {
|
||||
}
|
||||
}
|
||||
export class SessionStorageConfig extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [[1], [2], [3], [4]];
|
||||
#one_of_decls: number[][] = [[1], [2], [3], [4], [5], [6]];
|
||||
constructor(data?: any[] | ({} & (({
|
||||
messages?: boolean;
|
||||
}) | ({
|
||||
@@ -1432,6 +1432,10 @@ export namespace messages {
|
||||
chats?: boolean;
|
||||
}) | ({
|
||||
labels?: boolean;
|
||||
}) | ({
|
||||
contacts?: boolean;
|
||||
}) | ({
|
||||
message_secrets?: boolean;
|
||||
})))) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -1448,6 +1452,12 @@ export namespace messages {
|
||||
if ("labels" in data && data.labels != undefined) {
|
||||
this.labels = data.labels;
|
||||
}
|
||||
if ("contacts" in data && data.contacts != undefined) {
|
||||
this.contacts = data.contacts;
|
||||
}
|
||||
if ("message_secrets" in data && data.message_secrets != undefined) {
|
||||
this.message_secrets = data.message_secrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
get messages() {
|
||||
@@ -1486,6 +1496,24 @@ export namespace messages {
|
||||
get has_labels() {
|
||||
return pb_1.Message.getField(this, 4) != null;
|
||||
}
|
||||
get contacts() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 5, false) as boolean;
|
||||
}
|
||||
set contacts(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 5, this.#one_of_decls[4], value);
|
||||
}
|
||||
get has_contacts() {
|
||||
return pb_1.Message.getField(this, 5) != null;
|
||||
}
|
||||
get message_secrets() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 6, false) as boolean;
|
||||
}
|
||||
set message_secrets(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 6, this.#one_of_decls[5], value);
|
||||
}
|
||||
get has_message_secrets() {
|
||||
return pb_1.Message.getField(this, 6) != null;
|
||||
}
|
||||
get _messages() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "messages";
|
||||
@@ -1522,11 +1550,31 @@ export namespace messages {
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [4])];
|
||||
}
|
||||
get _contacts() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "contacts";
|
||||
} = {
|
||||
0: "none",
|
||||
5: "contacts"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [5])];
|
||||
}
|
||||
get _message_secrets() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "message_secrets";
|
||||
} = {
|
||||
0: "none",
|
||||
6: "message_secrets"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [6])];
|
||||
}
|
||||
static fromObject(data: {
|
||||
messages?: boolean;
|
||||
groups?: boolean;
|
||||
chats?: boolean;
|
||||
labels?: boolean;
|
||||
contacts?: boolean;
|
||||
message_secrets?: boolean;
|
||||
}): SessionStorageConfig {
|
||||
const message = new SessionStorageConfig({});
|
||||
if (data.messages != null) {
|
||||
@@ -1541,6 +1589,12 @@ export namespace messages {
|
||||
if (data.labels != null) {
|
||||
message.labels = data.labels;
|
||||
}
|
||||
if (data.contacts != null) {
|
||||
message.contacts = data.contacts;
|
||||
}
|
||||
if (data.message_secrets != null) {
|
||||
message.message_secrets = data.message_secrets;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -1549,6 +1603,8 @@ export namespace messages {
|
||||
groups?: boolean;
|
||||
chats?: boolean;
|
||||
labels?: boolean;
|
||||
contacts?: boolean;
|
||||
message_secrets?: boolean;
|
||||
} = {};
|
||||
if (this.messages != null) {
|
||||
data.messages = this.messages;
|
||||
@@ -1562,6 +1618,12 @@ export namespace messages {
|
||||
if (this.labels != null) {
|
||||
data.labels = this.labels;
|
||||
}
|
||||
if (this.contacts != null) {
|
||||
data.contacts = this.contacts;
|
||||
}
|
||||
if (this.message_secrets != null) {
|
||||
data.message_secrets = this.message_secrets;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -1576,6 +1638,10 @@ export namespace messages {
|
||||
writer.writeBool(3, this.chats);
|
||||
if (this.has_labels)
|
||||
writer.writeBool(4, this.labels);
|
||||
if (this.has_contacts)
|
||||
writer.writeBool(5, this.contacts);
|
||||
if (this.has_message_secrets)
|
||||
writer.writeBool(6, this.message_secrets);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -1597,6 +1663,12 @@ export namespace messages {
|
||||
case 4:
|
||||
message.labels = reader.readBool();
|
||||
break;
|
||||
case 5:
|
||||
message.contacts = reader.readBool();
|
||||
break;
|
||||
case 6:
|
||||
message.message_secrets = reader.readBool();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -6396,6 +6468,7 @@ export namespace messages {
|
||||
phone?: string;
|
||||
jid?: string;
|
||||
registered?: boolean;
|
||||
pn?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -6409,6 +6482,9 @@ export namespace messages {
|
||||
if ("registered" in data && data.registered != undefined) {
|
||||
this.registered = data.registered;
|
||||
}
|
||||
if ("pn" in data && data.pn != undefined) {
|
||||
this.pn = data.pn;
|
||||
}
|
||||
}
|
||||
}
|
||||
get phone() {
|
||||
@@ -6429,10 +6505,17 @@ export namespace messages {
|
||||
set registered(value: boolean) {
|
||||
pb_1.Message.setField(this, 3, value);
|
||||
}
|
||||
get pn() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 4, "") as string;
|
||||
}
|
||||
set pn(value: string) {
|
||||
pb_1.Message.setField(this, 4, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
phone?: string;
|
||||
jid?: string;
|
||||
registered?: boolean;
|
||||
pn?: string;
|
||||
}): PhoneInfo {
|
||||
const message = new PhoneInfo({});
|
||||
if (data.phone != null) {
|
||||
@@ -6444,6 +6527,9 @@ export namespace messages {
|
||||
if (data.registered != null) {
|
||||
message.registered = data.registered;
|
||||
}
|
||||
if (data.pn != null) {
|
||||
message.pn = data.pn;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -6451,6 +6537,7 @@ export namespace messages {
|
||||
phone?: string;
|
||||
jid?: string;
|
||||
registered?: boolean;
|
||||
pn?: string;
|
||||
} = {};
|
||||
if (this.phone != null) {
|
||||
data.phone = this.phone;
|
||||
@@ -6461,6 +6548,9 @@ export namespace messages {
|
||||
if (this.registered != null) {
|
||||
data.registered = this.registered;
|
||||
}
|
||||
if (this.pn != null) {
|
||||
data.pn = this.pn;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -6473,6 +6563,8 @@ export namespace messages {
|
||||
writer.writeString(2, this.jid);
|
||||
if (this.registered != false)
|
||||
writer.writeBool(3, this.registered);
|
||||
if (this.pn.length)
|
||||
writer.writeString(4, this.pn);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -6491,6 +6583,9 @@ export namespace messages {
|
||||
case 3:
|
||||
message.registered = reader.readBool();
|
||||
break;
|
||||
case 4:
|
||||
message.pn = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -11362,6 +11457,15 @@ export namespace messages {
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
SetGroupMemberAddMode: {
|
||||
path: "/messages.MessageService/SetGroupMemberAddMode",
|
||||
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))
|
||||
},
|
||||
UpdateGroupParticipants: {
|
||||
path: "/messages.MessageService/UpdateGroupParticipants",
|
||||
requestStream: false,
|
||||
@@ -11425,6 +11529,24 @@ 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,
|
||||
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))
|
||||
},
|
||||
GenerateNewMessageID: {
|
||||
path: "/messages.MessageService/GenerateNewMessageID",
|
||||
requestStream: false,
|
||||
@@ -11734,6 +11856,7 @@ export namespace messages {
|
||||
abstract SetGroupPicture(call: grpc_1.ServerUnaryCall<SetPictureRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetGroupLocked(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
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 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;
|
||||
@@ -11741,6 +11864,8 @@ 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;
|
||||
abstract SendReaction(call: grpc_1.ServerUnaryCall<MessageReaction, MessageResponse>, callback: grpc_1.sendUnaryData<MessageResponse>): void;
|
||||
@@ -11861,6 +11986,9 @@ export namespace messages {
|
||||
SetGroupAnnounce: 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.SetGroupAnnounce(message, metadata, options, callback);
|
||||
};
|
||||
SetGroupMemberAddMode: 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.SetGroupMemberAddMode(message, metadata, options, callback);
|
||||
};
|
||||
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);
|
||||
};
|
||||
@@ -11882,6 +12010,12 @@ 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);
|
||||
};
|
||||
GenerateNewMessageID: GrpcUnaryServiceInterface<Session, NewMessageIDResponse> = (message: Session, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<NewMessageIDResponse>, options?: grpc_1.CallOptions | grpc_1.requestCallback<NewMessageIDResponse>, callback?: grpc_1.requestCallback<NewMessageIDResponse>): grpc_1.ClientUnaryCall => {
|
||||
return super.GenerateNewMessageID(message, metadata, options, callback);
|
||||
};
|
||||
|
||||
@@ -1050,6 +1050,18 @@ setGroupAnnounce: {
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
// send messages only by admins
|
||||
setGroupMemberAddMode: {
|
||||
path: '/messages.MessageService/SetGroupMemberAddMode',
|
||||
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,
|
||||
},
|
||||
// who can add members - true - all members, false - admins only
|
||||
updateGroupParticipants: {
|
||||
path: '/messages.MessageService/UpdateGroupParticipants',
|
||||
requestStream: false,
|
||||
@@ -1131,6 +1143,33 @@ getProfilePicture: {
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
//
|
||||
// Account
|
||||
//
|
||||
// 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,
|
||||
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,
|
||||
},
|
||||
//
|
||||
// Message
|
||||
//
|
||||
generateNewMessageID: {
|
||||
|
||||
@@ -4816,7 +4816,9 @@ proto.messages.SessionStorageConfig.toObject = function(includeInstance, msg) {
|
||||
messages: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
|
||||
groups: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
|
||||
chats: jspb.Message.getBooleanFieldWithDefault(msg, 3, false),
|
||||
labels: jspb.Message.getBooleanFieldWithDefault(msg, 4, false)
|
||||
labels: jspb.Message.getBooleanFieldWithDefault(msg, 4, false),
|
||||
contacts: jspb.Message.getBooleanFieldWithDefault(msg, 5, false),
|
||||
messageSecrets: jspb.Message.getBooleanFieldWithDefault(msg, 6, false)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -4869,6 +4871,14 @@ proto.messages.SessionStorageConfig.deserializeBinaryFromReader = function(msg,
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setLabels(value);
|
||||
break;
|
||||
case 5:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setContacts(value);
|
||||
break;
|
||||
case 6:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setMessageSecrets(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -4926,6 +4936,20 @@ proto.messages.SessionStorageConfig.serializeBinaryToWriter = function(message,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 5));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 6));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
6,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5073,6 +5097,78 @@ proto.messages.SessionStorageConfig.prototype.hasLabels = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool contacts = 5;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getContacts = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 5, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setContacts = function(value) {
|
||||
return jspb.Message.setField(this, 5, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearContacts = function() {
|
||||
return jspb.Message.setField(this, 5, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasContacts = function() {
|
||||
return jspb.Message.getField(this, 5) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool message_secrets = 6;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getMessageSecrets = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 6, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setMessageSecrets = function(value) {
|
||||
return jspb.Message.setField(this, 6, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearMessageSecrets = function() {
|
||||
return jspb.Message.setField(this, 6, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasMessageSecrets = function() {
|
||||
return jspb.Message.getField(this, 6) != null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13869,7 +13965,8 @@ proto.messages.PhoneInfo.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
phone: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
jid: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
registered: jspb.Message.getBooleanFieldWithDefault(msg, 3, false)
|
||||
registered: jspb.Message.getBooleanFieldWithDefault(msg, 3, false),
|
||||
pn: jspb.Message.getFieldWithDefault(msg, 4, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -13918,6 +14015,10 @@ proto.messages.PhoneInfo.deserializeBinaryFromReader = function(msg, reader) {
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setRegistered(value);
|
||||
break;
|
||||
case 4:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setPn(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -13968,6 +14069,13 @@ proto.messages.PhoneInfo.serializeBinaryToWriter = function(message, writer) {
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getPn();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
4,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14025,6 +14133,24 @@ proto.messages.PhoneInfo.prototype.setRegistered = function(value) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string pn = 4;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.PhoneInfo.prototype.getPn = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.PhoneInfo} returns this
|
||||
*/
|
||||
proto.messages.PhoneInfo.prototype.setPn = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 4, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List of repeated fields within this message type.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { parseGowsReachoutTimelock } from '@waha/core/engines/gows/reachouttimelock';
|
||||
|
||||
describe('parseGowsReachoutTimelock', () => {
|
||||
it('parses a full payload', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
is_active: true,
|
||||
time_enforcement_ends: '1784477333',
|
||||
});
|
||||
expect(timelock).toEqual({
|
||||
enforcementType: 'RESTRICT_ALL_COMPANIONS',
|
||||
isActive: true,
|
||||
timeEnforcementEnds: 1784477333,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats absent is_active as inactive (omitempty)', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
});
|
||||
expect(timelock.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('treats absent time_enforcement_ends as null (omitzero)', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
is_active: true,
|
||||
});
|
||||
expect(timelock.timeEnforcementEnds).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults enforcement type on an empty payload', () => {
|
||||
const timelock = parseGowsReachoutTimelock({});
|
||||
expect(timelock).toEqual({
|
||||
enforcementType: 'DEFAULT',
|
||||
isActive: false,
|
||||
timeEnforcementEnds: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for a non numeric time_enforcement_ends', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
is_active: true,
|
||||
time_enforcement_ends: 'not-a-number',
|
||||
});
|
||||
expect(timelock.timeEnforcementEnds).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
|
||||
/**
|
||||
* Normalize whatsmeow's events.NotifyAccountReachoutTimelock payload.
|
||||
*/
|
||||
export function parseGowsReachoutTimelock(data: any): ReachoutTimelockData {
|
||||
// 'time_enforcement_ends' is a unix seconds string, absent when zero (Go's omitzero)
|
||||
let timeEnforcementEnds: number | null = null;
|
||||
if (data?.time_enforcement_ends) {
|
||||
const parsed = parseInt(data.time_enforcement_ends, 10);
|
||||
timeEnforcementEnds = Number.isNaN(parsed) ? null : EnsureSeconds(parsed);
|
||||
}
|
||||
const enforcementType =
|
||||
data?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT;
|
||||
return {
|
||||
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
|
||||
// 'is_active' is absent when false (Go's omitempty)
|
||||
isActive: data?.is_active === true,
|
||||
timeEnforcementEnds: timeEnforcementEnds,
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
parseJsonList,
|
||||
statusToAck,
|
||||
} from '@waha/core/engines/gows/helpers';
|
||||
import { parseMessageCapping } from '@waha/core/abc/capping';
|
||||
import { parseGowsReachoutTimelock } from '@waha/core/engines/gows/reachouttimelock';
|
||||
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
|
||||
import {
|
||||
extractBody,
|
||||
@@ -118,6 +120,7 @@ import {
|
||||
GroupSortField,
|
||||
Participant,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
@@ -134,7 +137,9 @@ import {
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
MessageCappingData,
|
||||
ProxyConfig,
|
||||
ReachoutTimelockData,
|
||||
SessionConfig,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
@@ -220,6 +225,8 @@ function getGowsStorageConfig(
|
||||
groups: storeConfig?.groups !== false,
|
||||
chats: storeConfig?.chats !== false,
|
||||
labels: storeConfig?.labels !== false,
|
||||
contacts: storeConfig?.contacts !== false,
|
||||
message_secrets: storeConfig?.messageSecrets !== false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,6 +242,8 @@ enum WhatsMeowEvent {
|
||||
CHAT_PRESENCE = 'events.ChatPresence',
|
||||
PUSH_NAME_SETTING = 'events.PushNameSetting',
|
||||
LOGGED_OUT = 'events.LoggedOut',
|
||||
NOTIFY_ACCOUNT_REACHOUT_TIMELOCK = 'events.NotifyAccountReachoutTimelock',
|
||||
MESSAGE_CAPPING = 'gows.MessageCapping',
|
||||
// Groups
|
||||
GROUP_INFO = 'events.GroupInfo',
|
||||
JOINED_GROUP = 'events.JoinedGroup',
|
||||
@@ -473,6 +482,12 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
this.logger.error('Logged out');
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
});
|
||||
events.on(WhatsMeowEvent.NOTIFY_ACCOUNT_REACHOUT_TIMELOCK, (data) => {
|
||||
this.reachoutTimelock.update(parseGowsReachoutTimelock(data));
|
||||
});
|
||||
events.on(WhatsMeowEvent.MESSAGE_CAPPING, (data) => {
|
||||
this.messageCapping.update(parseMessageCapping(data));
|
||||
});
|
||||
events.on(WhatsMeowEvent.PRESENCE, (event: gows.Presence) => {
|
||||
if (isJidGroup(event.From)) {
|
||||
// So group is not "online"
|
||||
@@ -918,7 +933,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
return this.me;
|
||||
if (!this.me) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...this.me,
|
||||
reachoutTimelock: this.reachoutTimelock.value,
|
||||
messageCapping: this.messageCapping.value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1255,6 +1277,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return {
|
||||
numberExists: info?.registered || false,
|
||||
chatId: toCusFormat(info?.jid || null),
|
||||
pn: toCusFormat(info?.pn || null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1642,6 +1665,24 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return;
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.getGroup(id);
|
||||
return {
|
||||
membersCanAddNewMember: group.MemberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const req = new messages.JidBoolRequest({
|
||||
session: this.session,
|
||||
jid: id,
|
||||
value: value,
|
||||
});
|
||||
await promisify(this.client.SetGroupMemberAddMode)(req);
|
||||
return;
|
||||
}
|
||||
|
||||
public deleteGroup(id) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -2248,6 +2289,28 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return this.toWAContact(data);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async fetchMessageCapping(): Promise<MessageCappingData> {
|
||||
const response = await promisify(this.client.FetchMessageCapping)(
|
||||
this.session,
|
||||
);
|
||||
const capping = parseMessageCapping(parseJson(response));
|
||||
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
|
||||
this.messageCapping.update(capping);
|
||||
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,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
GroupV2ParticipantsEvent,
|
||||
GroupV2UpdateEvent,
|
||||
} from '@waha/structures/groups.events.dto';
|
||||
import { toCusFormat } from '@waha/core/utils/jids';
|
||||
import { isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||
import esm from '@waha/vendor/esm';
|
||||
|
||||
export function ToGroupInfo(group: Partial<GroupMetadata>): GroupInfo {
|
||||
@@ -68,7 +68,7 @@ interface GroupParticipantUpdate {
|
||||
action: ParticipantAction;
|
||||
}
|
||||
|
||||
function getParticipantId(
|
||||
export function getParticipantId(
|
||||
participant: string | NOWEBGroupParticipant,
|
||||
): string | undefined {
|
||||
if (typeof participant === 'string') {
|
||||
@@ -77,6 +77,39 @@ function getParticipantId(
|
||||
return participant?.id;
|
||||
}
|
||||
|
||||
function getParticipantPn(
|
||||
participant: string | NOWEBGroupParticipant,
|
||||
): string | null {
|
||||
if (typeof participant === 'string') {
|
||||
return isPnUser(participant) ? participant : null;
|
||||
}
|
||||
return participant?.phoneNumber || null;
|
||||
}
|
||||
|
||||
function getParticipantIds(
|
||||
participant: string | NOWEBGroupParticipant,
|
||||
): string[] {
|
||||
if (typeof participant === 'string') {
|
||||
return [participant];
|
||||
}
|
||||
return [participant?.id, participant?.phoneNumber].filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if me.id or me.lid in participant list
|
||||
*/
|
||||
export function participantsIncludeMe(
|
||||
me: Contact,
|
||||
participants: Array<string | NOWEBGroupParticipant>,
|
||||
): boolean {
|
||||
const myIds = [me.id, me.lid].filter(Boolean);
|
||||
return participants.some((participant) =>
|
||||
getParticipantIds(participant).some((id) =>
|
||||
myIds.some((meId) => esm.b.areJidsSameUser(id, meId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function ToGroupV2Participants(
|
||||
update: GroupParticipantUpdate,
|
||||
): GroupV2ParticipantsEvent {
|
||||
@@ -105,6 +138,7 @@ export function ToGroupV2Participants(
|
||||
const id = getParticipantId(item);
|
||||
return {
|
||||
id: toCusFormat(id),
|
||||
pn: toCusFormat(getParticipantPn(item)),
|
||||
role: role,
|
||||
};
|
||||
});
|
||||
@@ -140,12 +174,7 @@ export function ToGroupV2LeaveEvent(
|
||||
if (!me) {
|
||||
return null;
|
||||
}
|
||||
const meId = esm.b.jidNormalizedUser(me.id);
|
||||
const includesMe = update.participants.some((participant) => {
|
||||
const id = getParticipantId(participant);
|
||||
return id === meId;
|
||||
});
|
||||
if (!includesMe) {
|
||||
if (!participantsIncludeMe(me, update.participants)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import makeWASocket, {
|
||||
WAMessageContent,
|
||||
WAMessageKey,
|
||||
WAMessageUpdate,
|
||||
WAVersion,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
|
||||
import { BaileysEventMap } from '@adiwajshing/baileys/lib/Types/Events';
|
||||
@@ -43,6 +44,7 @@ import type {
|
||||
import { ILogger } from '@adiwajshing/baileys/lib/Utils/logger';
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { parseMessageCapping } from '@waha/core/abc/capping';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
getPublicUrlFromDirectPath,
|
||||
@@ -73,7 +75,12 @@ import { AckToStatus, StatusToAck } from '@waha/core/utils/acks';
|
||||
import { pairs } from '@waha/utils/pairs';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { isJidNewsletter, toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
isJidNewsletter,
|
||||
jidsFromKey,
|
||||
toCusFormat,
|
||||
toJID,
|
||||
} from '@waha/core/utils/jids';
|
||||
import { DistinctAck, DistinctMessages } from '@waha/core/utils/reactive';
|
||||
import {
|
||||
flipObject,
|
||||
@@ -149,6 +156,7 @@ import {
|
||||
CreateGroupRequest,
|
||||
GroupParticipant,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
@@ -166,7 +174,13 @@ import {
|
||||
WAHAPresenceData,
|
||||
} from '@waha/structures/presence.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
MessageCappingData,
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
@@ -185,7 +199,7 @@ import {
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { promiseTimeout, sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { exclude } from '@waha/utils/reactive/ops/exclude';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
|
||||
@@ -235,6 +249,7 @@ import { detectMimetype } from '@waha/utils/files';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import axios from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { formatWaVersion } from '@waha/core/engines/noweb/waversion';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
@@ -259,11 +274,18 @@ const PresenceStatuses = {
|
||||
};
|
||||
const ToEnginePresenceStatus = flipObject(PresenceStatuses);
|
||||
|
||||
export interface NowebConfig {
|
||||
waVersion: WAVersion;
|
||||
}
|
||||
|
||||
export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
private AUTO_RESTART_AFTER_SECONDS = 28 * 60;
|
||||
// how long to wait on stop for the WebSocket close handshake and store close before forcing it
|
||||
private CLOSE_TIMEOUT_MS = 3_000;
|
||||
|
||||
engine = WAHAEngine.NOWEB;
|
||||
protected engineConfig: NowebConfig;
|
||||
authFactory = new NowebAuthFactoryCore();
|
||||
storageFactory = new NowebStorageFactoryCore();
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
@@ -381,7 +403,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (markOnlineOnConnect == undefined) {
|
||||
markOnlineOnConnect = true;
|
||||
}
|
||||
const version = this.engineConfig?.waVersion;
|
||||
return {
|
||||
version: version,
|
||||
agent: agents?.socket,
|
||||
// Baileys media upload uses Node https.request in Node runtime.
|
||||
fetchAgent: agents?.fetch as Agent,
|
||||
@@ -419,6 +443,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
agents,
|
||||
state,
|
||||
) as SocketConfig;
|
||||
this.logger.info(
|
||||
`Connecting using wa.version = ${formatWaVersion(socketConfig.version)}`,
|
||||
);
|
||||
const sock = makeWASocket(socketConfig);
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
return sock;
|
||||
@@ -540,13 +567,33 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
this.logger.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
|
||||
this.sock.ev.on('message-capping.update', (data) => {
|
||||
this.messageCapping.update(parseMessageCapping(data));
|
||||
});
|
||||
this.sock.ev.on('connection.update', async (update) => {
|
||||
const { connection, lastDisconnect, qr, isNewLogin } = update;
|
||||
if (update.reachoutTimeLock) {
|
||||
this.updateReachoutTimelockFromState(update.reachoutTimeLock);
|
||||
}
|
||||
if (isNewLogin) {
|
||||
this.restartClient();
|
||||
} else if (connection === 'open') {
|
||||
this.qr.save('');
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
// Ask WhatsApp for the current reachout timelock state, so a restarted session learns about
|
||||
// an ongoing timelock without waiting for a push. The result arrives via 'connection.update'
|
||||
this.sock?.fetchAccountReachoutTimelock?.().catch((error) => {
|
||||
this.logger.warn(`Failed to fetch reachout timelock: ${error}`);
|
||||
});
|
||||
// Same for the new-chat message capping - there is no push on (re)connect, only on changes
|
||||
this.sock
|
||||
?.fetchNewChatMessageCap?.()
|
||||
.then((data) => {
|
||||
this.messageCapping.update(parseMessageCapping(data));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.warn(`Failed to fetch message capping: ${error}`);
|
||||
});
|
||||
// Do we need to resubscribe?
|
||||
// Ideally not, we need to explicitly call interesting
|
||||
// jids every 1 minute
|
||||
@@ -624,16 +671,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
});
|
||||
this.logger.info('Creds saved');
|
||||
}
|
||||
await this.end();
|
||||
await this.closeStores();
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.stopEvents();
|
||||
|
||||
this.mediaManager.close();
|
||||
await this.end();
|
||||
await this.store?.close();
|
||||
this.authNOWEBStore?.close().catch((err) => {
|
||||
this.logger.error('Failed to close NOWEB auth store');
|
||||
this.logger.error(err, err.stack);
|
||||
});
|
||||
}
|
||||
|
||||
protected async failed() {
|
||||
@@ -649,8 +691,34 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
await sleep(1_000);
|
||||
}
|
||||
|
||||
this.stopEvents();
|
||||
this.mediaManager.close();
|
||||
await this.end();
|
||||
await this.store?.close();
|
||||
await this.closeStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the data and auth stores with a time bound - close() may flush pending writes,
|
||||
* and broken storage must not block session stop or process shutdown
|
||||
*/
|
||||
private async closeStores() {
|
||||
if (this.store) {
|
||||
await promiseTimeout(this.CLOSE_TIMEOUT_MS, this.store.close()).catch(
|
||||
(err) => {
|
||||
this.logger.error('Failed to close NOWEB store');
|
||||
this.logger.error(err, err.stack);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (this.authNOWEBStore) {
|
||||
await promiseTimeout(
|
||||
this.CLOSE_TIMEOUT_MS,
|
||||
this.authNOWEBStore.close(),
|
||||
).catch((err) => {
|
||||
this.logger.error('Failed to close NOWEB auth store');
|
||||
this.logger.error(err, err.stack);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private fixMessages() {
|
||||
@@ -830,13 +898,41 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.cleanupPresenceTimeout();
|
||||
this.presence = null;
|
||||
this.autoRestartJob.stop();
|
||||
const sock = this.sock;
|
||||
// @ts-ignore
|
||||
this.sock?.ev?.removeAllListeners();
|
||||
this.sock?.ws?.removeAllListeners();
|
||||
sock?.ev?.removeAllListeners();
|
||||
sock?.ws?.removeAllListeners();
|
||||
// wait until connection is not connecting to avoid error:
|
||||
// "WebSocket was closed before the connection was established"
|
||||
await waitUntil(async () => !this.sock?.ws?.isConnecting, 1_000, 10_000);
|
||||
this.sock?.end(undefined);
|
||||
await waitUntil(async () => !sock?.ws?.isConnecting, 1_000, 10_000);
|
||||
if (!sock) {
|
||||
return;
|
||||
}
|
||||
// sock.end() waits for the WebSocket close handshake - on a dead or already closed connection
|
||||
// it can hang forever, so bound it and destroy the raw TCP socket to let the process exit
|
||||
const closing = sock.end(undefined);
|
||||
try {
|
||||
await promiseTimeout(this.CLOSE_TIMEOUT_MS, closing);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`WebSocket did not close in ${this.CLOSE_TIMEOUT_MS}ms, terminating it: ${err}`,
|
||||
);
|
||||
this.terminate(sock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the raw TCP socket behind Baileys WebSocket wrapper.
|
||||
* ws.terminate() skips the close handshake, so it works even on half-open connections.
|
||||
*/
|
||||
private terminate(sock: ReturnType<typeof makeWASocket>) {
|
||||
// 'socket' is protected on Baileys WebSocketClient, reach it at runtime
|
||||
const raw = (sock.ws as any)?.socket;
|
||||
try {
|
||||
raw?.terminate?.();
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to terminate WebSocket: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
@@ -849,6 +945,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
id: toCusFormat(meId),
|
||||
pushName: me.name,
|
||||
lid: jidNormalizedUser(me.lid),
|
||||
reachoutTimelock: this.reachoutTimelock.value,
|
||||
messageCapping: this.messageCapping.value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -939,6 +1037,39 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async fetchMessageCapping(): Promise<MessageCappingData> {
|
||||
const data = await this.sock.fetchNewChatMessageCap();
|
||||
const capping = parseMessageCapping(data);
|
||||
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
|
||||
this.messageCapping.update(capping);
|
||||
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
|
||||
*/
|
||||
@@ -1205,12 +1336,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
urlInfo.jpegThumbnail = thumbnail.buffer;
|
||||
} else {
|
||||
// upload HQ thumbnail
|
||||
// newsletters need the thumbnail uploaded unencrypted (plaintext path),
|
||||
// otherwise clients can not render it (white/blurred preview image)
|
||||
const uploadToServer = this.sock.waUploadToServer;
|
||||
const uploadThumbnail = async (encFilePath, opts) => {
|
||||
opts.newsletter = isJidNewsletter(chatId);
|
||||
return await uploadToServer(encFilePath, opts);
|
||||
};
|
||||
const { imageMessage } = await esm.b.prepareWAMessageMedia(
|
||||
{ image: content },
|
||||
{
|
||||
upload: this.sock.waUploadToServer,
|
||||
upload: uploadThumbnail,
|
||||
mediaTypeOverride: 'thumbnail-link',
|
||||
options: { signal: AbortSignal.timeout(10_000) },
|
||||
jid: chatId,
|
||||
},
|
||||
);
|
||||
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
||||
@@ -1936,6 +2075,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return await this.sock.groupSettingUpdate(id, setting);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.getGroup(id);
|
||||
return { membersCanAddNewMember: group.memberAddMode };
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const mode = value ? 'all_member_add' : 'admin_add';
|
||||
return await this.sock.groupMemberAddMode(id, mode);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async leaveGroup(id) {
|
||||
return this.sock.groupLeave(id);
|
||||
@@ -2882,9 +3032,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (!origMsgId) {
|
||||
return '';
|
||||
}
|
||||
const jidsToTry = [targetKey.remoteJid, editMessage.key?.remoteJid].filter(
|
||||
Boolean,
|
||||
);
|
||||
const editKey = editMessage.key as WAMessageKey | undefined;
|
||||
const jidsToTry = [
|
||||
targetKey.remoteJid,
|
||||
editKey?.remoteJid,
|
||||
editKey?.remoteJidAlt,
|
||||
].filter(Boolean);
|
||||
let stored: proto.IWebMessageInfo | undefined;
|
||||
for (const jid of jidsToTry) {
|
||||
stored = await this.store?.loadMessage(jid, origMsgId);
|
||||
@@ -2915,40 +3068,75 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (!encPayload || !encIv) {
|
||||
return '';
|
||||
}
|
||||
const editInfo = {
|
||||
Chat: editMessage.key?.remoteJid,
|
||||
Sender:
|
||||
editMessage.key?.participant ||
|
||||
(editMessage.key?.fromMe ? undefined : editMessage.key?.remoteJid),
|
||||
};
|
||||
const modificationSenderJid = jidToNonAD(editInfo.Sender || '');
|
||||
const primaryOrig = getOrigSenderJidForMsgSecret(editInfo, {
|
||||
fromMe: targetKey.fromMe,
|
||||
remoteJID: targetKey.remoteJid,
|
||||
participant: targetKey.participant,
|
||||
});
|
||||
const candidates: string[] = [primaryOrig];
|
||||
|
||||
// The editor ("modification sender").
|
||||
let modificationSenderJids: Array<string | null | undefined>;
|
||||
if (editKey?.fromMe) {
|
||||
// Try both user lid and c.us
|
||||
modificationSenderJids = [this.sock?.user?.lid, this.sock?.user?.id];
|
||||
} else {
|
||||
const editJids = editKey ? jidsFromKey(editKey) : null;
|
||||
modificationSenderJids = [
|
||||
editKey?.participant || editKey?.remoteJid,
|
||||
editJids?.lid,
|
||||
editJids?.pn,
|
||||
];
|
||||
}
|
||||
modificationSenderJids = lodash
|
||||
.chain(modificationSenderJids)
|
||||
.filter(Boolean)
|
||||
.map(jidToNonAD)
|
||||
.uniq()
|
||||
.value();
|
||||
if (modificationSenderJids.length === 0) {
|
||||
modificationSenderJids.push('');
|
||||
}
|
||||
|
||||
const remoteNonAD = targetKey.remoteJid
|
||||
? jidToNonAD(targetKey.remoteJid)
|
||||
: '';
|
||||
if (remoteNonAD && !candidates.includes(remoteNonAD)) {
|
||||
candidates.push(remoteNonAD);
|
||||
}
|
||||
const participantNonAD = targetKey.participant
|
||||
? jidToNonAD(targetKey.participant)
|
||||
: '';
|
||||
if (participantNonAD && !candidates.includes(participantNonAD)) {
|
||||
candidates.push(participantNonAD);
|
||||
|
||||
// Map dedupes by key and keeps the first-insertion order
|
||||
const attempts = new Map<
|
||||
string,
|
||||
{ origSenderJid: string; modificationSenderJid: string }
|
||||
>();
|
||||
for (const modificationSenderJid of modificationSenderJids) {
|
||||
const primaryOrigSenderJid = getOrigSenderJidForMsgSecret(
|
||||
{ Chat: editKey?.remoteJid, Sender: modificationSenderJid },
|
||||
{
|
||||
fromMe: targetKey.fromMe,
|
||||
remoteJID: targetKey.remoteJid,
|
||||
participant: targetKey.participant,
|
||||
},
|
||||
);
|
||||
const origSenderJids = [
|
||||
primaryOrigSenderJid,
|
||||
remoteNonAD,
|
||||
participantNonAD,
|
||||
].filter(Boolean);
|
||||
for (const origSenderJid of origSenderJids) {
|
||||
// Avoid duplicates in attemps
|
||||
const attemptKey = `${origSenderJid}|${modificationSenderJid}`;
|
||||
attempts.set(attemptKey, {
|
||||
origSenderJid: origSenderJid,
|
||||
modificationSenderJid: modificationSenderJid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let lastErr: unknown;
|
||||
for (const origSenderJid of candidates) {
|
||||
for (const attempt of attempts.values()) {
|
||||
try {
|
||||
const decoded = decryptSecretEncryptedMessageEditProto({
|
||||
encPayload: encPayload,
|
||||
encIv: encIv,
|
||||
origMsgId: origMsgId,
|
||||
origSenderJid: origSenderJid,
|
||||
modificationSenderJid: modificationSenderJid,
|
||||
origSenderJid: attempt.origSenderJid,
|
||||
modificationSenderJid: attempt.modificationSenderJid,
|
||||
origMsgSecret: origSecret,
|
||||
});
|
||||
const text = extractBody(decoded) || '';
|
||||
@@ -2960,7 +3148,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
this.logger.debug(
|
||||
{ err: lastErr, origMsgId: origMsgId, candidates: candidates },
|
||||
{
|
||||
err: lastErr,
|
||||
origMsgId: origMsgId,
|
||||
attempts: Array.from(attempts.keys()),
|
||||
},
|
||||
'NOWEB message edit decrypt: AES-GCM or protobuf decode failed',
|
||||
);
|
||||
return '';
|
||||
|
||||
@@ -11,6 +11,10 @@ import type {
|
||||
import type { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import type { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import {
|
||||
getParticipantId,
|
||||
participantsIncludeMe,
|
||||
} from '@waha/core/engines/noweb/groups.noweb';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
@@ -380,16 +384,13 @@ export class NowebPersistentStore implements INowebStore {
|
||||
if (!this.jids.include(id)) {
|
||||
return;
|
||||
}
|
||||
const participants: string[] = data.participants;
|
||||
const participants: Array<string | GroupParticipant> = data.participants;
|
||||
const action: ParticipantAction = data.action;
|
||||
|
||||
if (action == 'remove') {
|
||||
// Remove the group if the current user is removed
|
||||
const myJid = this.socket?.authState?.creds?.me?.id;
|
||||
const participantsIncludesMe = lodash.find(participants, (p) =>
|
||||
esm.b.areJidsSameUser(p, myJid),
|
||||
);
|
||||
if (participantsIncludesMe) {
|
||||
const me = this.socket?.authState?.creds?.me;
|
||||
if (me && participantsIncludeMe(me, participants)) {
|
||||
await this.groupRepo.deleteById(id);
|
||||
return;
|
||||
}
|
||||
@@ -419,24 +420,33 @@ export class NowebPersistentStore implements INowebStore {
|
||||
|
||||
private participantUpdate(
|
||||
participantsById: DefaultMap<string, GroupParticipant>,
|
||||
participant: string,
|
||||
participant: string | GroupParticipant,
|
||||
action: ParticipantAction,
|
||||
) {
|
||||
const participantId = getParticipantId(participant);
|
||||
if (!participantId) {
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case 'add':
|
||||
// if there's no participant - add it (by id)
|
||||
participantsById.get(participant);
|
||||
if (typeof participant === 'string') {
|
||||
// if there's no participant - add it (by id)
|
||||
participantsById.get(participantId);
|
||||
} else {
|
||||
// keep the full object - it carries 'phoneNumber' and 'admin' fields
|
||||
participantsById.set(participantId, participant);
|
||||
}
|
||||
break;
|
||||
case 'remove':
|
||||
// remove the participant (by id)
|
||||
participantsById.delete(participant);
|
||||
participantsById.delete(participantId);
|
||||
break;
|
||||
case 'promote':
|
||||
// set admin: admin
|
||||
participantsById.get(participant).admin = 'admin';
|
||||
participantsById.get(participantId).admin = 'admin';
|
||||
break;
|
||||
case 'demote':
|
||||
participantsById.get(participant).admin = null;
|
||||
participantsById.get(participantId).admin = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
formatWaVersion,
|
||||
isWaVersionHigher,
|
||||
parseWaVersion,
|
||||
} from '@waha/core/engines/noweb/waversion';
|
||||
|
||||
describe('parseWaVersion', () => {
|
||||
it('parses a valid version', () => {
|
||||
expect(parseWaVersion('2.3000.1043857760')).toEqual([2, 3000, 1043857760]);
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(parseWaVersion(' 2.3000.1 ')).toEqual([2, 3000, 1]);
|
||||
});
|
||||
|
||||
it.each(['', '2.3000', '2.3000.1.2', '2.3000.abc', 'latest'])(
|
||||
"returns null for invalid value '%s'",
|
||||
(value) => {
|
||||
expect(parseWaVersion(value)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('formatWaVersion', () => {
|
||||
it('formats a version', () => {
|
||||
expect(formatWaVersion([2, 3000, 1043857760])).toBe('2.3000.1043857760');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWaVersionHigher', () => {
|
||||
it('compares by the last part', () => {
|
||||
expect(isWaVersionHigher([2, 3000, 2], [2, 3000, 1])).toBe(true);
|
||||
expect(isWaVersionHigher([2, 3000, 1], [2, 3000, 2])).toBe(false);
|
||||
});
|
||||
|
||||
it('compares by the middle part first', () => {
|
||||
expect(isWaVersionHigher([2, 3001, 1], [2, 3000, 999])).toBe(true);
|
||||
expect(isWaVersionHigher([2, 3000, 999], [2, 3001, 1])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for equal versions', () => {
|
||||
expect(isWaVersionHigher([2, 3000, 1], [2, 3000, 1])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { WAVersion } from '@adiwajshing/baileys';
|
||||
|
||||
export function parseWaVersion(value: string): WAVersion | null {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
}
|
||||
|
||||
export function formatWaVersion(version: WAVersion): string {
|
||||
return version.join('.');
|
||||
}
|
||||
|
||||
export function isWaVersionHigher(left: WAVersion, right: WAVersion): boolean {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
if (left[index] !== right[index]) {
|
||||
return left[index] > right[index];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { TextStatus } from '@waha/structures/status.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as lodash from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
import { Page } from 'puppeteer';
|
||||
import { Client, Events, Message as WebjsMessage } from 'whatsapp-web.js';
|
||||
import { Message } from 'whatsapp-web.js/src/structures';
|
||||
@@ -93,21 +94,31 @@ function extractReactionsByMessageKey(
|
||||
export class WebjsClientCore extends Client {
|
||||
public events = new EventEmitter();
|
||||
private wpage: WPage = null;
|
||||
private injecting: Promise<void> = null;
|
||||
|
||||
constructor(
|
||||
options,
|
||||
protected tags: boolean,
|
||||
protected logger: Logger,
|
||||
) {
|
||||
super(options);
|
||||
// Wait until it's READY and inject more utils
|
||||
this.on(Events.AUTHENTICATED, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
this.on(Events.READY, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
// AUTHENTICATED and READY fire back to back - run one injection for both
|
||||
this.on(Events.AUTHENTICATED, () => this.injectUtils());
|
||||
this.on(Events.READY, () => this.injectUtils());
|
||||
}
|
||||
|
||||
private injectUtils(): Promise<void> {
|
||||
if (this.injecting) {
|
||||
return this.injecting;
|
||||
}
|
||||
this.injecting = this.attachCustomEventListeners()
|
||||
.then(() => this.injectWaha())
|
||||
.catch((err) => this.logger.error(err, 'Failed to inject utils'))
|
||||
.finally(() => {
|
||||
this.injecting = null;
|
||||
});
|
||||
return this.injecting;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@@ -147,6 +158,37 @@ export class WebjsClientCore extends Client {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @result indicating whether the "What's New" auto-modal was prevented or dismissed.
|
||||
*/
|
||||
hideWhatsNewModal(): Promise<boolean> {
|
||||
return this.pupPage.evaluate(() => {
|
||||
// Module registry is not available until the app bundle has loaded
|
||||
if (typeof window.require !== 'function') {
|
||||
return false;
|
||||
}
|
||||
const WAWebWhatsNewNux = window.require('WAWebWhatsNewNux');
|
||||
if (!WAWebWhatsNewNux) {
|
||||
return false;
|
||||
}
|
||||
// user prefs are not writable before login
|
||||
const WAWebUserPrefsMeUser = window.require('WAWebUserPrefsMeUser');
|
||||
if (!WAWebUserPrefsMeUser.getMaybeMePnUser()) {
|
||||
return false;
|
||||
}
|
||||
const nux = WAWebWhatsNewNux.createWhatsNewNux();
|
||||
if (!nux.shouldShow()) {
|
||||
return false;
|
||||
}
|
||||
// Bump the dismiss count and start the cool-off, so the app never auto-opens the modal
|
||||
nux.dismiss();
|
||||
// If the modal has already opened - close the topmost modal (no-op when nothing is open)
|
||||
const WAWebModalManager = window.require('WAWebModalManager');
|
||||
WAWebModalManager.ModalManager.close();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async attachCustomEventListeners() {
|
||||
await exposeFunctionIfAbsent(
|
||||
this.pupPage,
|
||||
|
||||
@@ -34,7 +34,7 @@ function ToGroupInfo(
|
||||
subject: group.name,
|
||||
description: group.description,
|
||||
invite: invite,
|
||||
membersCanAddNewMember: groupMetadata.restrict,
|
||||
membersCanAddNewMember: groupMetadata.memberAddMode === 'all_member_add',
|
||||
membersCanSendMessages: groupMetadata.announce,
|
||||
newMembersApprovalRequired: groupMetadata.membershipApprovalMode,
|
||||
participants: participants,
|
||||
@@ -140,6 +140,7 @@ export function ToGroupV2ParticipantsEvent(
|
||||
(id) => {
|
||||
return {
|
||||
id: id,
|
||||
pn: isPnUser(id) ? id : null,
|
||||
role: role,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { parseMessageCapping } from '@waha/core/abc/capping';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
getPublicUrlFromDirectPath,
|
||||
@@ -108,6 +109,7 @@ import {
|
||||
GroupParticipant,
|
||||
GroupSortField,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
@@ -125,7 +127,13 @@ import {
|
||||
WAMessageReaction,
|
||||
} from '@waha/structures/responses.dto';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
MessageCappingData,
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
@@ -143,12 +151,13 @@ import {
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { promiseTimeout, sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
|
||||
import { TmpDir } from '@waha/utils/tmpdir';
|
||||
import * as lodash from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { ProtocolError } from 'puppeteer';
|
||||
import { Browser, ProtocolError } from 'puppeteer';
|
||||
import {
|
||||
filter,
|
||||
fromEvent,
|
||||
@@ -213,6 +222,8 @@ export interface WebJSConfig {
|
||||
|
||||
export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
// how long to wait on stop for the browser to close gracefully before force-killing it
|
||||
private DESTROY_TIMEOUT_MS = 10_000;
|
||||
|
||||
authFactory = new WebJSAuthFactory();
|
||||
|
||||
@@ -221,6 +232,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
|
||||
private whatsNewModalJob: SinglePeriodicJobRunner;
|
||||
private shouldRestart: boolean;
|
||||
private lastQRDate: Date = null;
|
||||
private static readonly REACTION_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
||||
@@ -245,6 +257,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
2 * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
// Hide the "What's New" modal that blocks app init after QR login
|
||||
this.whatsNewModalJob = new SinglePeriodicJobRunner(
|
||||
'hide-whats-new-modal',
|
||||
SECOND,
|
||||
this.logger,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,7 +326,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const clientOptions = this.getClientOptions();
|
||||
clientOptions.authStrategy = authStrategy;
|
||||
this.addProxyConfig(clientOptions);
|
||||
return new WebjsClientCore(clientOptions, this.getWebjsTagsFlag());
|
||||
return new WebjsClientCore(
|
||||
clientOptions,
|
||||
this.getWebjsTagsFlag(),
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
protected getWebjsTagsFlag() {
|
||||
@@ -415,6 +438,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
console.log(`url is ${location.href}`),
|
||||
);
|
||||
}
|
||||
|
||||
this.startWhatsNewModalJob();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(error);
|
||||
@@ -448,11 +473,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
async stop() {
|
||||
this.cleanupPresenceTimeout();
|
||||
this.shouldRestart = false;
|
||||
this.startDelayedJob.cancel();
|
||||
await this.end();
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.stopEvents();
|
||||
this.startDelayedJob.cancel();
|
||||
this.mediaManager.close();
|
||||
await this.end();
|
||||
}
|
||||
|
||||
protected failed() {
|
||||
@@ -483,6 +508,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.cleanupPresenceTimeout();
|
||||
this.presence = null;
|
||||
this.engineStateCheckDelayedJob.cancel();
|
||||
this.whatsNewModalJob.stop();
|
||||
this.whatsapp?.removeAllListeners();
|
||||
this.whatsapp?.pupBrowser?.removeAllListeners();
|
||||
this.whatsapp?.pupPage?.removeAllListeners();
|
||||
@@ -509,10 +535,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.whatsapp?.destroy();
|
||||
await promiseTimeout(this.DESTROY_TIMEOUT_MS, this.whatsapp?.destroy());
|
||||
this.logger.debug('Successfully destroyed whatsapp client');
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Failed to destroy whatsapp client');
|
||||
const browser = this.whatsapp?.pupBrowser;
|
||||
if (browser) {
|
||||
await this.kill(browser);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -525,6 +555,33 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-kill the Chromium process when the graceful destroy() failed or timed out.
|
||||
* Throws if the browser can not be proven dead - the session is not fully stopped then.
|
||||
*/
|
||||
private async kill(browser: Browser) {
|
||||
// shadows the Node.js global 'process' on purpose - the global one is not used here
|
||||
const process = browser.process();
|
||||
if (!process) {
|
||||
// remote or attached browser - we have no process handle to kill
|
||||
throw new Error(
|
||||
'No browser process to kill, can not guarantee the browser is closed',
|
||||
);
|
||||
}
|
||||
if (process.exitCode !== null || process.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
this.logger.warn('Force killing the browser process');
|
||||
const exited = new Promise<void>((resolve) =>
|
||||
process.once('exit', () => resolve()),
|
||||
);
|
||||
process.kill('SIGKILL');
|
||||
// the session is stopped only when the browser process is really dead - verify it, do not assume
|
||||
await promiseTimeout(5_000, exited).catch(() => {
|
||||
throw new Error('The browser process did not exit after SIGKILL');
|
||||
});
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
const clientInfo = this.whatsapp?.info;
|
||||
if (!clientInfo) {
|
||||
@@ -535,6 +592,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
id: GetSerialized(wid),
|
||||
lid: GetSerialized(clientInfo.lid),
|
||||
pushName: clientInfo?.pushname,
|
||||
reachoutTimelock: this.reachoutTimelock.value,
|
||||
messageCapping: this.messageCapping.value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -550,7 +609,54 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
private startWhatsNewModalJob() {
|
||||
// The "What's New" modal can block app init right after QR login, so the session never leaves STARTING
|
||||
// https://github.com/devlikeapro/waha/issues/2217
|
||||
this.whatsNewModalJob.start(async () => {
|
||||
let hidden = false;
|
||||
try {
|
||||
hidden = await this.whatsapp.hideWhatsNewModal();
|
||||
} catch (err) {
|
||||
// The page navigates during login, evaluate() can fail - retry on the next tick
|
||||
this.logger.debug(`Failed to hide "What's New" modal: ${err}`);
|
||||
return;
|
||||
}
|
||||
if (hidden) {
|
||||
this.logger.info(`"What's New" modal has been dismissed`);
|
||||
return;
|
||||
}
|
||||
// The cool-off is stored user-scoped, so keep re-checking until it holds after login
|
||||
if (this.status === WAHASessionStatus.WORKING) {
|
||||
this.whatsNewModalJob.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
this.whatsapp.on(Events.REACHOUT_TIMELOCK_UPDATE, (record: any) => {
|
||||
this.updateReachoutTimelockFromRecord(record);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.MESSAGE_CAPPING_UPDATE, (record: any) => {
|
||||
// Unlike the timelock, a null record means "no local data yet", not "capping lifted"
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
this.messageCapping.update(parseMessageCapping(record));
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.READY, async () => {
|
||||
// Ask WhatsApp for the current message capping state, the same fetch the app runs on startup
|
||||
try {
|
||||
const data = await this.whatsapp.fetchMessageCapping();
|
||||
if (data) {
|
||||
this.messageCapping.update(parseMessageCapping(data));
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(err, 'Failed to fetch message capping');
|
||||
}
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
|
||||
this.logger.debug('QR received');
|
||||
// Convert to image and save
|
||||
@@ -784,6 +890,63 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return await this.whatsapp.deleteProfilePicture();
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async fetchMessageCapping(): Promise<MessageCappingData> {
|
||||
const data = await this.whatsapp.fetchMessageCapping();
|
||||
if (!data) {
|
||||
throw new NotImplementedByEngineError(
|
||||
'Message capping is not available in the current WhatsApp Web version',
|
||||
);
|
||||
}
|
||||
const capping = parseMessageCapping(data);
|
||||
// Keep the tracker in sync so MeInfo and 'session.status' reflect the fetch
|
||||
this.messageCapping.update(capping);
|
||||
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
|
||||
*/
|
||||
@@ -1004,6 +1167,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const extra: any = {
|
||||
type: 'buttons_response',
|
||||
kind: 'buttonsResponse',
|
||||
// WAWebGenerateButtonsResponseMessageProto reads the top-level field; nested buttonsResponse is legacy-gated
|
||||
// behind the "wa_web_buttons_response_prop_removal_killswitch" ABProp (default off).
|
||||
// Keep both like the native WAWebSendButtonsMsgReplyChatAction does
|
||||
selectedButtonId: request.selectedButtonID,
|
||||
buttonsResponse: {
|
||||
selectedButtonId: request.selectedButtonID,
|
||||
selectedDisplayText: request.selectedDisplayText,
|
||||
@@ -1508,6 +1675,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return groupChat.setMessagesAdminsOnly(value);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
|
||||
return {
|
||||
membersCanAddNewMember:
|
||||
// @ts-ignore
|
||||
groupChat.groupMetadata.memberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
|
||||
// The library setter is inverted - it takes "adminsOnly"
|
||||
return groupChat.setAddMembersAdminsOnly(!value);
|
||||
}
|
||||
|
||||
public async getGroups(pagination: PaginationParams) {
|
||||
const chats = await this.whatsapp.getChats();
|
||||
const groups = lodash.filter(chats, (chat) => chat.isGroup);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toCusFormat } from '@waha/core/utils/jids';
|
||||
import { isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||
import { SerializeMsgKey } from '@waha/core/utils/ids';
|
||||
import { WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
|
||||
@@ -109,7 +109,7 @@ export function WppParticipantsToGroupV2Participants(
|
||||
const group: GroupId = { id: toCusFormat(data.groupId) };
|
||||
const participants: GroupParticipant[] = data.who.map((id) => ({
|
||||
id: id,
|
||||
pn: null,
|
||||
pn: isPnUser(id) ? id : null,
|
||||
role: role,
|
||||
}));
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ import {
|
||||
GroupParticipantRole,
|
||||
GroupSortField,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
@@ -1412,6 +1413,26 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.wpp!.getChatById(this.ensureSuffix(id));
|
||||
// Undocumented property - 'all_member_add' or 'admin_add'
|
||||
const memberAddMode = group?.groupMetadata?.memberAddMode;
|
||||
return {
|
||||
membersCanAddNewMember: memberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public setMemberAddMode(id: string, value: boolean): Promise<boolean> {
|
||||
// Not in the GroupProperty enum, but wa-js passes the property through
|
||||
// to WhatsApp's setGroupProperty - 1 - all members, 0 - admins only
|
||||
return this.wpp!.setGroupProperty(
|
||||
this.ensureSuffix(id),
|
||||
'member_add_mode' as GroupProperty,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public deleteGroup(id: string): Promise<boolean> {
|
||||
return this.wpp!.deleteChat(this.ensureSuffix(id));
|
||||
@@ -1492,14 +1513,15 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public promoteParticipantsToAdmin(
|
||||
public async promoteParticipantsToAdmin(
|
||||
id: string,
|
||||
request: ParticipantsRequest,
|
||||
): Promise<boolean> {
|
||||
const participants = request.participants.map((participant) =>
|
||||
this.ensureSuffix(participant.id),
|
||||
);
|
||||
return this.wpp!.promoteParticipant(this.ensureSuffix(id), participants);
|
||||
await this.wpp!.promoteParticipant(this.ensureSuffix(id), participants);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Activity()
|
||||
@@ -1874,7 +1896,8 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
subject: group?.name || '',
|
||||
description: group?.groupMetadata?.desc || '',
|
||||
invite: null,
|
||||
membersCanAddNewMember: !group?.groupMetadata?.restrict,
|
||||
membersCanAddNewMember:
|
||||
group?.groupMetadata?.memberAddMode === 'all_member_add',
|
||||
membersCanSendMessages: !group?.groupMetadata?.announce,
|
||||
newMembersApprovalRequired:
|
||||
!!group?.groupMetadata?.membershipApprovalMode,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { NowebEngineConfigService } from '@waha/core/config/NowebEngineConfigService';
|
||||
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
|
||||
@@ -96,6 +97,7 @@ export class SessionManagerCore
|
||||
private engineConfigService: EngineConfigService,
|
||||
private webjsEngineConfigService: WebJSEngineConfigService,
|
||||
private wppEngineConfigService: WPPEngineConfigService,
|
||||
private nowebEngineConfigService: NowebEngineConfigService,
|
||||
gowsConfigService: GowsEngineConfigService,
|
||||
log: PinoLogger,
|
||||
private mediaStorageFactory: MediaStorageFactory,
|
||||
@@ -235,14 +237,20 @@ export class SessionManagerCore
|
||||
this.log.info(`Restarting sessions with delay of ${sleepS} seconds...`);
|
||||
const sleepMs = this.config.autoStartDelaySeconds * 1000;
|
||||
for (const sessionName of sessions) {
|
||||
const log = this.log.logger.child({ session: sessionName });
|
||||
await this.withLock(sessionName, async () => {
|
||||
const log = this.log.logger.child({ session: sessionName });
|
||||
log.info(`Restarting STOPPED session...`);
|
||||
await this.start(sessionName).catch((error) => {
|
||||
log.error(`Failed to start STOPPED session: ${error}`);
|
||||
log.error(error.stack);
|
||||
});
|
||||
});
|
||||
})
|
||||
// withLock() itself can reject (e.g. "Maximum execution time is exceeded"),
|
||||
// catch it so one stuck session doesn't abort restarting the rest
|
||||
.catch((error) => {
|
||||
log.error(`Failed to restart STOPPED session: ${error}`);
|
||||
log.error(error.stack);
|
||||
});
|
||||
await sleep(sleepMs);
|
||||
}
|
||||
this.log.info(`STOPPED sessions have been restarted.`);
|
||||
@@ -358,6 +366,9 @@ export class SessionManagerCore
|
||||
sessionConfig.engineConfig = this.wppEngineConfigService.getConfig();
|
||||
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
|
||||
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
|
||||
} else if (this.EngineClass === WhatsappSessionNoWebCore) {
|
||||
sessionConfig.engineConfig =
|
||||
await this.nowebEngineConfigService.getConfig();
|
||||
}
|
||||
// @ts-ignore
|
||||
const session = new this.EngineClass(sessionConfig);
|
||||
|
||||
@@ -14,6 +14,7 @@ interface Message {
|
||||
interface File {
|
||||
extension: string;
|
||||
filename?: string;
|
||||
mimetype?: string;
|
||||
}
|
||||
|
||||
export interface MediaData {
|
||||
|
||||
@@ -75,6 +75,7 @@ export class MediaManager implements IMediaManager {
|
||||
file: {
|
||||
extension: extension,
|
||||
filename: filename,
|
||||
mimetype: mimetype,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export class MediaLocalStorage implements IMediaStorage {
|
||||
this.log.debug(`File ${filepath} was removed`);
|
||||
}),
|
||||
delay,
|
||||
);
|
||||
).unref();
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
@@ -40,6 +40,7 @@ export class MediaS3Storage implements IMediaStorage {
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: data.file.mimetype,
|
||||
Metadata: metadata,
|
||||
});
|
||||
await this.client.send(command);
|
||||
|
||||
+2
-2
@@ -43,11 +43,11 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
});
|
||||
logger.info('NODE - Catching unhandled rejections and exceptions enabled');
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
process.once('SIGINT', () => {
|
||||
logger.info('SIGINT received');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
process.once('SIGTERM', () => {
|
||||
logger.info('SIGTERM received');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { IsString, Matches, MaxLength } from 'class-validator';
|
||||
|
||||
// Affect almost all Databases - Sqlite, MongoDB, Postgres.
|
||||
const DB_NAME_LIMIT = 64;
|
||||
const DB_NAME_MAX_PREFIX_LEN = 'waha_noweb'.length;
|
||||
|
||||
export function SessionName() {
|
||||
return applyDecorators(
|
||||
IsString(),
|
||||
MaxLength(DB_NAME_LIMIT - DB_NAME_MAX_PREFIX_LEN),
|
||||
Matches(/^[a-zA-Z0-9_-]*$/, {
|
||||
message:
|
||||
'Session name can only contain alphanumeric characters, hyphens, and underscores (a-z, A-Z, 0-9, -, _) or be empty',
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { SessionActions } from '@waha/core/auth/casl.types';
|
||||
import { SessionName } from '@waha/structures/sessions.dto';
|
||||
import { SessionName } from '@waha/nestjs/validation/SessionName';
|
||||
|
||||
export class SessionActionsDTO implements SessionActions {
|
||||
@ApiProperty({
|
||||
|
||||
@@ -422,6 +422,14 @@ export class WANumberExistResult {
|
||||
'Chat id for the phone number. Undefined if the number does not exist',
|
||||
})
|
||||
chatId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Phone number id (@c.us). Can be null when only a @lid chatId is known',
|
||||
example: '11111111111@c.us',
|
||||
required: false,
|
||||
})
|
||||
pn?: string;
|
||||
}
|
||||
|
||||
export class MessagePoll {
|
||||
|
||||
@@ -26,6 +26,10 @@ export class SettingsSecurityChangeInfo {
|
||||
adminsOnly: boolean = true;
|
||||
}
|
||||
|
||||
export class SettingsMemberAddMode {
|
||||
membersCanAddNewMember: boolean = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries
|
||||
*/
|
||||
|
||||
+161
-17
@@ -1,8 +1,8 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
|
||||
import { SessionName } from '@waha/nestjs/validation/SessionName';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
@@ -153,6 +151,30 @@ export class GowsStorageConfig {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
labels?: boolean | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Store contacts locally. Set to false to disable; omit or null to keep enabled. ' +
|
||||
'When disabled: contacts API returns no data, no contact names in chats, ' +
|
||||
'no PushName/BusinessName events, and sending status to all contacts does not work.',
|
||||
required: false,
|
||||
example: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
contacts?: boolean | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Store message secrets locally. Set to false to disable; omit or null to keep enabled. ' +
|
||||
'When disabled: incoming poll votes, event responses and bot messages can not be decrypted, ' +
|
||||
'and sending own poll votes does not work.',
|
||||
required: false,
|
||||
example: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
messageSecrets?: boolean | null;
|
||||
}
|
||||
|
||||
export class GowsConfig {
|
||||
@@ -297,6 +319,8 @@ export class SessionConfig {
|
||||
groups: true,
|
||||
chats: true,
|
||||
labels: true,
|
||||
contacts: true,
|
||||
messageSecrets: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -327,6 +351,121 @@ export class SessionDTO {
|
||||
config?: SessionConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcement types as listed in the WhatsApp Web app (WAWebUserPrefsTypes.ReachoutTimelockEnforcementType).
|
||||
* WhatsApp may introduce new values at any time - treat it as an open set.
|
||||
*/
|
||||
export enum ReachoutTimelockEnforcementType {
|
||||
// No restriction
|
||||
DEFAULT = 'DEFAULT',
|
||||
BIZ_QUALITY = 'BIZ_QUALITY',
|
||||
BIZ_COMMERCE_VIOLATION_ADULT = 'BIZ_COMMERCE_VIOLATION_ADULT',
|
||||
BIZ_COMMERCE_VIOLATION_ALCOHOL = 'BIZ_COMMERCE_VIOLATION_ALCOHOL',
|
||||
BIZ_COMMERCE_VIOLATION_ANIMALS = 'BIZ_COMMERCE_VIOLATION_ANIMALS',
|
||||
BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS = 'BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS',
|
||||
BIZ_COMMERCE_VIOLATION_DATING = 'BIZ_COMMERCE_VIOLATION_DATING',
|
||||
BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS = 'BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS',
|
||||
BIZ_COMMERCE_VIOLATION_DRUGS = 'BIZ_COMMERCE_VIOLATION_DRUGS',
|
||||
BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC = 'BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC',
|
||||
BIZ_COMMERCE_VIOLATION_GAMBLING = 'BIZ_COMMERCE_VIOLATION_GAMBLING',
|
||||
BIZ_COMMERCE_VIOLATION_HEALTHCARE = 'BIZ_COMMERCE_VIOLATION_HEALTHCARE',
|
||||
BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY = 'BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY',
|
||||
BIZ_COMMERCE_VIOLATION_SUPPLEMENTS = 'BIZ_COMMERCE_VIOLATION_SUPPLEMENTS',
|
||||
BIZ_COMMERCE_VIOLATION_TOBACCO = 'BIZ_COMMERCE_VIOLATION_TOBACCO',
|
||||
BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT = 'BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT',
|
||||
BIZ_COMMERCE_VIOLATION_WEAPONS = 'BIZ_COMMERCE_VIOLATION_WEAPONS',
|
||||
WEB_COMPANION_ONLY = 'WEB_COMPANION_ONLY',
|
||||
RESTRICT_ALL_COMPANIONS = 'RESTRICT_ALL_COMPANIONS',
|
||||
}
|
||||
|
||||
export class ReachoutTimelockData {
|
||||
@ApiProperty({
|
||||
example: ReachoutTimelockEnforcementType.RESTRICT_ALL_COMPANIONS,
|
||||
enum: ReachoutTimelockEnforcementType,
|
||||
description:
|
||||
'Raw WhatsApp enforcement type. Informational only - it does not change what is blocked. ' +
|
||||
'WhatsApp may introduce new values, so treat it as an open set.',
|
||||
})
|
||||
enforcementType: ReachoutTimelockEnforcementType;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1784477333,
|
||||
nullable: true,
|
||||
description: 'Unix timestamp (seconds) when the enforcement ends.',
|
||||
})
|
||||
timeEnforcementEnds: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capping status for the per-cycle new-chat message quota.
|
||||
* WhatsApp may introduce new values at any time - treat it as an open set.
|
||||
*/
|
||||
export enum MessageCappingStatus {
|
||||
NONE = 'NONE',
|
||||
FIRST_WARNING = 'FIRST_WARNING',
|
||||
SECOND_WARNING = 'SECOND_WARNING',
|
||||
CAPPED = 'CAPPED',
|
||||
}
|
||||
|
||||
export class MessageCappingData {
|
||||
@ApiProperty({
|
||||
example: MessageCappingStatus.FIRST_WARNING,
|
||||
enum: MessageCappingStatus,
|
||||
description:
|
||||
'How close the account is to its new-chat quota. ' +
|
||||
'CAPPED means new chats are blocked. WhatsApp may introduce new values, ' +
|
||||
'so treat it as an open set.',
|
||||
})
|
||||
cappingStatus: MessageCappingStatus;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1000,
|
||||
description:
|
||||
'New-chat messages allowed in the current cycle. -1 when the account ' +
|
||||
'has no cap.',
|
||||
})
|
||||
totalQuota: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 640,
|
||||
description: 'New-chat messages already used in the current cycle.',
|
||||
})
|
||||
usedQuota: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1782874800,
|
||||
nullable: true,
|
||||
description: 'Unix timestamp (seconds) when the current cycle started.',
|
||||
})
|
||||
cycleStart: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1785553199,
|
||||
nullable: true,
|
||||
description: 'Unix timestamp (seconds) when the current cycle ends.',
|
||||
})
|
||||
cycleEnd: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'NOT_ELIGIBLE',
|
||||
nullable: true,
|
||||
description: 'Meta Verified status. Informational.',
|
||||
})
|
||||
mvStatus: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'NOT_ELIGIBLE',
|
||||
nullable: true,
|
||||
description: 'One-time engagement status. Informational.',
|
||||
})
|
||||
oteStatus: string | null;
|
||||
}
|
||||
|
||||
export class MeInfo {
|
||||
@ChatIdProperty()
|
||||
id: string;
|
||||
@@ -343,6 +482,24 @@ export class MeInfo {
|
||||
jid?: string;
|
||||
|
||||
pushName: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
nullable: true,
|
||||
description:
|
||||
'WhatsApp reachout timelock (account restriction) info. ' +
|
||||
'Null if no enforcement has been seen for the account.',
|
||||
})
|
||||
reachoutTimelock?: ReachoutTimelockData | null;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
nullable: true,
|
||||
description:
|
||||
'WhatsApp new-chat message capping (per-cycle quota) info. ' +
|
||||
'Null until the capping state has been fetched for the account.',
|
||||
})
|
||||
messageCapping?: MessageCappingData | null;
|
||||
}
|
||||
|
||||
export class SessionInfo extends SessionDTO {
|
||||
@@ -368,20 +525,7 @@ export class SessionDetailedInfo extends SessionInfo {
|
||||
engine?: any;
|
||||
}
|
||||
|
||||
// Affect almost all Databases - Sqlite, MongoDB, Postgres.
|
||||
const DB_NAME_LIMIT = 64;
|
||||
const DB_NAME_MAX_PREFIX_LEN = 'waha_noweb'.length;
|
||||
|
||||
export function SessionName() {
|
||||
return applyDecorators(
|
||||
IsString(),
|
||||
MaxLength(DB_NAME_LIMIT - DB_NAME_MAX_PREFIX_LEN),
|
||||
Matches(/^[a-zA-Z0-9_-]*$/, {
|
||||
message:
|
||||
'Session name can only contain alphanumeric characters, hyphens, and underscores (a-z, A-Z, 0-9, -, _) or be empty',
|
||||
}),
|
||||
);
|
||||
}
|
||||
export { SessionName };
|
||||
|
||||
export class SessionCreateRequest {
|
||||
@ApiProperty({
|
||||
|
||||
@@ -133,7 +133,10 @@ export class WASessionStatusBody {
|
||||
'Extra info that belongs to the current status, null for most of them.\n' +
|
||||
'PASSKEY_REQUIRED - the WebAuthn challenge, ' +
|
||||
'pass it to navigator.credentials.get({ publicKey: data }).\n' +
|
||||
'PASSKEY_CONFIRMATION_REQUIRED - { code } - the code to verify against the phone.',
|
||||
'PASSKEY_CONFIRMATION_REQUIRED - { code } - the code to verify against the phone.\n' +
|
||||
'WORKING - { reachoutTimelock } - present when WhatsApp restricts the account ' +
|
||||
'from messaging new contacts (the cause of 463 errors on send); ' +
|
||||
'the session stays connected and the restriction lifts automatically.',
|
||||
example: null,
|
||||
})
|
||||
data?: any;
|
||||
|
||||
@@ -20,6 +20,8 @@ export const promiseTimeout = function (
|
||||
ms: number,
|
||||
promise: Promise<any>,
|
||||
): Promise<any> {
|
||||
// the promise may reject after the timeout already won the race - do not treat it as unhandled
|
||||
promise?.catch?.(() => undefined);
|
||||
let timer: NodeJS.Timeout;
|
||||
return Promise.race([
|
||||
promise,
|
||||
@@ -57,12 +59,47 @@ export async function waitUntil(
|
||||
/**
|
||||
* Handle setTimeout 32-bit overflow
|
||||
*/
|
||||
export function setLongTimeout(callback, delayMs) {
|
||||
const MAX_DELAY = 2_147_483_647; // ~24.8 days
|
||||
export class LongTimeout {
|
||||
private static readonly MAX_DELAY = 2_147_483_647; // ~24.8 days
|
||||
|
||||
if (delayMs > MAX_DELAY) {
|
||||
setTimeout(() => setLongTimeout(callback, delayMs - MAX_DELAY), MAX_DELAY);
|
||||
} else {
|
||||
setTimeout(callback, delayMs);
|
||||
private timeout?: ReturnType<typeof setTimeout>;
|
||||
private unrefed: boolean = false;
|
||||
|
||||
constructor(
|
||||
private callback: () => void,
|
||||
delayMs: number,
|
||||
) {
|
||||
this.schedule(delayMs);
|
||||
}
|
||||
|
||||
private schedule(delayMs: number) {
|
||||
if (delayMs > LongTimeout.MAX_DELAY) {
|
||||
this.timeout = setTimeout(
|
||||
() => this.schedule(delayMs - LongTimeout.MAX_DELAY),
|
||||
LongTimeout.MAX_DELAY,
|
||||
);
|
||||
} else {
|
||||
this.timeout = setTimeout(this.callback, delayMs);
|
||||
}
|
||||
if (this.unrefed) {
|
||||
this.timeout.unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not keep the process alive just for this timer
|
||||
*/
|
||||
unref(): LongTimeout {
|
||||
this.unrefed = true;
|
||||
this.timeout?.unref?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export function setLongTimeout(callback: () => void, delayMs: number) {
|
||||
return new LongTimeout(callback, delayMs);
|
||||
}
|
||||
+18
-1
@@ -1,5 +1,15 @@
|
||||
/**
|
||||
* If value is ms - we convert it to seconds
|
||||
* Both helpers detect the unit by magnitude, using 1e12 as the threshold:
|
||||
* as unix SECONDS 1e12 is year 33658, as unix MILLISECONDS it's Sep 2001 - so any realistic "now-ish"
|
||||
* timestamp is below 1e12 in seconds and above it in milliseconds.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalize a unix timestamp to SECONDS.
|
||||
* Milliseconds get converted (floored), values already in seconds pass through unchanged.
|
||||
*
|
||||
* EnsureSeconds(1784477333) => 1784477333 (already seconds)
|
||||
* EnsureSeconds(1784477333000) => 1784477333 (ms => seconds)
|
||||
*/
|
||||
export function EnsureSeconds(ms: number) {
|
||||
if (!ms) {
|
||||
@@ -10,6 +20,13 @@ export function EnsureSeconds(ms: number) {
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a unix timestamp to MILLISECONDS. Seconds get converted, values already in milliseconds pass through unchanged.
|
||||
*
|
||||
* EnsureMilliseconds(1784477333) => 1784477333000 (seconds => ms)
|
||||
* EnsureMilliseconds(1784477333000) => 1784477333000 (already ms)
|
||||
*/
|
||||
export function EnsureMilliseconds(seconds: number) {
|
||||
if (!seconds) {
|
||||
return seconds;
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ function getPlatform() {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2026.7.1',
|
||||
version: '2026.8.1',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowser(),
|
||||
|
||||
@@ -16,5 +16,10 @@
|
||||
"incremental": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "src/dashboard"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["**/node_modules", "src/dashboard"]
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows-plus",
|
||||
"ref": "v1.0.43"
|
||||
"ref": "v1.0.45"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
"ref": "68a2e06035beed6ba04bd5dede49399d5478096d"
|
||||
"ref": "07f9a7b79c2f0db7513005b074824424e6ca1a67"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user