From bc7ff472be00190db5cb1b3ba7358b7c7463da32 Mon Sep 17 00:00:00 2001 From: devlikepro Date: Tue, 24 Dec 2024 13:32:48 +0700 Subject: [PATCH] [core] Chats overview API --- src/api/chats.controller.ts | 17 ++++++- src/core/abc/session.abc.ts | 10 +++- src/core/engines/noweb/session.noweb.core.ts | 46 ++++++++++++++++++- .../engines/noweb/store/IChatRepository.ts | 5 +- src/core/engines/noweb/store/INowebStore.ts | 2 +- .../engines/noweb/store/NowebInMemoryStore.ts | 2 +- .../noweb/store/NowebPersistentStore.ts | 4 +- .../store/sqlite3/Sqlite3ChatRepository.ts | 11 ++++- src/core/engines/webjs/session.webjs.core.ts | 46 +++++++++++++++---- src/structures/chats.dto.ts | 20 +++++++- src/structures/pagination.dto.ts | 5 +- 11 files changed, 148 insertions(+), 20 deletions(-) diff --git a/src/api/chats.controller.ts b/src/api/chats.controller.ts index bcdb4df4..203c6574 100644 --- a/src/api/chats.controller.ts +++ b/src/api/chats.controller.ts @@ -21,14 +21,15 @@ import { import { SessionManager } from '../core/abc/manager.abc'; import { WhatsappSession } from '../core/abc/session.abc'; -import { parseBool } from '../helpers'; import { ChatPictureQuery, ChatPictureResponse, ChatsPaginationParams, + ChatSummary, GetChatMessageQuery, GetChatMessagesFilter, GetChatMessagesQuery, + OverviewPaginationParams, PinMessageRequest, } from '../structures/chats.dto'; import { EditMessageRequest } from '../structures/chatting.dto'; @@ -50,6 +51,20 @@ class ChatsController { return session.getChats(pagination); } + @Get('overview') + @SessionApiParam + @ApiOperation({ + summary: + 'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp', + }) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + getChatsOverview( + @WorkingSessionParam session: WhatsappSession, + @Query() pagination: OverviewPaginationParams, + ): Promise { + return session.getChatsOverview(pagination); + } + @Delete(':chatId') @SessionApiParam @ApiOperation({ summary: 'Deletes the chat' }) diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index f146d477..54aa8cae 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -4,6 +4,7 @@ import { ListChannelsQuery, } from '@waha/structures/channels.dto'; import { + ChatSummary, GetChatMessageQuery, GetChatMessagesFilter, GetChatMessagesQuery, @@ -133,7 +134,7 @@ export abstract class WhatsappSession { private shouldPrintQR: boolean; protected events2: DefaultMap>; private status$: Subject; - private profilePictures: NodeCache = new NodeCache({ + protected profilePictures: NodeCache = new NodeCache({ stdTTL: 24 * 60 * 60, // 1 day }); @@ -397,6 +398,12 @@ export abstract class WhatsappSession { throw new NotImplementedByEngineError(); } + public getChatsOverview( + pagination: PaginationParams, + ): Promise { + throw new NotImplementedByEngineError(); + } + public deleteChat(chatId) { throw new NotImplementedByEngineError(); } @@ -533,6 +540,7 @@ export abstract class WhatsappSession { return null; }); this.profilePictures.set(id, url); + return url; } public blockContact(request: ContactRequest) { diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index a54c418f..d301a64f 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -1,5 +1,6 @@ import makeWASocket, { Browsers, + Chat, Contact, DisconnectReason, downloadMediaMessage, @@ -9,7 +10,9 @@ import makeWASocket, { getKeyAuthor, getUrlFromDirectPath, isJidGroup, + isJidNewsletter, isJidStatusBroadcast, + isJidUser, isRealMessage, jidNormalizedUser, makeCacheableSignalKeyStore, @@ -50,6 +53,7 @@ import { ListChannelsQuery, } from '@waha/structures/channels.dto'; import { + ChatSummary, GetChatMessageQuery, GetChatMessagesFilter, GetChatMessagesQuery, @@ -902,12 +906,46 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { */ async getChats(pagination: PaginationParams) { - const chats = await this.store.getChats(pagination); + const chats = await this.store.getChats(pagination, true); // Remove unreadCount, it's not ready yet chats.forEach((chat) => delete chat.unreadCount); return chats; } + public async getChatsOverview( + pagination: PaginationParams, + ): Promise { + const chats = await this.store.getChats(pagination, false); + // Remove unreadCount, it's not ready yet + chats.forEach((chat) => delete chat.unreadCount); + + const promises = []; + for (const chat of chats) { + promises.push(this.fetchChatSummary(chat)); + } + const result = await Promise.all(promises); + return result; + } + + protected async fetchChatSummary(chat: Chat): Promise { + const id = toCusFormat(chat.id); + const name = chat.name; + const picture = await this.getContactProfilePicture(chat.id, false); + const messages = await this.getChatMessages( + chat.id, + { limit: 1, offset: 0, downloadMedia: false }, + {}, + ); + const message = messages.length > 0 ? messages[0] : null; + return { + id: id, + name: name || null, + picture: picture, + lastMessage: message, + _chat: chat, + }; + } + protected async chatsPutArchive( chatId: string, archive: boolean, @@ -1065,6 +1103,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { public async fetchContactProfilePicture(id: string) { const contact = this.ensureSuffix(id); + if (isJidNewsletter(id)) { + return null; + } + if (isJidStatusBroadcast(id)) { + return null; + } const url = await this.sock.profilePictureUrl(contact, 'image'); return url; } diff --git a/src/core/engines/noweb/store/IChatRepository.ts b/src/core/engines/noweb/store/IChatRepository.ts index 3f30210d..65efee87 100644 --- a/src/core/engines/noweb/store/IChatRepository.ts +++ b/src/core/engines/noweb/store/IChatRepository.ts @@ -6,7 +6,10 @@ export interface IChatRepository { getAllByIds(ids: string[]): Promise; - getAllWithMessages(pagination: PaginationParams): Promise; + getAllWithMessages( + pagination: PaginationParams, + broadcast: boolean, + ): Promise; getById(id: string): Promise; diff --git a/src/core/engines/noweb/store/INowebStore.ts b/src/core/engines/noweb/store/INowebStore.ts index d5e0ed79..3df53b4a 100644 --- a/src/core/engines/noweb/store/INowebStore.ts +++ b/src/core/engines/noweb/store/INowebStore.ts @@ -28,7 +28,7 @@ export interface INowebStore { getMessageById(chatId: string, messageId: string): Promise; - getChats(pagination: PaginationParams): Promise; + getChats(pagination: PaginationParams, broadcast: boolean): Promise; getContacts(pagination: PaginationParams): Promise; diff --git a/src/core/engines/noweb/store/NowebInMemoryStore.ts b/src/core/engines/noweb/store/NowebInMemoryStore.ts index f750e2bc..88dee609 100644 --- a/src/core/engines/noweb/store/NowebInMemoryStore.ts +++ b/src/core/engines/noweb/store/NowebInMemoryStore.ts @@ -61,7 +61,7 @@ export class NowebInMemoryStore implements INowebStore { throw new BadRequestException(this.errorMessage); } - getChats(pagination: PaginationParams): Promise { + getChats(pagination: PaginationParams, broadcast: boolean): 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 fbea0b96..b39f83d4 100644 --- a/src/core/engines/noweb/store/NowebPersistentStore.ts +++ b/src/core/engines/noweb/store/NowebPersistentStore.ts @@ -448,10 +448,10 @@ export class NowebPersistentStore implements INowebStore { return this.messagesRepo.getByJidById(chatId, messageId); } - getChats(pagination: PaginationParams): Promise { + getChats(pagination: PaginationParams, broadcast: boolean): Promise { pagination.sortBy ||= 'conversationTimestamp'; pagination.sortOrder ||= SortOrder.DESC; - return this.chatRepo.getAllWithMessages(pagination); + return this.chatRepo.getAllWithMessages(pagination, broadcast); } private shouldFetchGroup(): boolean { diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts index 0bca16f1..64c93a15 100644 --- a/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts @@ -15,9 +15,18 @@ export class Sqlite3ChatRepository { protected Paginator = ChatPaginator; - async getAllWithMessages(pagination: PaginationParams): Promise { + async getAllWithMessages( + pagination: PaginationParams, + broadcast: boolean, + ): Promise { // Get chats with conversationTimestamp is not Null let query = this.select().whereNotNull('conversationTimestamp'); + if (!broadcast) { + // filter out chat by id if it ends at @newsletter or @broadcast + query = query + .andWhereNot('id', 'like', '%@broadcast') + .andWhereNot('id', 'like', '%@newsletter'); + } query = this.pagination(query, pagination); return await this.all(query); } diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 4adbaa4d..74589ed9 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -13,7 +13,6 @@ import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; import { QR } from '@waha/core/QR'; import { splitAt } from '@waha/helpers'; import { PairingCodeResponse } from '@waha/structures/auth.dto'; -import { CallData } from '@waha/structures/calls.dto'; import { Channel, ChannelRole, @@ -21,8 +20,8 @@ import { ListChannelsQuery, } from '@waha/structures/channels.dto'; import { - ChatArchiveEvent, ChatSortField, + ChatSummary, GetChatMessageQuery, GetChatMessagesFilter, GetChatMessagesQuery, @@ -61,14 +60,10 @@ import { } from '@waha/structures/groups.dto'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; import { ReplyToMessage } from '@waha/structures/message.dto'; -import { PaginationParams } from '@waha/structures/pagination.dto'; +import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto'; import { MeInfo } from '@waha/structures/sessions.dto'; -import { - BROADCAST_ID, - StatusRequest, - TextStatus, -} from '@waha/structures/status.dto'; +import { StatusRequest, TextStatus } from '@waha/structures/status.dto'; import { EnginePayload, WAMessageRevokedBody, @@ -573,6 +568,41 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return this.whatsapp.getChats(pagination); } + public async getChatsOverview( + pagination: PaginationParams, + ): Promise { + pagination = { + ...pagination, + sortBy: ChatSortField.CONVERSATION_TIMESTAMP, + sortOrder: SortOrder.DESC, + }; + const chats = await this.getChats(pagination); + + const promises = []; + for (const chat of chats) { + promises.push(this.fetchChatSummary(chat)); + } + const result = await Promise.all(promises); + return result; + } + + protected async fetchChatSummary(chat: Chat): Promise { + const picture = await this.getContactProfilePicture( + chat.id._serialized, + false, + ); + const lastMessage = !!chat.lastMessage + ? this.toWAMessage(chat.lastMessage) + : null; + return { + id: chat.id._serialized, + name: chat.name || null, + picture: picture, + lastMessage: lastMessage, + _chat: chat, + }; + } + public async getChatMessages( chatId: string, query: GetChatMessagesQuery, diff --git a/src/structures/chats.dto.ts b/src/structures/chats.dto.ts index 5db98325..18b46dea 100644 --- a/src/structures/chats.dto.ts +++ b/src/structures/chats.dto.ts @@ -1,6 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { BooleanString } from '@waha/nestjs/validation/BooleanString'; -import { PaginationParams } from '@waha/structures/pagination.dto'; +import { + LimitOffsetParams, + PaginationParams, +} from '@waha/structures/pagination.dto'; import { ChatIdProperty } from '@waha/structures/properties.dto'; import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator'; @@ -122,6 +125,21 @@ export class PinMessageRequest { duration: number; } +export class OverviewPaginationParams extends LimitOffsetParams { + @IsNumber() + @IsOptional() + @Type(() => Number) + limit?: number = 20; +} + +export class ChatSummary { + id: string; + name: string | null; + picture: string | null; + lastMessage: any; + _chat: any; +} + /** * Events */ diff --git a/src/structures/pagination.dto.ts b/src/structures/pagination.dto.ts index 60ea2df6..214f03e2 100644 --- a/src/structures/pagination.dto.ts +++ b/src/structures/pagination.dto.ts @@ -6,8 +6,7 @@ export enum SortOrder { DESC = 'desc', ASC = 'asc', } - -export class PaginationParams { +export class LimitOffsetParams { @IsNumber() @IsOptional() @Type(() => Number) @@ -17,7 +16,9 @@ export class PaginationParams { @IsOptional() @Type(() => Number) offset?: number; +} +export class PaginationParams extends LimitOffsetParams { @ApiProperty({ description: 'Sort by field', })