From df3bc979888912dd992e9d28e994cab40e296282 Mon Sep 17 00:00:00 2001 From: devlikepro Date: Thu, 13 Jun 2024 15:45:25 +0700 Subject: [PATCH] [core] noweb store --- package.json | 4 + src/api/BufferJsonReplacerInterceptor.ts | 34 ++ src/core/app.module.core.ts | 6 + src/core/engines/noweb/session.noweb.core.ts | 270 ++++++++--- .../engines/noweb/store/IChatRepository.ts | 15 + .../engines/noweb/store/IContactRepository.ts | 13 + .../noweb/store/IMessagesRepository.ts | 17 + src/core/engines/noweb/store/INowebStorage.ts | 15 + src/core/engines/noweb/store/INowebStore.ts | 26 ++ .../engines/noweb/store/NowebInMemoryStore.ts | 54 +++ .../noweb/store/NowebPersistentStore.ts | 305 +++++++++++++ .../noweb/store/NowebStorageFactoryCore.ts | 9 + src/core/engines/noweb/store/Schema.ts | 56 +++ .../store/sqlite3/Sqlite3ChatRepository.ts | 17 + .../store/sqlite3/Sqlite3ContactRepository.ts | 8 + .../store/sqlite3/Sqlite3KVRepository.ts | 155 +++++++ .../sqlite3/Sqlite3MessagesRepository.ts | 48 ++ .../store/sqlite3/Sqlite3SchemaValidation.ts | 50 +++ .../noweb/store/sqlite3/Sqlite3Storage.ts | 107 +++++ src/core/engines/noweb/utils.ts | 71 +++ src/core/engines/webjs/session.webjs.core.ts | 2 +- src/structures/enums.dto.ts | 1 - src/structures/sessions.dto.ts | 30 ++ tsconfig.json | 5 +- yarn.lock | 425 +++++++++++++++++- 25 files changed, 1666 insertions(+), 77 deletions(-) create mode 100644 src/api/BufferJsonReplacerInterceptor.ts create mode 100644 src/core/engines/noweb/store/IChatRepository.ts create mode 100644 src/core/engines/noweb/store/IContactRepository.ts create mode 100644 src/core/engines/noweb/store/IMessagesRepository.ts create mode 100644 src/core/engines/noweb/store/INowebStorage.ts create mode 100644 src/core/engines/noweb/store/INowebStore.ts create mode 100644 src/core/engines/noweb/store/NowebInMemoryStore.ts create mode 100644 src/core/engines/noweb/store/NowebPersistentStore.ts create mode 100644 src/core/engines/noweb/store/NowebStorageFactoryCore.ts create mode 100644 src/core/engines/noweb/store/Schema.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3ContactRepository.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3KVRepository.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3MessagesRepository.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation.ts create mode 100644 src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts diff --git a/package.json b/package.json index 7a6e5f7c..68946798 100644 --- a/package.json +++ b/package.json @@ -33,15 +33,18 @@ "@nestjs/serve-static": "^2.1.3", "@nestjs/swagger": "^7.1.11", "@nestjs/terminus": "^10.2.3", + "@types/better-sqlite3": "^7.6.10", "@types/lodash": "^4.14.194", "@types/ws": "^8.5.4", "async-lock": "^1.4.1", + "better-sqlite3": "^11.0.0", "check-disk-space": "^3.4.0", "class-validator": "^0.12.2", "del": "^6.0.0", "express-basic-auth": "^1.2.1", "file-type": "16.5.4", "https-proxy-agent": "^7.0.0", + "knex": "^3.1.0", "libphonenumber-js": "^1.10.36", "link-preview-js": "^3.0.4", "lodash": "^4.17.21", @@ -57,6 +60,7 @@ "requestretry": "^4.1.1", "rimraf": "^3.0.2", "rxjs": "^7.1.0", + "sharp": "^0.33.4", "swagger-ui-express": "^4.1.4", "venom-bot": "5.0.1", "whatsapp-web.js": "https://github.com/devlikeapro/whatsapp-web.js#main-fork-update" diff --git a/src/api/BufferJsonReplacerInterceptor.ts b/src/api/BufferJsonReplacerInterceptor.ts new file mode 100644 index 00000000..1adc22cf --- /dev/null +++ b/src/api/BufferJsonReplacerInterceptor.ts @@ -0,0 +1,34 @@ +import { BufferJSON } from '@adiwajshing/baileys/lib/Utils'; +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + StreamableFile, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class BufferJsonReplacerInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => { + if (typeof data !== 'object' || data === null) { + return data; + } + // Buffer + if (Buffer.isBuffer(data) || data?.data || data?.url) { + return data; + } + + // StreamableFile + if (data instanceof StreamableFile) { + return data; + } + + return JSON.parse(JSON.stringify(data, BufferJSON.replacer)); + }), + ); + } +} diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index 8d21d312..bca6ed2e 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -1,8 +1,10 @@ import { ConsoleLogger, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_INTERCEPTOR } from '@nestjs/core'; import { PassportModule } from '@nestjs/passport'; import { ServeStaticModule } from '@nestjs/serve-static'; import { TerminusModule } from '@nestjs/terminus'; +import { BufferJsonReplacerInterceptor } from '@waha/api/BufferJsonReplacerInterceptor'; import { join } from 'path'; import { AuthController } from '../api/auth.controller'; @@ -82,6 +84,10 @@ const PROVIDERS = [ provide: WAHAHealthCheckService, useClass: WAHAHealthCheckServiceCore, }, + { + provide: APP_INTERCEPTOR, + useClass: BufferJsonReplacerInterceptor, + }, DashboardConfigServiceCore, SwaggerConfigServiceCore, WhatsappConfigService, diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index 8dc66747..45f9c5db 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, + Contact, DisconnectReason, extractMessageContent, getAggregateVotesInPollMessage, @@ -8,23 +9,33 @@ import makeWASocket, { isJidStatusBroadcast, jidNormalizedUser, makeCacheableSignalKeyStore, - makeInMemoryStore, + normalizeMessageContent, PresenceData, proto, WAMessageContent, WAMessageKey, } from '@adiwajshing/baileys'; +import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils'; 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 { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto'; +import { + PollVote, + PollVotePayload, + WAMessageAckBody, +} from '@waha/structures/webhooks.dto'; import * as Buffer from 'buffer'; import { Agent } from 'https'; import * as lodash from 'lodash'; -import { PairingCodeResponse } from 'src/structures/auth.dto'; +import { toNumber } from 'lodash'; -import { flipObject, splitAt } from '../../../helpers'; import { ChatRequest, CheckNumberStatusQuery, EditMessageRequest, + GetMessageQuery, MessageContactVcardRequest, MessageDestination, MessageFileRequest, @@ -40,7 +51,6 @@ import { SendSeenRequest, WANumberExistResult, } from '../../../structures/chatting.dto'; -import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto'; import { ACK_UNKNOWN, SECOND, @@ -64,11 +74,6 @@ import { } from '../../../structures/responses.dto'; import { MeInfo } from '../../../structures/sessions.dto'; import { BROADCAST_ID, TextStatus } from '../../../structures/status.dto'; -import { - PollVote, - PollVotePayload, - WAMessageAckBody, -} from '../../../structures/webhooks.dto'; import { IEngineMediaProcessor } from '../../abc/media.abc'; import { ensureSuffix, @@ -81,8 +86,12 @@ import { } from '../../exceptions'; import { toVcard } from '../../helpers'; import { createAgentProxy } from '../../helpers.proxy'; +import { buildLogger } from '../../manager.core'; import { QR } from '../../QR'; import { NowebAuthFactoryCore } from './NowebAuthFactoryCore'; +import { INowebStore } from './store/INowebStore'; +import { NowebPersistentStore } from './store/NowebPersistentStore'; +import { NowebStorageFactoryCore } from './store/NowebStorageFactoryCore'; import { extractMediaContent } from './utils'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -115,6 +124,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { engine = WAHAEngine.NOWEB; authFactory = new NowebAuthFactoryCore(); + storageFactory = new NowebStorageFactoryCore(); private startTimeoutId: null | ReturnType = null; private autoRestartTimeoutId: null | ReturnType = null; @@ -122,8 +132,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return true; } - sock: any; - store: any; + sock: ReturnType; + store: INowebStore; private qr: QR; public constructor(config) { @@ -137,6 +147,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } getSocketConfig(agent, state) { + const fullSyncEnabled = this.sessionConfig?.noweb?.store?.fullSync || false; + const browser = fullSyncEnabled + ? Browsers.ubuntu('Desktop') + : Browsers.ubuntu('Chrome'); return { agent: agent, fetchAgent: agent, @@ -146,11 +160,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { keys: makeCacheableSignalKeyStore(state.keys, logger), }, printQRInTerminal: false, - browser: Browsers.ubuntu('Chrome'), + browser: browser, logger: logger, mobile: false, defaultQueryTimeoutMs: undefined, getMessage: (key) => this.getMessage(key), + syncFullHistory: fullSyncEnabled, }; } @@ -161,8 +176,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { ); const agent = this.makeAgent(); const socketConfig = this.getSocketConfig(agent, state); - const sock: any = makeWASocket(socketConfig); - sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds); + const sock = makeWASocket(socketConfig); + sock.ev.on('creds.update', saveCreds); return sock; } @@ -173,14 +188,33 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return createAgentProxy(this.proxyConfig); } - connectStore() { + async connectStore() { this.log.debug(`Connecting store...`); if (!this.store) { - this.log.debug(`Making a new auth store...`); - this.store = makeInMemoryStore({ logger: logger }); + this.log.debug(`Making a new store...`); + const levels = getLogLevels(false); + const log = buildLogger(`NowebStore - ${this.name}`, levels); + const storeEnabled = this.sessionConfig?.noweb?.store?.enabled || false; + if (storeEnabled) { + this.log.debug('Using NowebPersistentStore'); + const storage = this.storageFactory.createStorage( + this.sessionStore, + this.name, + ); + this.store = new NowebPersistentStore(log, storage); + await this.store.init().catch((err) => { + this.log.error(`Failed to initialize storage or store: ${err}`); + this.status = WAHASessionStatus.FAILED; + this.end(); + throw err; + }); + } else { + this.log.debug('Using NowebInMemoryStore'); + this.store = new NowebInMemoryStore(); + } } this.log.debug(`Binding store to socket...`); - this.store.bind(this.sock.ev); + this.store.bind(this.sock.ev, this.sock); } resubscribeToKnownPresences() { @@ -191,10 +225,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { async buildClient() { this.sock = await this.makeSocket(); - this.connectStore(); + this.issueMessageUpdateOnEdits(); if (this.isDebugEnabled()) { this.listenEngineEventsInDebugMode(); } + await this.connectStore(); if (this.listenConnectionEventsFromTheStart) { this.listenConnectionEvents(); this.events.emit(WAHAInternalEvent.ENGINE_START); @@ -214,7 +249,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { this.autoRestartTimeoutId = setTimeout(() => { this.autoRestartTimeoutId = null; this.log.log('Auto-restarting the client connection...'); - this.sock?.end('auto-restart'); + this.sock?.end(new Error('auto-restart')); }, delay * SECOND); } @@ -252,7 +287,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { protected listenConnectionEvents() { this.log.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`); - this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, async (update) => { + this.sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr, isNewLogin } = update; if (isNewLogin) { this.restartClient(); @@ -263,6 +298,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return; } else if (connection === 'close') { const shouldReconnect = + // @ts-ignore: Property output does not exist on type 'Error' lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut; this.qr.save(''); @@ -297,12 +333,40 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return; } + private issueMessageUpdateOnEdits() { + // Remove it after it's been merged + // https://github.com/WhiskeySockets/Baileys/pull/855/ + this.sock.ev.on('messages.upsert', ({ messages }) => { + for (const message of messages) { + const content = normalizeMessageContent(message.message); + const protocolMsg = content?.protocolMessage; + if ( + protocolMsg !== null && + protocolMsg !== undefined && + protocolMsg.editedMessage + ) { + this.sock?.ev.emit('messages.update', [ + { + key: { + ...message.key, + id: protocolMsg.key.id, + }, + update: { message: protocolMsg.editedMessage }, + }, + ]); + } + } + }); + } + private async end() { clearTimeout(this.autoRestartTimeoutId); clearTimeout(this.startTimeoutId); + // @ts-ignore this.sock?.ev?.removeAllListeners(); this.sock?.ws?.removeAllListeners(); - this.sock?.end(); + this.sock?.end(undefined); + this.store?.close(); } async getSessionMeInfo(): Promise { @@ -392,7 +456,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } sendText(request: MessageTextRequest) { - const chatId = this.ensureSuffix(request.chatId); + const chatId = toJID(this.ensureSuffix(request.chatId)); const message = { text: request.text, mentions: request.mentions?.map(toJID), @@ -422,7 +486,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } async sendContactVCard(request: MessageContactVcardRequest) { - const chatId = this.ensureSuffix(request.chatId); + const chatId = toJID(this.ensureSuffix(request.chatId)); const contacts = request.contacts.map((el) => ({ vcard: toVcard(el) })); await this.sock.sendMessage(chatId, { contacts: { @@ -484,7 +548,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { sendLinkPreview(request: MessageLinkPreviewRequest) { const text = `${request.title}\n${request.url}`; - const chatId = this.ensureSuffix(request.chatId); + const chatId = toJID(this.ensureSuffix(request.chatId)); return this.sock.sendMessage(chatId, { text: text }); } @@ -506,6 +570,31 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return this.sock.sendPresenceUpdate('paused', request.chatId); } + async getMessages(query: GetMessageQuery) { + return this.getChatMessages(query.chatId, query.limit, query.downloadMedia); + } + + public async getChatMessages( + chatId: string, + limit: number, + downloadMedia: boolean, + ) { + downloadMedia = parseBool(downloadMedia); + const messages = await this.store.getMessagesByJid( + toJID(chatId), + toNumber(limit), + ); + const result = []; + for (const msg of messages) { + const wamsg = await this.processIncomingMessage(msg, downloadMedia); + if (!wamsg) { + continue; + } + result.push(wamsg); + } + return result; + } + async setReaction(request: MessageReactionRequest) { const key = parseMessageId(request.messageId); const reactionMessage = { @@ -530,15 +619,32 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { ); } + /** + * Chats methods + */ + + async getChats() { + const chats = await this.store.getChats(); + // Remove unreadCount, it's not ready yet + chats.forEach((chat) => delete chat.unreadCount); + return chats; + } + /** * Contacts methods */ - getContact(query: ContactQuery) { - throw new NotImplementedByEngineError(); + async getContact(query: ContactQuery) { + const jid = toJID(query.contactId); + const contact = await this.store.getContactById(jid); + if (!contact) { + return null; + } + return this.toWAContact(contact); } - getContacts() { - throw new NotImplementedByEngineError(); + async getContacts() { + const contacts = await this.store.getContacts(); + return contacts.map(this.toWAContact); } public async getContactAbout(query: ContactQuery) { @@ -667,14 +773,26 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { */ public sendTextStatus(status: TextStatus) { const message = { text: status.text }; + const JIDs = status.contacts.map(toJID); + this.upsertMeInJIDs(JIDs); const options = { backgroundColor: status.backgroundColor, font: status.font, - statusJidList: status.contacts.map(toJID), + statusJidList: JIDs, }; return this.sock.sendMessage(BROADCAST_ID, message, options); } + protected upsertMeInJIDs(JIDs: string[]) { + if (!this.sock?.authState?.creds?.me) { + return; + } + const myJID = jidNormalizedUser(this.sock.authState.creds.me.id); + if (!JIDs.includes(myJID)) { + JIDs.push(myJID); + } + } + /** * END - Methods for API */ @@ -682,23 +800,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { subscribeEngineEvent(event, handler): boolean { switch (event) { case WAHAEvents.MESSAGE: - this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { + this.sock.ev.on('messages.upsert', ({ messages }) => { this.handleIncomingMessages(messages, handler, false); }); return true; case WAHAEvents.MESSAGE_REACTION: - this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { + this.sock.ev.on('messages.upsert', ({ messages }) => { const reactions = this.processMessageReaction(messages); reactions.map(handler); }); return true; case WAHAEvents.MESSAGE_ANY: - this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => + this.sock.ev.on('messages.upsert', ({ messages }) => this.handleIncomingMessages(messages, handler, true), ); return true; case WAHAEvents.MESSAGE_ACK: // Direct message ack - this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => { + this.sock.ev.on('messages.update', (events) => { events .filter(isMine) .filter(isAckUpdateMessageEvent) @@ -706,7 +824,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { .forEach(handler); }); // Group message ack - this.sock.ev.on(BaileysEvents.MESSAGE_RECEIPT_UPDATE, (events) => { + this.sock.ev.on('message-receipt.update', (events) => { events .filter(isMine) .map(this.convertMessageReceiptUpdateToMessageAck) @@ -714,25 +832,25 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { }); return true; case WAHAEvents.STATE_CHANGE: - this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler); + this.sock.ev.on('connection.update', handler); return true; case WAHAEvents.GROUP_JOIN: - this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler); + this.sock.ev.on('groups.upsert', handler); return true; case WAHAEvents.PRESENCE_UPDATE: - this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) => + this.sock.ev.on('presence.update', (data) => handler(this.toWahaPresences(data.id, data.presences)), ); return true; case WAHAEvents.POLL_VOTE: - this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => { + this.sock.ev.on('messages.update', (events) => { events.forEach((event) => this.handleMessagesUpdatePollVote(event, handler), ); }); return true; case WAHAEvents.POLL_VOTE_FAILED: - this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { + this.sock.ev.on('messages.upsert', ({ messages }) => { messages.forEach((message) => this.handleMessageUpsertPollVoteFailed(message, handler), ); @@ -745,18 +863,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { private handleIncomingMessages(messages, handler, includeFromMe) { for (const message of messages) { - // if there is no text or media message - if (!message) return; - if (!message.message) return; - // Ignore reactions, we have dedicated handler for that - if (message.message.reactionMessage) return; - // Ignore poll votes, we have dedicated handler for that - if (message.message.pollUpdateMessage) return; // Do not include my messages if (!includeFromMe && message.key.fromMe) { continue; } - this.processIncomingMessage(message).then(handler); + this.processIncomingMessage(message).then((msg) => { + if (!msg) { + return; + } + handler(msg); + }); } } @@ -788,14 +904,32 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return reactions; } - private processIncomingMessage(message) { - return this.downloadMedia(message) - .then(this.toWAMessage) - .catch((error) => { - this.log.error('Failed to process incoming message'); - this.log.error(error); - console.trace(error); - }); + private async processIncomingMessage(message, downloadMedia = true) { + // if there is no text or media message + if (!message) return; + if (!message.message) return; + // Ignore reactions, we have dedicated handler for that + if (message.message.reactionMessage) return; + // Ignore poll votes, we have dedicated handler for that + if (message.message.pollUpdateMessage) return; + + if (downloadMedia) { + try { + message = await this.downloadMedia(message); + } catch (e) { + this.log.error('Failed when tried to download media for a message'); + this.log.error(e, e.stack); + } + } + + try { + return await this.toWAMessage(message); + } catch (error) { + this.log.error('Failed to process incoming message'); + this.log.error(error); + console.trace(error); + return null; + } } protected toWAMessage(message): Promise { @@ -813,6 +947,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { // @ts-ignore - AudioMessage doesn't have caption field body = mediaContent?.caption; } + const ack = message.ack || message.status - 1; return Promise.resolve({ id: id, timestamp: message.messageTimestamp, @@ -826,15 +961,24 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { media: message.media, mediaUrl: message.media?.url, // @ts-ignore - ack: message.ack, + ack: ack, // @ts-ignore - ackName: WAMessageAck[message.ack] || ACK_UNKNOWN, + ackName: WAMessageAck[ack] || ACK_UNKNOWN, location: message.location, vCards: message.vCards, _data: message, }); } + protected toWAContact(contact: Contact) { + contact.id = toCusFormat(contact.id); + // @ts-ignore + contact.pushname = contact.notify; + // @ts-ignore + delete contact.notify; + return contact; + } + protected convertMessageUpdateToMessageAck(event): WAMessageAckBody { const message = event; const fromToParticipant = getFromToParticipant(message); @@ -950,8 +1094,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } private toWahaPresences( - remoteJid, - storedPresences: PresenceData[], + remoteJid: string, + storedPresences: { [participant: string]: PresenceData }, ): WAHAChatPresences { const presences: WAHAPresenceData[] = []; for (const participant in storedPresences) { @@ -1014,6 +1158,9 @@ function toCusFormat(remoteJid) { if (isJidStatusBroadcast(remoteJid)) { return remoteJid; } + if (isLidUser(remoteJid)) { + return remoteJid; + } if (!remoteJid) { return; } @@ -1032,6 +1179,9 @@ export function toJID(chatId) { if (isJidGroup(chatId)) { return chatId; } + if (isJidStatusBroadcast(chatId)) { + return chatId; + } const number = chatId.split('@')[0]; return number + '@s.whatsapp.net'; } diff --git a/src/core/engines/noweb/store/IChatRepository.ts b/src/core/engines/noweb/store/IChatRepository.ts new file mode 100644 index 00000000..12688ec2 --- /dev/null +++ b/src/core/engines/noweb/store/IChatRepository.ts @@ -0,0 +1,15 @@ +import { Chat } from '@adiwajshing/baileys'; + +export interface IChatRepository { + getAll(): Promise; + + getAllWithMessages(): Promise; + + getById(id: string): Promise; + + deleteAll(): Promise; + + deleteById(id: string): Promise; + + save(chat: Chat): Promise; +} diff --git a/src/core/engines/noweb/store/IContactRepository.ts b/src/core/engines/noweb/store/IContactRepository.ts new file mode 100644 index 00000000..0ebf26fa --- /dev/null +++ b/src/core/engines/noweb/store/IContactRepository.ts @@ -0,0 +1,13 @@ +import { Contact } from '@adiwajshing/baileys'; + +export interface IContactRepository { + getAll(): Promise; + + getById(id: string): Promise; + + deleteAll(): Promise; + + deleteById(id: string): Promise; + + save(contact: Contact): Promise; +} diff --git a/src/core/engines/noweb/store/IMessagesRepository.ts b/src/core/engines/noweb/store/IMessagesRepository.ts new file mode 100644 index 00000000..77f514f9 --- /dev/null +++ b/src/core/engines/noweb/store/IMessagesRepository.ts @@ -0,0 +1,17 @@ +export interface IMessagesRepository { + deleteAll(): Promise; + + upsert(messages: any[]): Promise; + + upsertOne(message: any): Promise; + + getAllByJid(jid: string, limit: number): Promise; + + getByJidById(jid: string, id: string): Promise; + + updateByJidAndId(jid: string, id: string, update: any): Promise; + + deleteByJidByIds(jid: string, ids: string[]): Promise; + + deleteAllByJid(jid: string): Promise; +} diff --git a/src/core/engines/noweb/store/INowebStorage.ts b/src/core/engines/noweb/store/INowebStorage.ts new file mode 100644 index 00000000..8fd32ace --- /dev/null +++ b/src/core/engines/noweb/store/INowebStorage.ts @@ -0,0 +1,15 @@ +import { IChatRepository } from './IChatRepository'; +import { IContactRepository } from './IContactRepository'; +import { IMessagesRepository } from './IMessagesRepository'; + +export interface INowebStorage { + init(): Promise; + + close(): Promise; + + getContactsRepository(): IContactRepository; + + getChatRepository(): IChatRepository; + + getMessagesRepository(): IMessagesRepository; +} diff --git a/src/core/engines/noweb/store/INowebStore.ts b/src/core/engines/noweb/store/INowebStore.ts new file mode 100644 index 00000000..b14c119c --- /dev/null +++ b/src/core/engines/noweb/store/INowebStore.ts @@ -0,0 +1,26 @@ +import { + BaileysEventEmitter, + Chat, + Contact, + proto, +} from '@adiwajshing/baileys'; + +export interface INowebStore { + presences: any; + + init(): Promise; + + close(): Promise; + + bind(ev: BaileysEventEmitter, socket: any): void; + + loadMessage(jid: string, id: string): Promise; + + getMessagesByJid(chatId: string, limit: number): Promise; + + getChats(): Promise; + + getContacts(): Promise; + + getContactById(jid: string): Promise; +} diff --git a/src/core/engines/noweb/store/NowebInMemoryStore.ts b/src/core/engines/noweb/store/NowebInMemoryStore.ts new file mode 100644 index 00000000..2ee471f6 --- /dev/null +++ b/src/core/engines/noweb/store/NowebInMemoryStore.ts @@ -0,0 +1,54 @@ +import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys'; +import { BadRequestException, ConsoleLogger } from '@nestjs/common'; + +import { INowebStore } from './INowebStore'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const logger = require('pino')(); + +export class NowebInMemoryStore implements INowebStore { + private store: ReturnType; + errorMessage = + 'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' + + 'Read more: https://waha.devlike.pro/docs/engines/noweb#store'; + + constructor() { + this.store = makeInMemoryStore({ logger: logger }); + } + + init(): Promise { + return; + } + + close(): Promise { + return; + } + + get presences() { + return this.store.presences; + } + + bind(ev: any, socket: any) { + this.store.bind(ev); + } + + loadMessage(jid: string, id: string): Promise { + return this.store.loadMessage(jid, id); + } + + getMessagesByJid(chatId: string, limit: number): Promise { + throw new BadRequestException(this.errorMessage); + } + + getChats(): Promise { + throw new BadRequestException(this.errorMessage); + } + + getContacts(): Promise { + throw new BadRequestException(this.errorMessage); + } + + getContactById(jid: string): Promise { + throw new BadRequestException(this.errorMessage); + } +} diff --git a/src/core/engines/noweb/store/NowebPersistentStore.ts b/src/core/engines/noweb/store/NowebPersistentStore.ts new file mode 100644 index 00000000..a7129b9a --- /dev/null +++ b/src/core/engines/noweb/store/NowebPersistentStore.ts @@ -0,0 +1,305 @@ +import { + BaileysEventEmitter, + Chat, + ChatUpdate, + Contact, + isRealMessage, + jidNormalizedUser, + proto, + updateMessageWithReaction, + updateMessageWithReceipt, +} from '@adiwajshing/baileys'; +import { ConsoleLogger } from '@nestjs/common'; +import { toNumber } from 'lodash'; + +import { toJID } from '../session.noweb.core'; +import { IChatRepository } from './IChatRepository'; +import { IContactRepository } from './IContactRepository'; +import { IMessagesRepository } from './IMessagesRepository'; +import { INowebStorage } from './INowebStorage'; +import { INowebStore } from './INowebStore'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const AsyncLock = require('async-lock'); + +export class NowebPersistentStore implements INowebStore { + private socket: any; + private chatRepo: IChatRepository; + private contactRepo: IContactRepository; + private messagesRepo: IMessagesRepository; + public presences: any; + private lock: any; + + constructor( + private log: ConsoleLogger, + public storage: INowebStorage, + ) { + this.socket = null; + this.chatRepo = storage.getChatRepository(); + this.contactRepo = storage.getContactsRepository(); + this.messagesRepo = storage.getMessagesRepository(); + this.presences = {}; + this.lock = new AsyncLock({ maxPending: Infinity }); + } + + init(): Promise { + return this.storage.init(); + } + + bind(ev: BaileysEventEmitter, socket: any) { + // All + ev.on('messaging-history.set', (data) => this.onMessagingHistorySet(data)); + // Messages + ev.on('messages.upsert', (data) => + this.withLock('messages', () => this.onMessagesUpsert(data)), + ); + ev.on('messages.update', (data) => + this.withLock('messages', () => this.onMessageUpdate(data)), + ); + ev.on('messages.delete', (data) => + this.withLock('messages', () => this.onMessageDelete(data)), + ); + ev.on('messages.reaction', (data) => + this.withLock('messages', () => this.onMessageReaction(data)), + ); + ev.on('message-receipt.update', (data) => + this.withLock('messages', () => this.onMessageReceiptUpdate(data)), + ); + // Chats + ev.on('chats.upsert', (data) => + this.withLock('chats', () => this.onChatUpsert(data)), + ); + ev.on('chats.update', (data) => + this.withLock('chats', () => this.onChatUpdate(data)), + ); + ev.on('chats.delete', (data) => + this.withLock('chats', () => this.onChatDelete(data)), + ); + // Contacts + ev.on('contacts.upsert', (data) => + this.withLock('contacts', () => this.onContactsUpsert(data)), + ); + ev.on('contacts.update', (data) => + this.withLock('contacts', () => this.onContactUpdate(data)), + ); + // Presence + ev.on('presence.update', (data) => this.onPresenceUpdate(data)); + this.socket = socket; + } + + async close(): Promise { + await this.storage?.close().catch((error) => { + this.log.warn(`Failed to close storage: ${error}`); + }); + return; + } + + private async onMessagingHistorySet(history) { + const { contacts, chats, messages, isLatest } = history; + if (isLatest) { + this.log.debug( + 'history sync - clearing all entities, got latest history', + ); + await Promise.all([ + this.withLock('contacts', () => this.contactRepo.deleteAll()), + this.withLock('chats', () => this.chatRepo.deleteAll()), + this.withLock('messages', () => this.messagesRepo.deleteAll()), + ]); + } + + await Promise.all([ + this.withLock('contacts', async () => { + await this.onContactsUpsert(contacts); + this.log.log(`history sync - '${contacts.length}' synced contacts`); + }), + this.withLock('chats', () => this.onChatUpsert(chats)), + this.withLock('messages', () => this.syncMessagesHistory(messages)), + ]); + } + + private async syncMessagesHistory(messages) { + const realMessages = messages.filter(isRealMessage); + await this.messagesRepo.upsert(realMessages); + this.log.log( + `history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`, + ); + } + + private async onMessagesUpsert(update) { + const { messages, type } = update; + if (type !== 'notify' && type !== 'append') { + this.log.debug(`unexpected type for messages.upsert: '${type}'`); + return; + } + const realMessages = messages.filter(isRealMessage); + await this.messagesRepo.upsert(realMessages); + this.log.debug( + `messages.upsert - ${messages.length} got messages, ${realMessages.length} real messages`, + ); + } + + private async onMessageUpdate(updates) { + for (const update of updates) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const jid = jidNormalizedUser(update.key.remoteJid!); + const message = await this.messagesRepo.getByJidById(jid, update.key.id); + if (!message) { + continue; + } + const fields = { ...update.update }; + // It can overwrite the key, so we need to delete it + delete fields['key']; + Object.assign(message, fields); + // In case of revoked messages - remove it + // TODO: May be we should save the flag instead of completely removing the message + const isYetRealMessage = + isRealMessage(message, this.socket?.authState?.creds?.me?.id) || false; + if (isYetRealMessage) { + await this.messagesRepo.upsertOne(message); + } else { + await this.messagesRepo.deleteByJidByIds(jid, [update.key.id]); + } + } + } + + private async onMessageDelete(item) { + if ('all' in item) { + await this.messagesRepo.deleteAllByJid(item.jid); + return; + } + const jid = jidNormalizedUser(item.keys[0].remoteJid); + const ids = item.keys.map((key) => key.id); + await this.messagesRepo.deleteByJidByIds(jid, ids); + } + + private async onChatUpsert(chats: Chat[]) { + for (const chat of chats) { + delete chat['messages']; + chat.conversationTimestamp = toNumber(chat.conversationTimestamp); + await this.chatRepo.save(chat); + } + this.log.log(`history sync - '${chats.length}' synced chats`); + } + + private async onChatUpdate(updates: ChatUpdate[]) { + for (const update of updates) { + const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat); + Object.assign(chat, update); + chat.conversationTimestamp = toNumber(chat.conversationTimestamp); + delete chat['messages']; + await this.chatRepo.save(chat); + } + } + + private async onChatDelete(ids: string[]) { + for (const id of ids) { + await this.chatRepo.deleteById(id); + await this.messagesRepo.deleteAllByJid(id); + } + } + + private withLock(key, fn) { + return this.lock.acquire(key, fn); + } + + private async onContactsUpsert(contacts: Contact[]) { + for (const update of contacts) { + const contact = await this.contactRepo.getById(update.id); + // remove undefined from data + Object.keys(update).forEach( + (key) => update[key] === undefined && delete update[key], + ); + const result = { ...(contact || {}), ...update }; + await this.contactRepo.save(result); + } + } + + private async onContactUpdate(updates: Partial[]) { + for (const update of updates) { + const contact = await this.contactRepo.getById(update.id); + + if (!contact) { + this.log.warn( + `got update for non-existent contact. update: '${JSON.stringify( + update, + )}'`, + ); + continue; + // TODO: Find contact by hash if not found + // find contact by attrs.hash, when user is not saved as a contact + // check the in-memory for that + } + Object.assign(contact, update); + + if (update.imgUrl === 'changed') { + contact.imgUrl = this.socket + ? await this.socket?.profilePictureUrl(contact.id) + : undefined; + } else if (update.imgUrl === 'removed') { + delete contact.imgUrl; + } + await this.onContactsUpsert([contact]); + } + } + + private async onMessageReaction(reactions) { + for (const { key, reaction } of reactions) { + const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id); + if (!msg) { + this.log.warn( + `got reaction update for non-existent message. key: '${JSON.stringify( + key, + )}'`, + ); + continue; + } + updateMessageWithReaction(msg, reaction); + await this.messagesRepo.upsertOne(msg); + } + } + + private async onMessageReceiptUpdate(updates) { + for (const { key, receipt } of updates) { + const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id); + if (!msg) { + this.log.warn( + `got receipt update for non-existent message. key: '${JSON.stringify( + key, + )}'`, + ); + continue; + } + updateMessageWithReceipt(msg, receipt); + await this.messagesRepo.upsertOne(msg); + } + } + + private async onPresenceUpdate({ id, presences: update }) { + this.presences[id] = this.presences[id] || {}; + Object.assign(this.presences[id], update); + } + + async loadMessage(jid: string, id: string) { + const data = await this.messagesRepo.getByJidById(jid, id); + if (!data) { + return null; + } + return proto.WebMessageInfo.fromObject(data); + } + + getMessagesByJid(chatId: string, limit: number) { + return this.messagesRepo.getAllByJid(toJID(chatId), toNumber(limit)); + } + + getChats() { + return this.chatRepo.getAllWithMessages(); + } + + getContactById(jid) { + return this.contactRepo.getById(jid); + } + + getContacts() { + return this.contactRepo.getAll(); + } +} diff --git a/src/core/engines/noweb/store/NowebStorageFactoryCore.ts b/src/core/engines/noweb/store/NowebStorageFactoryCore.ts new file mode 100644 index 00000000..bab8d9d2 --- /dev/null +++ b/src/core/engines/noweb/store/NowebStorageFactoryCore.ts @@ -0,0 +1,9 @@ +import { DataStore } from '../../../abc/DataStore'; +import { INowebStorage } from './INowebStorage'; +import { Sqlite3Storage } from './sqlite3/Sqlite3Storage'; + +export class NowebStorageFactoryCore { + createStorage(store: DataStore, name: string): INowebStorage { + return new Sqlite3Storage(':memory:'); + } +} diff --git a/src/core/engines/noweb/store/Schema.ts b/src/core/engines/noweb/store/Schema.ts new file mode 100644 index 00000000..b74e4273 --- /dev/null +++ b/src/core/engines/noweb/store/Schema.ts @@ -0,0 +1,56 @@ +export class Field { + constructor( + public fieldName: string, + public type: string, + ) {} +} + +export class Index { + constructor( + public name: string, + public columns: string[], + ) {} +} + +export class Schema { + constructor( + public name: string, + public columns: Field[], + public indexes: Index[], + ) {} +} + +export const NOWEB_STORE_SCHEMA = [ + new Schema( + 'contacts', + [new Field('id', 'TEXT'), new Field('data', 'TEXT')], + [new Index('contacts_id_index', ['id'])], + ), + new Schema( + 'chats', + [ + new Field('id', 'TEXT'), + new Field('conversationTimestamp', 'INTEGER'), + new Field('data', 'TEXT'), + ], + [ + new Index('chats_id_index', ['id']), + new Index('chats_conversationTimestamp_index', ['conversationTimestamp']), + ], + ), + new Schema( + 'messages', + [ + new Field('jid', 'TEXT'), + new Field('id', 'TEXT'), + new Field('messageTimestamp', 'INTEGER'), + new Field('data', 'TEXT'), + ], + [ + new Index('messages_id_index', ['id']), + new Index('messages_jid_id_index', ['jid', 'id']), + new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']), + new Index('timestamp_index', ['messageTimestamp']), + ], + ), +]; diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts new file mode 100644 index 00000000..cf735071 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3ChatRepository.ts @@ -0,0 +1,17 @@ +import { Chat } from '@adiwajshing/baileys'; + +import { IChatRepository } from '../IChatRepository'; +import { Sqlite3KVRepository } from './Sqlite3KVRepository'; + +export class Sqlite3ChatRepository + extends Sqlite3KVRepository + implements IChatRepository +{ + getAllWithMessages(): Promise { + // Get chats with conversationTimestamp is not Null + const query = this.select() + .whereNotNull('conversationTimestamp') + .orderBy('conversationTimestamp', 'desc'); + return this.all(query); + } +} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3ContactRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3ContactRepository.ts new file mode 100644 index 00000000..9d6ec954 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3ContactRepository.ts @@ -0,0 +1,8 @@ +import { Contact } from '@adiwajshing/baileys'; + +import { IContactRepository } from '../IContactRepository'; +import { Sqlite3KVRepository } from './Sqlite3KVRepository'; + +export class Sqlite3ContactRepository + extends Sqlite3KVRepository + implements IContactRepository {} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3KVRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3KVRepository.ts new file mode 100644 index 00000000..9937128a --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3KVRepository.ts @@ -0,0 +1,155 @@ +import { BufferJSON } from '@adiwajshing/baileys/lib/Utils'; +import { + convertProtobufToPlainObject, + replaceLongsWithNumber, +} from '@waha/core/engines/noweb/utils'; +import { Database } from 'better-sqlite3'; +import Knex from 'knex'; + +import { Field, Schema } from '../Schema'; + +/** + * Key value repository with extra metadata + */ +export class Sqlite3KVRepository { + private UPSERT_BATCH_SIZE = 100; + protected db: Database; + + private readonly metadata: Map any>; + protected readonly table: string; + private readonly columns: Field[]; + private knex: Knex.Knex; + + constructor( + db: Database, + schema: Schema, + metadata: Map any> | null = null, + ) { + this.db = db; + this.columns = schema.columns; + this.table = schema.name; + this.metadata = metadata || new Map(); + + // sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert). + this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true }); + } + + getAll() { + return this.all(this.select()); + } + + protected async getBy(filters: any) { + const query = this.select().where(filters).limit(1); + return this.get(query); + } + + private dump(entity: Entity) { + const data = {}; + const raw = convertProtobufToPlainObject(entity); + replaceLongsWithNumber(raw); + for (const field of this.columns) { + const fn = this.metadata.get(field.fieldName); + if (fn) { + data[field.fieldName] = fn(raw); + } else if (field.fieldName == 'data') { + data['data'] = JSON.stringify(raw, BufferJSON.replacer); + } else { + data[field.fieldName] = raw[field.fieldName]; + } + } + return data; + } + + save(entity: Entity) { + return this.upsertOne(entity); + } + + async getById(id: string): Promise { + return this.getBy({ id: id }); + } + + async upsertOne(entity: Entity): Promise { + await this.upsertMany([entity]); + } + + async upsertMany(entities: Entity[]): Promise { + if (entities.length === 0) { + return; + } + const batchSize = this.UPSERT_BATCH_SIZE; + for (let i = 0; i < entities.length; i += batchSize) { + const batch = entities.slice(i, i + batchSize); + await this.upsertBatch(batch); + } + } + + private async upsertBatch(entities: Entity[]): Promise { + const data = entities.map((entity) => this.dump(entity)); + const keys = this.columns.map((c) => c.fieldName); + const values = data.map((d) => Object.values(d)).flat(); + try { + this.db + .prepare( + `INSERT INTO ${this.table} (${keys.join(', ')}) + VALUES ${data + .map(() => `(${keys.map(() => '?').join(', ')})`) + .join(', ')} + ON CONFLICT(id) DO UPDATE + SET ${keys.map((key) => `${key} = excluded.${key}`).join(', ')}`, + ) + .run(values); + } catch (err) { + console.error(`Error upserting data: ${err}, values: ${values}`); + throw err; + } + } + + protected async deleteBy(filters: any) { + const query = this.delete().where(filters); + await this.run(query); + } + + async deleteAll() { + const query = this.delete(); + await this.run(query); + } + + async deleteById(id: string) { + await this.deleteBy({ id: id }); + } + + /** + * SQL helpers + */ + + protected select() { + return this.knex.select().from(this.table); + } + + protected delete() { + return this.knex.delete().from(this.table); + } + + protected async all(query: any) { + const sql = query.toSQL().sql; + const bind = query.toSQL().bindings; + const rows: any[] = this.db.prepare(sql).all(bind); + return rows.map((row) => JSON.parse(row.data, BufferJSON.reviver)); + } + + protected async get(query: any) { + const sql = query.toSQL().sql; + const bind = query.toSQL().bindings; + const row: any = this.db.prepare(sql).get(bind); + if (!row) { + return null; + } + return JSON.parse(row.data, BufferJSON.reviver); + } + + protected async run(query: any) { + const sql = query.toSQL().sql; + const bind = query.toSQL().bindings; + return this.db.prepare(sql).run(bind); + } +} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3MessagesRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3MessagesRepository.ts new file mode 100644 index 00000000..d699e3c5 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3MessagesRepository.ts @@ -0,0 +1,48 @@ +import { IMessagesRepository } from '../IMessagesRepository'; +import { Sqlite3KVRepository } from './Sqlite3KVRepository'; + +export class Sqlite3MessagesRepository + extends Sqlite3KVRepository + implements IMessagesRepository +{ + upsert(messages: any[]): Promise { + return this.upsertMany(messages); + } + + getAllByJid(jid: string, limit: number): Promise { + const query = this.select() + .where({ jid: jid }) + .limit(limit) + .orderBy('messageTimestamp', 'DESC'); + return this.all(query); + } + + async getByJidById(jid: string, id: string): Promise { + return this.getBy({ jid: jid, id: id }); + } + + async updateByJidAndId( + jid: string, + id: string, + update: any, + ): Promise { + const entity = await this.getByJidById(jid, id); + if (!entity) { + return false; + } + Object.assign(entity, update); + await this.upsertOne(entity); + } + + async deleteByJidByIds(jid: string, ids: string[]): Promise { + const query = `DELETE + FROM ${this.table} + WHERE jid = ? + AND id IN (${ids.map(() => '?').join(', ')})`; + this.db.prepare(query).run([jid, ...ids]); + } + + deleteAllByJid(jid: string): Promise { + return this.deleteBy({ jid: jid }); + } +} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation.ts new file mode 100644 index 00000000..01f8e0b1 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation.ts @@ -0,0 +1,50 @@ +import { Schema } from '../Schema'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Database = require('better-sqlite3'); + +export class Sqlite3SchemaValidation { + constructor( + private table: Schema, + private db, + ) {} + + validate() { + const table = this.table; + + // Check table has the columns + const columns = this.db.prepare(`PRAGMA table_info(${table.name})`).all(); + // Check exact number of columns + if (columns.length !== table.columns.length) { + throw new Error( + `Table '${table.name}' does not have expected number of columns. Expected ${table.columns.length}, got ${columns.length}`, + ); + } + + // Check column + type + for (const column of table.columns) { + const columnInfo = columns.find((c) => c.name === column.fieldName); + if (!columnInfo) { + throw new Error( + `Table '${table.name}' does not have column '${column.fieldName}'`, + ); + } + if (columnInfo.type !== column.type) { + throw new Error( + `Table '${table.name}' column '${column.fieldName}' has type '${columnInfo.type}' but expected '${column.type}'`, + ); + } + } + + // Check table has expected indexes + const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all(); + const indexNames = indexes.map((index) => index.name); + for (const index of table.indexes) { + if (!indexNames.includes(index.name)) { + throw new Error( + `Table '${table.name}' does not have index '${index.name}'`, + ); + } + } + } +} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts new file mode 100644 index 00000000..20722289 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts @@ -0,0 +1,107 @@ +import { WAMessage } from '@adiwajshing/baileys'; + +import { INowebStorage } from '../INowebStorage'; +import { Field, Index, NOWEB_STORE_SCHEMA, Schema } from '../Schema'; +import { Sqlite3ChatRepository } from './Sqlite3ChatRepository'; +import { Sqlite3ContactRepository } from './Sqlite3ContactRepository'; +import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository'; +import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Database = require('better-sqlite3'); + +export class Sqlite3Storage implements INowebStorage { + private readonly db: any; + private readonly tables: Schema[]; + + constructor(filePath: string) { + this.db = new Database(filePath); + this.tables = NOWEB_STORE_SCHEMA; + } + + async init() { + this.db.pragma('journal_mode = WAL;'); + this.migrate(); + this.validateSchema(); + } + + private migrate() { + this.migration0001init(); + } + + private validateSchema() { + for (const table of this.tables) { + new Sqlite3SchemaValidation(table, this.db).validate(); + } + } + + private migration0001init() { + // Contacts + this.db.exec( + 'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)', + ); + this.db.exec( + 'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)', + ); + + // Chats + this.db.exec( + 'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, conversationTimestamp INTEGER, data TEXT)', + ); + this.db.exec( + 'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)', + ); + this.db.exec( + 'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)', + ); + + // Messages + this.db.exec( + 'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)', + ); + this.db.exec( + 'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)', + ); + this.db.exec( + 'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)', + ); + this.db.exec( + 'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, messageTimestamp)', + ); + this.db.exec( + 'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)', + ); + } + + async close() { + this.db.close(); + } + + getContactsRepository() { + return new Sqlite3ContactRepository(this.db, this.getSchema('contacts')); + } + + getChatRepository() { + return new Sqlite3ChatRepository(this.db, this.getSchema('chats')); + } + + getMessagesRepository() { + const metadata = new Map() + .set('jid', (msg: WAMessage) => msg.key.remoteJid) + .set('id', (msg: WAMessage) => msg.key.id) + .set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp); + return new Sqlite3MessagesRepository( + this.db, + this.getSchema('messages'), + metadata, + ); + } + + getSchema(name: string) { + const schema = this.tables.find((table) => table.name === name); + if (!schema) { + throw new Error(`Schema not found: ${name}`); + } + return schema; + } +} diff --git a/src/core/engines/noweb/utils.ts b/src/core/engines/noweb/utils.ts index 6bdea291..c7b9ab92 100644 --- a/src/core/engines/noweb/utils.ts +++ b/src/core/engines/noweb/utils.ts @@ -12,3 +12,74 @@ export function extractMediaContent( content?.stickerMessage; return mediaContent; } + +interface Long { + low: number; + high: number; + unsigned: boolean; + + toNumber?(): number; +} + +type AnyObject = { [key: string]: any }; + +export const replaceLongsWithNumber = (obj: AnyObject): void => { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (isObjectALong(obj[key])) { + obj[key] = toNumber(obj[key]); + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + replaceLongsWithNumber(obj[key]); + } + } + } +}; + +export function convertProtobufToPlainObject(obj: any): any { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + // Ignore Buffer + if ( + Buffer.isBuffer(obj) || + obj?.type == 'Buffer' || + obj instanceof Uint8Array + ) { + return obj; + } + + // Remove empty array + if (Array.isArray(obj) && obj.length === 0) { + return undefined; + } + + if (Array.isArray(obj)) { + return obj.map((item) => convertProtobufToPlainObject(item)); + } + + const plainObject: any = {}; + + Object.keys(obj).forEach((key) => { + const value = obj[key]; + plainObject[key] = convertProtobufToPlainObject(value); + }); + + return { ...plainObject }; +} + +const isObjectALong = (value: any): value is Long => { + return ( + value && + typeof value === 'object' && + 'low' in value && + 'high' in value && + 'unsigned' in value + ); +}; + +const toNumber = (longValue: Long): number => { + const { low, high, unsigned } = longValue; + const result = unsigned ? low >>> 0 : low + high * 0x100000000; + return result; +}; diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index a0abe069..9588e4ad 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -422,7 +422,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { } async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) { - downloadMedia = parseBool(downloadMedia) + downloadMedia = parseBool(downloadMedia); const chat: Chat = await this.whatsapp.getChatById( this.ensureSuffix(chatId), ); diff --git a/src/structures/enums.dto.ts b/src/structures/enums.dto.ts index b76eb3df..995a3e93 100644 --- a/src/structures/enums.dto.ts +++ b/src/structures/enums.dto.ts @@ -27,7 +27,6 @@ export enum WAHAEngine { VENOM = 'VENOM', WEBJS = 'WEBJS', NOWEB = 'NOWEB', - MOBILE = 'MOBILE', } export enum WAHAPresenceStatus { diff --git a/src/structures/sessions.dto.ts b/src/structures/sessions.dto.ts index 0add6ded..7584e275 100644 --- a/src/structures/sessions.dto.ts +++ b/src/structures/sessions.dto.ts @@ -37,6 +37,26 @@ export class ProxyConfig { password?: string; } +export class NowebStoreConfig { + @ApiProperty({ + description: + 'Enable or disable the store for contacts, chats, and messages.', + }) + enabled: boolean = false; + + @ApiProperty({ + description: + 'Enable full sync on session initialization (when scanning QR code).\n' + + 'Full sync will download all contacts, chats, and messages from the phone.\n' + + 'If disabled, only messages early than 90 days will be downloaded and some contacts may be missing.', + }) + fullSync: boolean = false; +} + +export class NowebConfig { + store?: NowebStoreConfig; +} + export class SessionConfig { webhooks?: WebhookConfig[]; @@ -46,6 +66,16 @@ export class SessionConfig { proxy?: ProxyConfig; debug: boolean = false; + + @ApiProperty({ + example: { + store: { + enabled: true, + fullSync: false, + }, + }, + }) + noweb?: NowebConfig; } export class SessionStartRequest { diff --git a/tsconfig.json b/tsconfig.json index ec7c754e..0ec648a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,10 @@ "target": "es2017", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", + "baseUrl": "./src", + "paths": { + "@waha/*": ["*"] + }, "incremental": true, "resolveJsonModule": true, "skipLibCheck": true diff --git a/yarn.lock b/yarn.lock index d693fdc7..759518f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,8 +13,8 @@ __metadata: linkType: hard "@adiwajshing/baileys@github:WhiskeySockets/Baileys": - version: 6.7.2 - resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=9065ab690fde0641e3eac458c1521e1d151c5050" + version: 6.7.5 + resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=76d2a9d759c31e4b2346fb2f02ec572f4d4c0437" dependencies: "@adiwajshing/keyed-db": ^0.2.4 "@hapi/boom": ^9.1.3 @@ -29,7 +29,7 @@ __metadata: node-cache: ^5.1.2 pino: ^7.0.0 protobufjs: ^7.2.4 - uuid: ^9.0.0 + uuid: ^10.0.0 ws: ^8.13.0 peerDependencies: jimp: ^0.16.1 @@ -45,7 +45,7 @@ __metadata: optional: true sharp: optional: true - checksum: 255c9aa3107a56055b33adbfba5cce41bad16d1b2453a19f3896438a85e191786c144ea73ebdb896f58c7436044aca8e740d5d9e7271c8df12f9e89318abf90f + checksum: 956723959cd4d37277ba3930f8f3a9deed8f5face0ffcf6d6a08d9a6f6276e6d4d92faffa8d713dfab18e3aaa765970dff9684ae92314946870a1ae661d93a88 languageName: node linkType: hard @@ -831,6 +831,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.1.1": + version: 1.2.0 + resolution: "@emnapi/runtime@npm:1.2.0" + dependencies: + tslib: ^2.4.0 + checksum: c9f5814f65a7851eda3fae96320b7ebfaf3b7e0db4e1ac2d77b55f5c0785e56b459a029413dbfc0abb1b23f059b850169888f92833150a28cdf24b9a53e535c5 + languageName: node + linkType: hard + "@eshaz/web-worker@npm:1.2.2": version: 1.2.2 resolution: "@eshaz/web-worker@npm:1.2.2" @@ -872,6 +881,181 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-arm64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-x64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm@npm:1.0.2" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.2" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-s390x@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-s390x": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-x64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.0.2 + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-wasm32@npm:0.33.4" + dependencies: + "@emnapi/runtime": ^1.1.1 + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-ia32@npm:0.33.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-x64@npm:0.33.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -1872,6 +2056,15 @@ __metadata: languageName: node linkType: hard +"@types/better-sqlite3@npm:^7.6.10": + version: 7.6.10 + resolution: "@types/better-sqlite3@npm:7.6.10" + dependencies: + "@types/node": "*" + checksum: 1e3c743f9bf23233ee6a51df4bbe8e9ba3d90c03e70b9ffe96e8c97174405c90a9672ec215c4d9c192ae7b7928bd0c0c83828f54d55ca55ea2642443c4cee422 + languageName: node + linkType: hard + "@types/body-parser@npm:*": version: 1.19.5 resolution: "@types/body-parser@npm:1.19.5" @@ -3412,6 +3605,17 @@ __metadata: languageName: node linkType: hard +"better-sqlite3@npm:^11.0.0": + version: 11.0.0 + resolution: "better-sqlite3@npm:11.0.0" + dependencies: + bindings: ^1.5.0 + node-gyp: latest + prebuild-install: ^7.1.1 + checksum: 7bf02ea6ba3253f53cb36bd872f86379ebf1a66cb103d39b91c610995c4396dde2b976bdf866f07eb7d67eb6f5433aa5490a6a4f4264244f83a53562f68832dc + languageName: node + linkType: hard + "big-integer@npm:^1.6.17": version: 1.6.52 resolution: "big-integer@npm:1.6.52" @@ -3443,6 +3647,15 @@ __metadata: languageName: node linkType: hard +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: 1.0.0 + checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7 + languageName: node + linkType: hard + "bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -4210,6 +4423,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:2.0.19": + version: 2.0.19 + resolution: "colorette@npm:2.0.19" + checksum: 888cf5493f781e5fcf54ce4d49e9d7d698f96ea2b2ef67906834bb319a392c667f9ec69f4a10e268d2946d13a9503d2d19b3abaaaf174e3451bfe91fb9d82427 + languageName: node + linkType: hard + "colorspace@npm:1.1.x": version: 1.1.4 resolution: "colorspace@npm:1.1.4" @@ -4236,6 +4456,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 436901d64a818295803c1996cd856621a74f30b9f9e28a588e726b2b1670665bccd7c1a77007ebf328729f0139838a88a19265858a0fa7a8728c4656796db948 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -4815,6 +5042,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.3": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 2ba6a939ae55f189aea996ac67afceb650413c7a34726ee92c40fb0deb2400d57ef94631a8a3f052055eea7efb0f99a9b5e6ce923415daa3e68221f963cfc27d + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -5873,6 +6107,13 @@ __metadata: languageName: node linkType: hard +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -6347,6 +6588,13 @@ __metadata: languageName: node linkType: hard +"getopts@npm:2.3.0": + version: 2.3.0 + resolution: "getopts@npm:2.3.0" + checksum: bbb5fcef8d4a8582cf4499ea3fc492d95322df2184e65d550ddacede04871e7ba33194c7abd06a6c5d540de3b70112a16f988787e236e1c66b89521032b398ce + languageName: node + linkType: hard + "getpass@npm:^0.1.1": version: 0.1.7 resolution: "getpass@npm:0.1.7" @@ -6912,6 +7160,13 @@ __metadata: languageName: node linkType: hard +"interpret@npm:^2.2.0": + version: 2.2.0 + resolution: "interpret@npm:2.2.0" + checksum: f51efef7cb8d02da16408ffa3504cd6053014c5aeb7bb8c223727e053e4235bf565e45d67028b0c8740d917c603807aa3c27d7bd2f21bf20b6417e2bb3e5fd6e + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -8074,6 +8329,45 @@ __metadata: languageName: node linkType: hard +"knex@npm:^3.1.0": + version: 3.1.0 + resolution: "knex@npm:3.1.0" + dependencies: + colorette: 2.0.19 + commander: ^10.0.0 + debug: 4.3.4 + escalade: ^3.1.1 + esm: ^3.2.25 + get-package-type: ^0.1.0 + getopts: 2.3.0 + interpret: ^2.2.0 + lodash: ^4.17.21 + pg-connection-string: 2.6.2 + rechoir: ^0.8.0 + resolve-from: ^5.0.0 + tarn: ^3.0.2 + tildify: 2.0.0 + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + bin: + knex: bin/cli.js + checksum: 3905f8d27960975f7f57f3f488d1ef3ccf47784acc8eb627e8a28cbbe1f296c6879c8ef0cbd9e17e867be80117d305cd948545f3fbd4c74b24c90d2413bbc021 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -9663,6 +9957,13 @@ __metadata: languageName: node linkType: hard +"pg-connection-string@npm:2.6.2": + version: 2.6.2 + resolution: "pg-connection-string@npm:2.6.2" + checksum: 22265882c3b6f2320785378d0760b051294a684989163d5a1cde4009e64e84448d7bf67d9a7b9e7f69440c3ee9e2212f9aa10dd17ad6773f6143c6020cebbcb5 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -10415,6 +10716,15 @@ __metadata: languageName: node linkType: hard +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" + dependencies: + resolve: ^1.20.0 + checksum: ad3caed8afdefbc33fbc30e6d22b86c35b3d51c2005546f4e79bcc03c074df804b3640ad18945e6bef9ed12caedc035655ec1082f64a5e94c849ff939dc0a788 + languageName: node + linkType: hard + "reflect-metadata@npm:^0.1.13": version: 0.1.14 resolution: "reflect-metadata@npm:0.1.14" @@ -10799,7 +11109,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.5.4": +"semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.2 resolution: "semver@npm:7.6.2" bin: @@ -10925,6 +11235,75 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.33.4": + version: 0.33.4 + resolution: "sharp@npm:0.33.4" + dependencies: + "@img/sharp-darwin-arm64": 0.33.4 + "@img/sharp-darwin-x64": 0.33.4 + "@img/sharp-libvips-darwin-arm64": 1.0.2 + "@img/sharp-libvips-darwin-x64": 1.0.2 + "@img/sharp-libvips-linux-arm": 1.0.2 + "@img/sharp-libvips-linux-arm64": 1.0.2 + "@img/sharp-libvips-linux-s390x": 1.0.2 + "@img/sharp-libvips-linux-x64": 1.0.2 + "@img/sharp-libvips-linuxmusl-arm64": 1.0.2 + "@img/sharp-libvips-linuxmusl-x64": 1.0.2 + "@img/sharp-linux-arm": 0.33.4 + "@img/sharp-linux-arm64": 0.33.4 + "@img/sharp-linux-s390x": 0.33.4 + "@img/sharp-linux-x64": 0.33.4 + "@img/sharp-linuxmusl-arm64": 0.33.4 + "@img/sharp-linuxmusl-x64": 0.33.4 + "@img/sharp-wasm32": 0.33.4 + "@img/sharp-win32-ia32": 0.33.4 + "@img/sharp-win32-x64": 0.33.4 + color: ^4.2.3 + detect-libc: ^2.0.3 + semver: ^7.6.0 + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: f5f91ce2a657128db9b45bc88781b1df185f91dffb16af12e76dc367b170a88353f8b0c406a93c7f110d9734b33a3c8b2d3faa6efb6508cdb5f382ffa36fdad0 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -11610,6 +11989,13 @@ __metadata: languageName: node linkType: hard +"tarn@npm:^3.0.2": + version: 3.0.2 + resolution: "tarn@npm:3.0.2" + checksum: 27a69658f02504979c5b02e500522e78ec12ef893b90cb00fdef794f9d847a92ed78f6c0ad12e82b8919519bded6a8d6d0000442cd0c6d6ea83cd9b7297729af + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^5.3.7": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" @@ -11687,6 +12073,13 @@ __metadata: languageName: node linkType: hard +"tildify@npm:2.0.0": + version: 2.0.0 + resolution: "tildify@npm:2.0.0" + checksum: 0f5fee93624c4afdf75ee224c3b65aece4817ba5317fd70f49eaf084ea720d73556a6ef3f50079425a773ba3b93805b4524d14057841d4e4336516fdbe80635b + languageName: node + linkType: hard + "timed-out@npm:^4.0.0": version: 4.0.1 resolution: "timed-out@npm:4.0.1" @@ -12307,6 +12700,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -12316,15 +12718,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -12451,6 +12844,7 @@ __metadata: "@nestjs/swagger": ^7.1.11 "@nestjs/terminus": ^10.2.3 "@nestjs/testing": ^9.0.9 + "@types/better-sqlite3": ^7.6.10 "@types/express": ^4.17.3 "@types/jest": 26.0.10 "@types/lodash": ^4.14.194 @@ -12460,6 +12854,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 3.9.1 "@typescript-eslint/parser": 3.9.1 async-lock: ^1.4.1 + better-sqlite3: ^11.0.0 check-disk-space: ^3.4.0 class-validator: ^0.12.2 del: ^6.0.0 @@ -12471,6 +12866,7 @@ __metadata: file-type: 16.5.4 https-proxy-agent: ^7.0.0 jest: ^29.7.0 + knex: ^3.1.0 libphonenumber-js: ^1.10.36 link-preview-js: ^3.0.4 lodash: ^4.17.21 @@ -12487,6 +12883,7 @@ __metadata: requestretry: ^4.1.1 rimraf: ^3.0.2 rxjs: ^7.1.0 + sharp: ^0.33.4 supertest: ^4.0.2 swagger-ui-express: ^4.1.4 ts-jest: ^29.1.3