diff --git a/package.json b/package.json index 7cbe1989..45342349 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "async-lock": "^1.4.1", "better-sqlite3": "9.6.0", "check-disk-space": "^3.4.0", - "class-validator": "^0.12.2", + "class-transformer": "^0.5.1", + "class-validator": "0.14.0", "del": "^6.0.0", "express-basic-auth": "^1.2.1", "file-type": "16.5.4", diff --git a/src/api/chats.controller.ts b/src/api/chats.controller.ts index ae16027b..0263f30a 100644 --- a/src/api/chats.controller.ts +++ b/src/api/chats.controller.ts @@ -4,16 +4,17 @@ import { Delete, Get, Param, - Post, Put, Query, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; import { SessionManager } from '../core/abc/manager.abc'; import { WhatsappSession } from '../core/abc/session.abc'; import { parseBool } from '../helpers'; -import { GetChatMessagesQuery } from '../structures/chats.dto'; +import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto'; import { EditMessageRequest } from '../structures/chatting.dto'; import { ChatIdApiParam, @@ -25,14 +26,18 @@ import { @ApiSecurity('api_key') @Controller('api/:session/chats') @ApiTags('chats') +@UsePipes(new ValidationPipe({ transform: true })) class ChatsController { constructor(private manager: SessionManager) {} @Get('') @SessionApiParam - @ApiOperation({ summary: 'Get all chats' }) - getAllChats(@SessionParam session: WhatsappSession) { - return session.getChats(); + @ApiOperation({ summary: 'Get chats' }) + getChats( + @SessionParam session: WhatsappSession, + @Query() query: GetChatsQuery, + ) { + return session.getChats(query); } @Delete(':chatId') diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index 98499745..987b506e 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -1,4 +1,5 @@ import { ConsoleLogger } from '@nestjs/common'; +import { GetChatsQuery } from '@waha/structures/chats.dto'; import { EventEmitter } from 'events'; import * as fs from 'fs'; import { MessageId } from 'whatsapp-web.js'; @@ -285,7 +286,7 @@ export abstract class WhatsappSession { /** * Chats methods */ - public getChats() { + public getChats(query: GetChatsQuery) { throw new NotImplementedByEngineError(); } diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index 45f9c5db..f5891675 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -20,6 +20,7 @@ import { UnprocessableEntityException } from '@nestjs/common'; import { NowebInMemoryStore } from '@waha/core/engines/noweb/store/NowebInMemoryStore'; import { flipObject, getLogLevels, parseBool, splitAt } from '@waha/helpers'; import { PairingCodeResponse } from '@waha/structures/auth.dto'; +import { GetChatsQuery } from '@waha/structures/chats.dto'; import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto'; import { PollVote, @@ -623,8 +624,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { * Chats methods */ - async getChats() { - const chats = await this.store.getChats(); + async getChats(query: GetChatsQuery) { + const chats = await this.store.getChats(query.limit, query.offset); // Remove unreadCount, it's not ready yet chats.forEach((chat) => delete chat.unreadCount); return chats; diff --git a/src/core/engines/noweb/store/IChatRepository.ts b/src/core/engines/noweb/store/IChatRepository.ts index 12688ec2..e80b3c1e 100644 --- a/src/core/engines/noweb/store/IChatRepository.ts +++ b/src/core/engines/noweb/store/IChatRepository.ts @@ -3,7 +3,7 @@ import { Chat } from '@adiwajshing/baileys'; export interface IChatRepository { getAll(): Promise; - getAllWithMessages(): Promise; + getAllWithMessages(limit?: number, offset?: number): Promise; getById(id: string): Promise; diff --git a/src/core/engines/noweb/store/INowebStore.ts b/src/core/engines/noweb/store/INowebStore.ts index b14c119c..5145aa74 100644 --- a/src/core/engines/noweb/store/INowebStore.ts +++ b/src/core/engines/noweb/store/INowebStore.ts @@ -18,7 +18,7 @@ export interface INowebStore { getMessagesByJid(chatId: string, limit: number): Promise; - getChats(): Promise; + getChats(limit?: number, offset?: number): Promise; getContacts(): Promise; diff --git a/src/core/engines/noweb/store/NowebInMemoryStore.ts b/src/core/engines/noweb/store/NowebInMemoryStore.ts index 2ee471f6..a47879b3 100644 --- a/src/core/engines/noweb/store/NowebInMemoryStore.ts +++ b/src/core/engines/noweb/store/NowebInMemoryStore.ts @@ -40,7 +40,7 @@ export class NowebInMemoryStore implements INowebStore { throw new BadRequestException(this.errorMessage); } - getChats(): Promise { + getChats(limit?: number, offset?: number): Promise { throw new BadRequestException(this.errorMessage); } diff --git a/src/core/engines/noweb/store/NowebPersistentStore.ts b/src/core/engines/noweb/store/NowebPersistentStore.ts index a7129b9a..a1d7ba17 100644 --- a/src/core/engines/noweb/store/NowebPersistentStore.ts +++ b/src/core/engines/noweb/store/NowebPersistentStore.ts @@ -291,8 +291,8 @@ export class NowebPersistentStore implements INowebStore { return this.messagesRepo.getAllByJid(toJID(chatId), toNumber(limit)); } - getChats() { - return this.chatRepo.getAllWithMessages(); + getChats(limit?: number, offset?: number): Promise { + return this.chatRepo.getAllWithMessages(limit, offset); } getContactById(jid) { diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts index cf735071..909e4eca 100644 --- a/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts @@ -7,11 +7,17 @@ export class Sqlite3ChatRepository extends Sqlite3KVRepository implements IChatRepository { - getAllWithMessages(): Promise { + async getAllWithMessages(limit?: number, offset?: number): Promise { // Get chats with conversationTimestamp is not Null - const query = this.select() + let query = this.select() .whereNotNull('conversationTimestamp') .orderBy('conversationTimestamp', 'desc'); - return this.all(query); + if (limit != null) { + query = query.limit(limit); + } + if (offset != null) { + query = query.offset(offset); + } + return await this.all(query); } } diff --git a/src/core/engines/webjs/WahaInjected.js b/src/core/engines/webjs/WahaInjected.js new file mode 100644 index 00000000..6fd171b4 --- /dev/null +++ b/src/core/engines/webjs/WahaInjected.js @@ -0,0 +1,15 @@ +exports.LoadUtils = () => { + window.WAHA = {}; + + window.WAHA.getChats = async (limit, offset) => { + let chats = window.Store.Chat.getModelsArray(); + if (limit || offset) { + offset = offset || 0; + limit = limit || Infinity; + chats = chats.slice(offset, offset + limit); + } + + const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat)); + return await Promise.all(chatPromises); + }; +}; diff --git a/src/core/engines/webjs/WebjsClient.ts b/src/core/engines/webjs/WebjsClient.ts new file mode 100644 index 00000000..9825a451 --- /dev/null +++ b/src/core/engines/webjs/WebjsClient.ts @@ -0,0 +1,35 @@ +import { Client } from 'whatsapp-web.js'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { LoadUtils } = require('./WahaInjected'); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory'); + +export class WebjsClient extends Client { + async initialize() { + await super.initialize(); + //Load util functions (serializers, helper functions) + await this.pupPage.evaluate(LoadUtils); + } + + async getChats(limit?: number, offset?: number) { + if (limit == null && offset == null) { + return await super.getChats(); + } + return await this.getChats2(limit, offset); + } + + private async getChats2(limit?: number, offset?: number) { + const chats = await this.pupPage.evaluate( + async (limit, offset) => { + // @ts-ignore + return await window.WAHA.getChats(limit, offset); + }, + limit, + offset, + ); + + return chats.map((chat) => ChatFactory.create(this, chat)); + } +} diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 9588e4ad..007243c7 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -1,18 +1,14 @@ import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception'; +import { IEngineMediaProcessor } from '@waha/core/abc/media.abc'; +import { WAHAInternalEvent, WhatsappSession } from '@waha/core/abc/session.abc'; +import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient'; import { - Chat, - Client, - ClientOptions, - Contact, - Events, - GroupChat, - Location, - Message, - Reaction, -} from 'whatsapp-web.js'; -import { Message as MessageInstance } from 'whatsapp-web.js/src/structures'; - -import { parseBool } from '../../../helpers'; + AvailableInPlusVersion, + NotImplementedByEngineError, +} from '@waha/core/exceptions'; +import { QR } from '@waha/core/QR'; +import { parseBool } from '@waha/helpers'; +import { GetChatsQuery } from '@waha/structures/chats.dto'; import { ChatRequest, CheckNumberStatusQuery, @@ -28,8 +24,8 @@ import { MessageVoiceRequest, SendSeenRequest, WANumberExistResult, -} from '../../../structures/chatting.dto'; -import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto'; +} from '@waha/structures/chatting.dto'; +import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto'; import { ACK_UNKNOWN, SECOND, @@ -38,32 +34,30 @@ import { WAHAPresenceStatus, WAHASessionStatus, WAMessageAck, -} from '../../../structures/enums.dto'; +} from '@waha/structures/enums.dto'; import { CreateGroupRequest, ParticipantsRequest, SettingsSecurityChangeInfo, -} from '../../../structures/groups.dto'; +} from '@waha/structures/groups.dto'; +import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; +import { MeInfo } from '@waha/structures/sessions.dto'; +import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto'; import { - WAMessage, - WAMessageReaction, -} from '../../../structures/responses.dto'; -import { MeInfo } from '../../../structures/sessions.dto'; -import { WAMessageRevokedBody } from '../../../structures/webhooks.dto'; -import { IEngineMediaProcessor } from '../../abc/media.abc'; -import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc'; -import { - AvailableInPlusVersion, - NotImplementedByEngineError, -} from '../../exceptions'; -import { QR } from '../../QR'; + Chat, + ClientOptions, + Contact, + Events, + GroupChat, + Location, + Message, + Reaction, +} from 'whatsapp-web.js'; +import { Message as MessageInstance } from 'whatsapp-web.js/src/structures'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const qrcode = require('qrcode-terminal'); - export interface WebJSConfig { webVersion?: string; } @@ -77,7 +71,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { private startTimeoutId: null | ReturnType = null; private shouldRestart: boolean; - whatsapp: Client; + whatsapp: WebjsClient; protected qr: QR; public constructor(config) { @@ -116,7 +110,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { protected async buildClient() { const clientOptions = this.getClientOptions(); this.addProxyConfig(clientOptions); - return new Client(clientOptions); + return new WebjsClient(clientOptions); } private restartClient() { @@ -417,8 +411,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { /** * Chats methods */ - getChats() { - return this.whatsapp.getChats(); + getChats(query: GetChatsQuery) { + return this.whatsapp.getChats(query.limit, query.offset); } async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) { diff --git a/src/main.ts b/src/main.ts index 2092fc9a..90a0036c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { json, urlencoded } from 'express'; @@ -47,6 +48,9 @@ async function bootstrap() { app.enableShutdownHooks(); app.useGlobalFilters(new AllExceptionsFilter()); app.enableCors(); + // Ideally, we should apply it globally. + // but for now we added it ValidationPipe on Controller or endpoint level + // app.useGlobalPipes(new ValidationPipe({ transform: true })); // Allow to send big body - for images and attachments app.use(json({ limit: '50mb' })); diff --git a/src/structures/chats.dto.ts b/src/structures/chats.dto.ts index 294e2eba..d176acc8 100644 --- a/src/structures/chats.dto.ts +++ b/src/structures/chats.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber, IsOptional } from 'class-validator'; -import { SessionBaseRequest, SessionQuery } from './base.dto'; +import { SessionQuery } from './base.dto'; /** * Queries @@ -8,6 +10,7 @@ import { SessionBaseRequest, SessionQuery } from './base.dto'; export class GetChatMessagesQuery extends SessionQuery { limit: number = 100; + @ApiProperty({ example: true, required: false, @@ -16,6 +19,18 @@ export class GetChatMessagesQuery extends SessionQuery { downloadMedia: boolean = true; } +export class GetChatsQuery { + @IsNumber() + @IsOptional() + @Type(() => Number) + limit?: number; + + @IsNumber() + @IsOptional() + @Type(() => Number) + offset?: number; +} + /** * Requests */ diff --git a/yarn.lock b/yarn.lock index a19594ea..ff2f1cf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2401,10 +2401,10 @@ __metadata: languageName: node linkType: hard -"@types/validator@npm:13.0.0": - version: 13.0.0 - resolution: "@types/validator@npm:13.0.0" - checksum: 7001e96e663daa28f45120339ea4478cdf74cadfe74aba8b8de4c56bdaab559ab65de2eb334ce9026bba153231b96d9e6506b5d5dff8fea4e6c1a6babec523b3 +"@types/validator@npm:^13.7.10": + version: 13.12.0 + resolution: "@types/validator@npm:13.12.0" + checksum: 1d814a4a9f8fbd01f834bf45215aa8111879e71045391eec8e8fda6a7acf71d8b7839a0d312098c2b9c8285963203dfb3e3680a41484d1e9b3d576f95b6d71c0 languageName: node linkType: hard @@ -4211,15 +4211,21 @@ __metadata: languageName: node linkType: hard -"class-validator@npm:^0.12.2": - version: 0.12.2 - resolution: "class-validator@npm:0.12.2" +"class-transformer@npm:^0.5.1": + version: 0.5.1 + resolution: "class-transformer@npm:0.5.1" + checksum: f191c8b4cc4239990f5efdd790cabdd852c243ed66248e39f6616a349c910c6eed2d9fd1fbf7ee6ea89f69b4f1d0b493b27347fe0cd0ae26b47c3745a805b6d3 + languageName: node + linkType: hard + +"class-validator@npm:0.14.0": + version: 0.14.0 + resolution: "class-validator@npm:0.14.0" dependencies: - "@types/validator": 13.0.0 - google-libphonenumber: ^3.2.8 - tslib: ">=1.9.0" - validator: 13.0.0 - checksum: 37a806f62794d0c473045b8a77bc48892e87a959d57588b52774385058a862e77952c3af4c02d61db5fd863ff08d19935b1bdd38851028e1460bb94eb97ffec7 + "@types/validator": ^13.7.10 + libphonenumber-js: ^1.10.14 + validator: ^13.7.0 + checksum: f62e4a0ad24cee68f4b2bc70d32b96de90cb598f96bde362b4dbf4234151af8eb6ae225458312a38fc49fa3959844cf61c60e731a8205e9a570454cff8de2710 languageName: node linkType: hard @@ -6740,13 +6746,6 @@ __metadata: languageName: node linkType: hard -"google-libphonenumber@npm:^3.2.8": - version: 3.2.34 - resolution: "google-libphonenumber@npm:3.2.34" - checksum: a14579bb8d4934dc6b9cfb33f818375ccdc67be12f13f842a4c16d7c3bd8ef9f2aacc30d368770f33a97ba8674e23b318cb59f26616c7fa2ce60fae52b2f6ebf - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -8433,6 +8432,13 @@ __metadata: languageName: node linkType: hard +"libphonenumber-js@npm:^1.10.14": + version: 1.11.3 + resolution: "libphonenumber-js@npm:1.11.3" + checksum: c6b73b64b69aeea42f031dd56d6b51d1a1ccda5a079bad4bc6357f4e7ed5cf7f9303e0545fd6d30e08245d8c0f3140216d9130336aeaebb32494b5b567715326 + languageName: node + linkType: hard + "libphonenumber-js@npm:^1.10.20, libphonenumber-js@npm:^1.10.36": version: 1.10.56 resolution: "libphonenumber-js@npm:1.10.56" @@ -12331,13 +12337,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:>=1.9.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad - languageName: node - linkType: hard - "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -12345,6 +12344,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + "tsutils@npm:^3.17.1, tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -12743,10 +12749,10 @@ __metadata: languageName: node linkType: hard -"validator@npm:13.0.0": - version: 13.0.0 - resolution: "validator@npm:13.0.0" - checksum: 7b5df6d0aec660ad12089fbcdd07cf506181ca2cd711d6e2bec7502c5cac1b31e8eb03875d23bcbc1b81a93648d5aacd52e2dc7b62cf02e71d3768533630f4c5 +"validator@npm:^13.7.0": + version: 13.12.0 + resolution: "validator@npm:13.12.0" + checksum: fb8f070724770b1449ea1a968605823fdb112dbd10507b2802f8841cda3e7b5c376c40f18c84e6a7b59de320a06177e471554101a85f1fa8a70bac1a84e48adf languageName: node linkType: hard @@ -12856,7 +12862,8 @@ __metadata: async-lock: ^1.4.1 better-sqlite3: 9.6.0 check-disk-space: ^3.4.0 - class-validator: ^0.12.2 + class-transformer: ^0.5.1 + class-validator: 0.14.0 del: ^6.0.0 eslint: 7.7.0 eslint-config-prettier: ^6.10.0