diff --git a/src/api/lids.controller.ts b/src/api/lids.controller.ts new file mode 100644 index 00000000..def2e694 --- /dev/null +++ b/src/api/lids.controller.ts @@ -0,0 +1,101 @@ +import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + UnprocessableEntityException, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { WhatsappSession } from '@waha/core/abc/session.abc'; +import { + SessionApiParam, + WorkingSessionParam, +} from '@waha/nestjs/params/SessionApiParam'; +import { CountResponse } from '@waha/structures/base.dto'; +import { + LidsListQueryParams, + LidToPhoneNumber, +} from '@waha/structures/lids.dto'; +import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; +import { assignWith } from 'lodash'; + +import { SessionManager } from '../core/abc/manager.abc'; + +@ApiSecurity('api_key') +@Controller('api/:session/lids') +@ApiTags('👤 Contacts') +export class LidsController { + constructor(private manager: SessionManager) {} + + @Get('/') + @SessionApiParam + @ApiOperation({ summary: 'Get all known lids to phone number mapping' }) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async getAll( + @WorkingSessionParam session: WhatsappSession, + @Query() params: LidsListQueryParams, + ): Promise> { + // Always lid + const pagination: PaginationParams = params; + pagination.sortBy = 'lid'; + pagination.sortOrder = SortOrder.ASC; + const lids = await session.getAllLids(pagination); + return lids; + } + + @Get('/count') + @SessionApiParam + @ApiOperation({ summary: 'Get the number of known lids' }) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async getLidsCount( + @WorkingSessionParam session: WhatsappSession, + ): Promise { + const count = await session.getLidsCount(); + return { + count: count, + }; + } + + @Get('/:lid') + @SessionApiParam + @ApiOperation({ summary: 'Get phone number by lid' }) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async findPNByLid( + @WorkingSessionParam session: WhatsappSession, + @Param('lid') lid: string, + ): Promise { + if (!isLidUser(lid)) { + throw new UnprocessableEntityException( + 'Invalid LID - it must end with @lid', + ); + } + const result = await session.findPNByLid(lid); + result.pn = result.pn || null; + return result; + } + + @Get('/pn/:phoneNumber') + @SessionApiParam + @ApiOperation({ summary: 'Get lid by phone number (chat id)' }) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async findLIDByPhoneNumber( + @WorkingSessionParam session: WhatsappSession, + @Param('phoneNumber') phoneNumber: string, + ): Promise { + if (isLidUser(phoneNumber)) { + return { + lid: phoneNumber, + pn: null, + }; + } + + const result = await session.findLIDByPhoneNumber(phoneNumber); + result.lid = result.lid || null; + return result; + } +} diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index 948d38c2..667b1446 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -22,7 +22,11 @@ import { import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto'; import { BinaryFile, RemoteFile } from '@waha/structures/files.dto'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; -import { PaginationParams } from '@waha/structures/pagination.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; +import { + LimitOffsetParams, + PaginationParams, +} from '@waha/structures/pagination.dto'; import { MessageSource, WAMessage } from '@waha/structures/responses.dto'; import { DefaultMap } from '@waha/utils/DefaultMap'; import { generatePrefixedId } from '@waha/utils/ids'; @@ -627,6 +631,29 @@ export abstract class WhatsappSession { throw new NotImplementedByEngineError(); } + /** + * Lid to Phone Number methods + */ + public async getAllLids( + pagination: PaginationParams, + ): Promise> { + throw new NotImplementedByEngineError(); + } + + public async getLidsCount(): Promise { + throw new NotImplementedByEngineError(); + } + + public async findPNByLid(lid: string): Promise { + throw new NotImplementedByEngineError(); + } + + public async findLIDByPhoneNumber( + phoneNumber: string, + ): Promise { + throw new NotImplementedByEngineError(); + } + /** * Fetch the latest profile picture of the contact (group, newsletter, etc.) * @param id diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index 080e2802..067e58b6 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -5,6 +5,7 @@ import { PassportModule } from '@nestjs/passport'; import { ServeStaticModule } from '@nestjs/serve-static'; import { TerminusModule } from '@nestjs/terminus'; import { ChannelsController } from '@waha/api/channels.controller'; +import { LidsController } from '@waha/api/lids.controller'; import { ProfileController } from '@waha/api/profile.controller'; import { ServerController, @@ -130,6 +131,7 @@ export const CONTROLLERS = [ StatusController, LabelsController, ContactsController, + LidsController, GroupsController, PresenceController, ScreenshotController, diff --git a/src/core/engines/gows/grpc/gows.ts b/src/core/engines/gows/grpc/gows.ts index 4cf19ef6..af013b54 100644 --- a/src/core/engines/gows/grpc/gows.ts +++ b/src/core/engines/gows/grpc/gows.ts @@ -8720,6 +8720,76 @@ export namespace messages { return CancelEventMessageRequest.deserialize(bytes); } } + export class GetLidsRequest extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + session?: Session; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("session" in data && data.session != undefined) { + this.session = data.session; + } + } + } + get session() { + return pb_1.Message.getWrapperField(this, Session, 1) as Session; + } + set session(value: Session) { + pb_1.Message.setWrapperField(this, 1, value); + } + get has_session() { + return pb_1.Message.getField(this, 1) != null; + } + static fromObject(data: { + session?: ReturnType; + }): GetLidsRequest { + const message = new GetLidsRequest({}); + if (data.session != null) { + message.session = Session.fromObject(data.session); + } + return message; + } + toObject() { + const data: { + session?: ReturnType; + } = {}; + if (this.session != null) { + data.session = this.session.toObject(); + } + return data; + } + serialize(): Uint8Array; + serialize(w: pb_1.BinaryWriter): void; + serialize(w?: pb_1.BinaryWriter): Uint8Array | void { + const writer = w || new pb_1.BinaryWriter(); + if (this.has_session) + writer.writeMessage(1, this.session, () => this.session.serialize(writer)); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): GetLidsRequest { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new GetLidsRequest(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 1: + reader.readMessage(message.session, () => message.session = Session.deserialize(reader)); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): GetLidsRequest { + return GetLidsRequest.deserialize(bytes); + } + } interface GrpcUnaryServiceInterface { (message: P, metadata: grpc_1.Metadata, options: grpc_1.CallOptions, callback: grpc_1.requestCallback): grpc_1.ClientUnaryCall; (message: P, metadata: grpc_1.Metadata, callback: grpc_1.requestCallback): grpc_1.ClientUnaryCall; @@ -8841,6 +8911,42 @@ export namespace messages { responseSerialize: (message: Empty) => Buffer.from(message.serialize()), responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes)) }, + GetAllLids: { + path: "/messages.MessageService/GetAllLids", + requestStream: false, + responseStream: false, + requestSerialize: (message: GetLidsRequest) => Buffer.from(message.serialize()), + requestDeserialize: (bytes: Buffer) => GetLidsRequest.deserialize(new Uint8Array(bytes)), + responseSerialize: (message: JsonList) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => JsonList.deserialize(new Uint8Array(bytes)) + }, + GetLidsCount: { + path: "/messages.MessageService/GetLidsCount", + requestStream: false, + responseStream: false, + requestSerialize: (message: Session) => Buffer.from(message.serialize()), + requestDeserialize: (bytes: Buffer) => Session.deserialize(new Uint8Array(bytes)), + responseSerialize: (message: OptionalUInt64) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => OptionalUInt64.deserialize(new Uint8Array(bytes)) + }, + FindPNByLid: { + path: "/messages.MessageService/FindPNByLid", + requestStream: false, + responseStream: false, + requestSerialize: (message: EntityByIdRequest) => Buffer.from(message.serialize()), + requestDeserialize: (bytes: Buffer) => EntityByIdRequest.deserialize(new Uint8Array(bytes)), + responseSerialize: (message: OptionalString) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => OptionalString.deserialize(new Uint8Array(bytes)) + }, + FindLIDByPhoneNumber: { + path: "/messages.MessageService/FindLIDByPhoneNumber", + requestStream: false, + responseStream: false, + requestSerialize: (message: EntityByIdRequest) => Buffer.from(message.serialize()), + requestDeserialize: (bytes: Buffer) => EntityByIdRequest.deserialize(new Uint8Array(bytes)), + responseSerialize: (message: OptionalString) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => OptionalString.deserialize(new Uint8Array(bytes)) + }, FetchGroups: { path: "/messages.MessageService/FetchGroups", requestStream: false, @@ -9292,6 +9398,10 @@ export namespace messages { abstract SetProfileName(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract SetProfileStatus(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract SetProfilePicture(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract GetAllLids(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract GetLidsCount(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract FindPNByLid(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract FindLIDByPhoneNumber(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract FetchGroups(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract GetGroups(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract GetGroupInfo(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; @@ -9370,6 +9480,18 @@ export namespace messages { SetProfilePicture: GrpcUnaryServiceInterface = (message: SetProfilePictureRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { return super.SetProfilePicture(message, metadata, options, callback); }; + GetAllLids: GrpcUnaryServiceInterface = (message: GetLidsRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + return super.GetAllLids(message, metadata, options, callback); + }; + GetLidsCount: GrpcUnaryServiceInterface = (message: Session, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + return super.GetLidsCount(message, metadata, options, callback); + }; + FindPNByLid: GrpcUnaryServiceInterface = (message: EntityByIdRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + return super.FindPNByLid(message, metadata, options, callback); + }; + FindLIDByPhoneNumber: GrpcUnaryServiceInterface = (message: EntityByIdRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + return super.FindLIDByPhoneNumber(message, metadata, options, callback); + }; FetchGroups: GrpcUnaryServiceInterface = (message: Session, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { return super.FetchGroups(message, metadata, options, callback); }; diff --git a/src/core/engines/gows/grpc/gows_grpc_pb.js b/src/core/engines/gows/grpc/gows_grpc_pb.js index 1f469a9a..db564365 100644 --- a/src/core/engines/gows/grpc/gows_grpc_pb.js +++ b/src/core/engines/gows/grpc/gows_grpc_pb.js @@ -202,6 +202,17 @@ function deserialize_messages_GetLabelsRequest(buffer_arg) { return gows_pb.GetLabelsRequest.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_messages_GetLidsRequest(arg) { + if (!(arg instanceof gows_pb.GetLidsRequest)) { + throw new Error('Expected argument of type messages.GetLidsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_messages_GetLidsRequest(buffer_arg) { + return gows_pb.GetLidsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_messages_GetMessagesRequest(arg) { if (!(arg instanceof gows_pb.GetMessagesRequest)) { throw new Error('Expected argument of type messages.GetMessagesRequest'); @@ -433,6 +444,17 @@ function deserialize_messages_OptionalString(buffer_arg) { return gows_pb.OptionalString.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_messages_OptionalUInt64(arg) { + if (!(arg instanceof gows_pb.OptionalUInt64)) { + throw new Error('Expected argument of type messages.OptionalUInt64'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_messages_OptionalUInt64(buffer_arg) { + return gows_pb.OptionalUInt64.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_messages_PairCodeRequest(arg) { if (!(arg instanceof gows_pb.PairCodeRequest)) { throw new Error('Expected argument of type messages.PairCodeRequest'); @@ -746,6 +768,53 @@ setProfileName: { responseDeserialize: deserialize_messages_Empty, }, // +// Lids +// +getAllLids: { + path: '/messages.MessageService/GetAllLids', + requestStream: false, + responseStream: false, + requestType: gows_pb.GetLidsRequest, + responseType: gows_pb.JsonList, + requestSerialize: serialize_messages_GetLidsRequest, + requestDeserialize: deserialize_messages_GetLidsRequest, + responseSerialize: serialize_messages_JsonList, + responseDeserialize: deserialize_messages_JsonList, + }, + getLidsCount: { + path: '/messages.MessageService/GetLidsCount', + requestStream: false, + responseStream: false, + requestType: gows_pb.Session, + responseType: gows_pb.OptionalUInt64, + requestSerialize: serialize_messages_Session, + requestDeserialize: deserialize_messages_Session, + responseSerialize: serialize_messages_OptionalUInt64, + responseDeserialize: deserialize_messages_OptionalUInt64, + }, + findPNByLid: { + path: '/messages.MessageService/FindPNByLid', + requestStream: false, + responseStream: false, + requestType: gows_pb.EntityByIdRequest, + responseType: gows_pb.OptionalString, + requestSerialize: serialize_messages_EntityByIdRequest, + requestDeserialize: deserialize_messages_EntityByIdRequest, + responseSerialize: serialize_messages_OptionalString, + responseDeserialize: deserialize_messages_OptionalString, + }, + findLIDByPhoneNumber: { + path: '/messages.MessageService/FindLIDByPhoneNumber', + requestStream: false, + responseStream: false, + requestType: gows_pb.EntityByIdRequest, + responseType: gows_pb.OptionalString, + requestSerialize: serialize_messages_EntityByIdRequest, + requestDeserialize: deserialize_messages_EntityByIdRequest, + responseSerialize: serialize_messages_OptionalString, + responseDeserialize: deserialize_messages_OptionalString, + }, + // // Groups // fetchGroups: { diff --git a/src/core/engines/gows/grpc/gows_pb.js b/src/core/engines/gows/grpc/gows_pb.js index d615721d..b5d02bee 100644 --- a/src/core/engines/gows/grpc/gows_pb.js +++ b/src/core/engines/gows/grpc/gows_pb.js @@ -43,6 +43,7 @@ goog.exportSymbol('proto.messages.EventMessage', null, global); goog.exportSymbol('proto.messages.GetChatsRequest', null, global); goog.exportSymbol('proto.messages.GetContactsRequest', null, global); goog.exportSymbol('proto.messages.GetLabelsRequest', null, global); +goog.exportSymbol('proto.messages.GetLidsRequest', null, global); goog.exportSymbol('proto.messages.GetMessagesRequest', null, global); goog.exportSymbol('proto.messages.GetNewsletterMessagesByInviteRequest', null, global); goog.exportSymbol('proto.messages.GroupCodeRequest', null, global); @@ -1766,6 +1767,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.messages.CancelEventMessageRequest.displayName = 'proto.messages.CancelEventMessageRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.messages.GetLidsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.messages.GetLidsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.messages.GetLidsRequest.displayName = 'proto.messages.GetLidsRequest'; +} @@ -18154,6 +18176,157 @@ proto.messages.CancelEventMessageRequest.prototype.setMessageid = function(value }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.messages.GetLidsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.messages.GetLidsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.messages.GetLidsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.GetLidsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.messages.GetLidsRequest} + */ +proto.messages.GetLidsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.messages.GetLidsRequest; + return proto.messages.GetLidsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.messages.GetLidsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.messages.GetLidsRequest} + */ +proto.messages.GetLidsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.messages.Session; + reader.readMessage(value,proto.messages.Session.deserializeBinaryFromReader); + msg.setSession(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.messages.GetLidsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.messages.GetLidsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.messages.GetLidsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.GetLidsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSession(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.messages.Session.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Session session = 1; + * @return {?proto.messages.Session} + */ +proto.messages.GetLidsRequest.prototype.getSession = function() { + return /** @type{?proto.messages.Session} */ ( + jspb.Message.getWrapperField(this, proto.messages.Session, 1)); +}; + + +/** + * @param {?proto.messages.Session|undefined} value + * @return {!proto.messages.GetLidsRequest} returns this +*/ +proto.messages.GetLidsRequest.prototype.setSession = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.messages.GetLidsRequest} returns this + */ +proto.messages.GetLidsRequest.prototype.clearSession = function() { + return this.setSession(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.messages.GetLidsRequest.prototype.hasSession = function() { + return jspb.Message.getField(this, 1) != null; +}; + + /** * @enum {number} */ diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index 9d36c8c1..9d9f8e7c 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -150,6 +150,7 @@ import { AckToStatus } from '@waha/core/utils/acks'; import { ParseEventResponseType } from '@waha/core/utils/events'; import { DistinctAck } from '@waha/core/utils/reactive'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { exclude } from '@waha/utils/reactive/ops/exclude'; import * as lodash from 'lodash'; @@ -1406,6 +1407,62 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { return data.map(this.toWAContact.bind(this)); } + /** + * Lid to Phone Number methods + */ + public async getAllLids( + pagination: PaginationParams, + ): Promise> { + const request = new messages.GetLidsRequest({ + session: this.session, + }); + const response = await promisify(this.client.GetAllLids)(request); + const data = parseJsonList(response); + + const lids = data.map((item) => ({ + lid: item.lid, + pn: toCusFormat(item.pn), + })); + + // Use in-memory pagination + const paginator = new PaginatorInMemory(pagination); + return paginator.apply(lids); + } + + public async getLidsCount(): Promise { + const response = await promisify(this.client.GetLidsCount)(this.session); + return response?.value; + } + + public async findPNByLid(lid: string): Promise { + const request = new messages.EntityByIdRequest({ + session: this.session, + id: lid, + }); + const response = await promisify(this.client.FindPNByLid)(request); + const phoneNumber = response?.value; + return { + lid: lid, + pn: phoneNumber ? toCusFormat(phoneNumber) : null, + }; + } + + public async findLIDByPhoneNumber( + phoneNumber: string, + ): Promise { + const pn = toJID(phoneNumber); + const request = new messages.EntityByIdRequest({ + session: this.session, + id: pn, + }); + const response = await promisify(this.client.FindLIDByPhoneNumber)(request); + const lid = response.value; + return { + lid: lid || null, + pn: toCusFormat(pn), + }; + } + /** * Chats methods */ diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index f6ddaa8c..857b6502 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -138,6 +138,7 @@ import { LabelDTO, LabelID, } from '@waha/structures/labels.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { ReplyToMessage } from '@waha/structures/message.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { @@ -1339,6 +1340,44 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { throw new NotImplementedByEngineError(); } + /** + * Lid to Phone Number methods + */ + public async getAllLids( + pagination: PaginationParams, + ): Promise> { + const lids = await this.store.getAllLids(pagination); + return lids.map((value) => { + return { + lid: value.lid, + pn: toCusFormat(value.pn), + }; + }); + } + + public async getLidsCount(): Promise { + return this.store.getLidsCount(); + } + + public async findPNByLid(lid: string): Promise { + const pn = await this.store.findPNByLid(lid); + return { + lid: lid, + pn: pn ? toCusFormat(pn) : null, + }; + } + + public async findLIDByPhoneNumber( + phoneNumber: string, + ): Promise { + const pn = toJID(phoneNumber); + const lid = await this.store.findLidByPN(pn); + return { + lid: lid || null, + pn: toCusFormat(pn), + }; + } + /** * Group methods */ diff --git a/src/core/engines/noweb/store/INowebLidPNRepository.ts b/src/core/engines/noweb/store/INowebLidPNRepository.ts new file mode 100644 index 00000000..e70bf529 --- /dev/null +++ b/src/core/engines/noweb/store/INowebLidPNRepository.ts @@ -0,0 +1,33 @@ +import { LimitOffsetParams } from '@waha/structures/pagination.dto'; + +export class LidToPN { + id: string; + pn: string; +} + +export interface INowebLidPNRepository { + /** + * Save lid to phone number mapping + */ + saveLids(lids: LidToPN[]): Promise; + + /** + * Get all lids with pagination + */ + getAllLids(pagination?: LimitOffsetParams): Promise; + + /** + * Get a total count of lids + */ + getLidsCount(): Promise; + + /** + * Find a phone number by lid + */ + findPNByLid(lid: string): Promise; + + /** + * Find lid by phone number + */ + findLidByPN(pn: string): Promise; +} diff --git a/src/core/engines/noweb/store/INowebStorage.ts b/src/core/engines/noweb/store/INowebStorage.ts index 84f53ab8..17011f60 100644 --- a/src/core/engines/noweb/store/INowebStorage.ts +++ b/src/core/engines/noweb/store/INowebStorage.ts @@ -5,6 +5,7 @@ import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsReposit import { IChatRepository } from './IChatRepository'; import { IContactRepository } from './IContactRepository'; import { IMessagesRepository } from './IMessagesRepository'; +import { INowebLidPNRepository } from './INowebLidPNRepository'; export abstract class INowebStorage { abstract init(): Promise; @@ -22,4 +23,6 @@ export abstract class INowebStorage { abstract getLabelsRepository(): ILabelsRepository; abstract getLabelAssociationRepository(): ILabelAssociationRepository; + + abstract getLidPNRepository(): INowebLidPNRepository; } diff --git a/src/core/engines/noweb/store/INowebStore.ts b/src/core/engines/noweb/store/INowebStore.ts index b8616293..5e7501b8 100644 --- a/src/core/engines/noweb/store/INowebStore.ts +++ b/src/core/engines/noweb/store/INowebStore.ts @@ -7,7 +7,11 @@ import { import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata'; import { Label } from '@adiwajshing/baileys/lib/Types/Label'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; -import { PaginationParams } from '@waha/structures/pagination.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; +import { + LimitOffsetParams, + PaginationParams, +} from '@waha/structures/pagination.dto'; export interface INowebStore { presences: any; @@ -47,4 +51,13 @@ export interface INowebStore { getGroups(pagination: PaginationParams): Promise; resetGroupsCache(): void; + + // Lid Repository methods + getAllLids(pagination?: LimitOffsetParams): Promise; + + getLidsCount(): Promise; + + findPNByLid(lid: string): Promise; + + findLidByPN(pn: string): Promise; } diff --git a/src/core/engines/noweb/store/NowebInMemoryStore.ts b/src/core/engines/noweb/store/NowebInMemoryStore.ts index 7f8ab5ef..32526899 100644 --- a/src/core/engines/noweb/store/NowebInMemoryStore.ts +++ b/src/core/engines/noweb/store/NowebInMemoryStore.ts @@ -7,7 +7,11 @@ import makeWASocket, { import { Label } from '@adiwajshing/baileys/lib/Types/Label'; import { BadRequestException } from '@nestjs/common'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; -import { PaginationParams } from '@waha/structures/pagination.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; +import { + LimitOffsetParams, + PaginationParams, +} from '@waha/structures/pagination.dto'; import { PaginatorInMemory } from '@waha/utils/Paginator'; import { INowebStore } from './INowebStore'; @@ -107,4 +111,23 @@ export class NowebInMemoryStore implements INowebStore { resetGroupsCache() { return; } + + // + // Lids methods + // + getAllLids(pagination?: LimitOffsetParams): Promise { + throw new BadRequestException(this.errorMessage); + } + + findLidByPN(pn: string): Promise { + throw new BadRequestException(this.errorMessage); + } + + findPNByLid(lid: string): Promise { + throw new BadRequestException(this.errorMessage); + } + + getLidsCount(): 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 79a7e8e4..2b5cf54f 100644 --- a/src/core/engines/noweb/store/NowebPersistentStore.ts +++ b/src/core/engines/noweb/store/NowebPersistentStore.ts @@ -5,6 +5,7 @@ import makeWASocket, { ChatUpdate, Contact, GroupParticipant, + isJidUser, isRealMessage, jidNormalizedUser, ParticipantAction, @@ -18,13 +19,19 @@ import { LabelAssociation, LabelAssociationType, } from '@adiwajshing/baileys/lib/Types/LabelAssociation'; +import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils'; import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository'; import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository'; import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; -import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; +import { + LimitOffsetParams, + PaginationParams, + SortOrder, +} from '@waha/structures/pagination.dto'; import { DefaultMap } from '@waha/utils/DefaultMap'; -import { sleep, waitUntil } from '@waha/utils/promiseTimeout'; +import { waitUntil } from '@waha/utils/promiseTimeout'; import * as lodash from 'lodash'; import { toNumber } from 'lodash'; import { Logger } from 'pino'; @@ -32,6 +39,7 @@ import { Logger } from 'pino'; import { IChatRepository } from './IChatRepository'; import { IContactRepository } from './IContactRepository'; import { IMessagesRepository } from './IMessagesRepository'; +import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository'; import { INowebStorage } from './INowebStorage'; import { INowebStore } from './INowebStore'; @@ -49,6 +57,7 @@ export class NowebPersistentStore implements INowebStore { private messagesRepo: IMessagesRepository; private labelsRepo: ILabelsRepository; private labelAssociationsRepo: ILabelAssociationRepository; + private lidRepo: INowebLidPNRepository; public presences: any; private lock: any = new AsyncLock({ @@ -76,6 +85,7 @@ export class NowebPersistentStore implements INowebStore { this.messagesRepo = storage.getMessagesRepository(); this.labelsRepo = storage.getLabelsRepository(); this.labelAssociationsRepo = storage.getLabelAssociationRepository(); + this.lidRepo = storage.getLidPNRepository(); this.presences = {}; } @@ -116,9 +126,16 @@ export class NowebPersistentStore implements INowebStore { ev.on('groups.upsert', (data) => this.withLock('groups', () => this.onGroupUpsert(data)), ); - ev.on('groups.update', (data) => - this.withLock('groups', () => this.onGroupUpdate(data)), - ); + ev.on('groups.update', (data) => { + this.withLock('groups', () => this.onGroupUpdate(data)); + this.withLock('lids', async () => { + const participants = lodash.flatMap(data, (g) => g?.participants || []); + const lids = await this.handleLidPNUpdates(participants); + this.logger.debug( + `groups.update - '${lids.length}' synced lid to pn mapping`, + ); + }); + }); ev.on('group-participants.update', (data) => this.withLock(`group-${data.id}`, () => this.onGroupParticipantsUpdate(data), @@ -126,12 +143,24 @@ export class NowebPersistentStore implements INowebStore { ); // Contacts - ev.on('contacts.upsert', (data) => - this.withLock('contacts', () => this.onContactsUpsert(data)), - ); - ev.on('contacts.update', (data) => - this.withLock('contacts', () => this.onContactUpdate(data)), - ); + ev.on('contacts.upsert', (data) => { + this.withLock('contacts', () => this.onContactsUpsert(data)); + this.withLock('lids', async () => { + const lids = await this.handleLidPNUpdates(data); + this.logger.debug( + `contacts.upsert - '${lids.length}' synced lid to pn mapping`, + ); + }); + }); + ev.on('contacts.update', (data) => { + this.withLock('contacts', () => this.onContactUpdate(data)); + this.withLock('lids', async () => { + const lids = await this.handleLidPNUpdates(data); + this.logger.debug( + `contacts.update - '${lids.length}' synced lid to pn mapping`, + ); + }); + }); ev.on('labels.edit', (data) => this.onLabelsEdit(data)); ev.on('labels.association', ({ association, type }) => this.onLabelsAssociation(association, type), @@ -166,6 +195,12 @@ export class NowebPersistentStore implements INowebStore { await this.onContactsUpsert(contacts); this.logger.info(`history sync - '${contacts.length}' synced contacts`); }), + this.withLock('lids', async () => { + const lids = await this.handleLidPNUpdates(contacts); + this.logger.info( + `history sync - '${lids.length}' synced lid to pn mapping`, + ); + }), this.withLock('chats', () => this.onChatUpsert(chats)), this.withLock('messages', () => this.syncMessagesHistory(messages)), ]); @@ -563,4 +598,64 @@ export class NowebPersistentStore implements INowebStore { const ids = associations.map((association) => association.labelId); return await this.labelsRepo.getAllByIds(ids); } + + // + // Lid methods + // + private async handleLidPNUpdates(contacts: Array>) { + let lids: LidToPN[] = []; + for (const contact of contacts) { + // contact.id = pn, contact.lid = lid + if (isJidUser(contact.id) && isLidUser(contact.lid)) { + lids.push({ + pn: contact.id, + id: contact.lid, + }); + } + // contact.pn = pn, contact.lid = lid + else if (isJidUser(contact.pn) && isLidUser(contact.lid)) { + lids.push({ + pn: contact.pn, + id: contact.lid, + }); + } + // contact.pn = pn, contact.id = lid + else if (isJidUser(contact.pn) && isLidUser(contact.id)) { + lids.push({ + pn: contact.pn, + id: contact.id, + }); + } + } + // make lids unique by id + lids = lodash.uniqBy(lids, 'id'); + if (lids.length > 0) { + await this.lidRepo.saveLids(lids); + } + return lids; + } + + async getAllLids( + pagination?: LimitOffsetParams, + ): Promise { + const lids = await this.lidRepo.getAllLids(pagination); + return lids.map((value) => { + return { + lid: value.id, + pn: value.pn, + }; + }); + } + + getLidsCount(): Promise { + return this.lidRepo.getLidsCount(); + } + + findPNByLid(lid: string): Promise { + return this.lidRepo.findPNByLid(lid); + } + + findLidByPN(pn: string): Promise { + return this.lidRepo.findLidByPN(pn); + } } diff --git a/src/core/engines/noweb/store/schemas.ts b/src/core/engines/noweb/store/schemas.ts index cbf67aab..b20a8f7c 100644 --- a/src/core/engines/noweb/store/schemas.ts +++ b/src/core/engines/noweb/store/schemas.ts @@ -65,6 +65,15 @@ export const NowebLabelAssociationsSchema = new Schema( ], ); +export const NowebLidMapSchema = new Schema( + 'lid_map', + [new Field('id', 'TEXT'), new Field('pn', 'TEXT'), new Field('data', 'TEXT')], + [ + new Index('lid_map_id_index', ['id']), + new Index('lid_map_pn_index', ['pn']), + ], +); + export const NOWEB_STORE_SCHEMA = [ NowebContactSchema, NowebChatSchema, @@ -72,6 +81,7 @@ export const NOWEB_STORE_SCHEMA = [ NowebMessagesSchema, NowebLabelsSchema, NowebLabelAssociationsSchema, + NowebLidMapSchema, ]; export const Migrations: string[] = [ @@ -100,4 +110,8 @@ export const Migrations: string[] = [ 'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON "labelAssociations" (type, "labelId")', 'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON "labelAssociations" (type, "chatId")', 'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON "labelAssociations" (type, "messageId")', + // Lid Map + 'CREATE TABLE IF NOT EXISTS lid_map (id TEXT PRIMARY KEY, pn TEXT, data TEXT)', + 'CREATE UNIQUE INDEX IF NOT EXISTS lid_map_id_index ON lid_map (id)', + 'CREATE INDEX IF NOT EXISTS lid_map_pn_index ON lid_map (pn)', ]; diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository.ts new file mode 100644 index 00000000..3afebfe1 --- /dev/null +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository.ts @@ -0,0 +1,41 @@ +import { NowebLidMapSchema } from '@waha/core/engines/noweb/store/schemas'; +import { LimitOffsetParams } from '@waha/structures/pagination.dto'; +import { KnexPaginator } from '@waha/utils/Paginator'; + +import { INowebLidPNRepository, LidToPN } from '../INowebLidPNRepository'; +import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository'; + +export class LidPaginator extends KnexPaginator { + indexes = ['id', 'pn']; +} + +export class Sqlite3LidPNRepository + extends NOWEBSqlite3KVRepository + implements INowebLidPNRepository +{ + get schema() { + return NowebLidMapSchema; + } + + saveLids(lids: LidToPN[]): Promise { + return this.upsertMany(lids); + } + + getAllLids(pagination?: LimitOffsetParams): Promise { + return this.getAll(pagination); + } + + getLidsCount(): Promise { + return this.getCount(); + } + + async findLidByPN(pn: string): Promise { + const value = await this.getBy({ pn: pn }); + return value?.id || null; + } + + async findPNByLid(lid: string): Promise { + const value = await this.getBy({ id: lid }); + return value?.pn || null; + } +} diff --git a/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts b/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts index 28393c6e..c83e02c2 100644 --- a/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts +++ b/src/core/engines/noweb/store/sqlite3/Sqlite3Storage.ts @@ -1,8 +1,10 @@ import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository'; import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository'; +import { INowebLidPNRepository } from '@waha/core/engines/noweb/store/INowebLidPNRepository'; import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository'; import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository'; import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository'; +import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository'; import { Schema } from '@waha/core/storage/Schema'; import { INowebStorage } from '../INowebStorage'; @@ -74,4 +76,8 @@ export class Sqlite3Storage extends INowebStorage { getMessagesRepository() { return new Sqlite3MessagesRepository(this.db); } + + getLidPNRepository(): INowebLidPNRepository { + return new Sqlite3LidPNRepository(this.db); + } } diff --git a/src/core/engines/webjs/WebjsClientCore.ts b/src/core/engines/webjs/WebjsClientCore.ts index 30956c95..01d4f1f3 100644 --- a/src/core/engines/webjs/WebjsClientCore.ts +++ b/src/core/engines/webjs/WebjsClientCore.ts @@ -1,5 +1,6 @@ import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; import { Label } from '@waha/structures/labels.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { TextStatus } from '@waha/structures/status.dto'; import { EventEmitter } from 'events'; @@ -99,7 +100,7 @@ export class WebjsClientCore extends Client { } async createLabel(name: string, color: number): Promise { - const labelId: number = await this.pupPage.evaluate( + const labelId: number = (await this.pupPage.evaluate( async (name, color) => { // @ts-ignore return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction( @@ -109,7 +110,7 @@ export class WebjsClientCore extends Client { }, name, color, - ); + )) as any; return labelId; } @@ -262,4 +263,77 @@ export class WebjsClientCore extends Client { return messages.map((m) => new Message(this, m)); } + + public async getAllLids( + pagination: PaginationParams, + ): Promise> { + const lids: Array = (await this.pupPage.evaluate( + async (pagination) => { + pagination.limit ||= Infinity; + pagination.offset ||= 0; + pagination.sortBy ||= 'lid'; + + // @ts-ignore + const WAWebApiContact = window.Store.LidUtils; + + await WAWebApiContact.warmUpAllLidPnMappings(); + const lidMap = WAWebApiContact.lidPnCache['$1']; + const values = Array.from(lidMap.values()); + const result = values.map((map) => { + return { + // @ts-ignore + lid: map.lid._serialized, + // @ts-ignore + pn: map.phoneNumber._serialized, + }; + }); + // @ts-ignore + const paginator = new window.Paginator(pagination); + const page = paginator.apply(result); + return page; + }, + pagination, + )) as any; + return lids; + } + + public async getLidsCount(): Promise { + const count: number = (await this.pupPage.evaluate(async () => { + // @ts-ignore + const WAWebApiContact = window.Store.LidUtils; + + await WAWebApiContact.warmUpAllLidPnMappings(); + const lidMap = WAWebApiContact.lidPnCache['$1']; + return lidMap.size; + })) as any; + return count; + } + + public async findPNByLid(lid: string): Promise { + const pn = await this.pupPage.evaluate(async (lid) => { + // @ts-ignore + const WAWebApiContact = window.Store.LidUtils; + // @ts-ignore + const WAWebWidFactory = window.Store.WidFactory; + + const wid = WAWebWidFactory.createWid(lid); + const result = WAWebApiContact.getPhoneNumber(wid); + return result ? result._serialized : null; + }, lid); + return pn; + } + + public async findLIDByPhoneNumber(phoneNumber: string): Promise { + const lid: string = (await this.pupPage.evaluate(async (pn) => { + // @ts-ignore + const WAWebApiContact = window.Store.LidUtils; + // @ts-ignore + const WAWebWidFactory = window.Store.WidFactory; + + const wid = WAWebWidFactory.createWid(pn); + const result = WAWebApiContact.getCurrentLid(wid); + return result ? result._serialized : null; + }, phoneNumber)) as any; + return lid; + } } diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index c50ddb05..53776f0b 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -93,6 +93,7 @@ import { SettingsSecurityChangeInfo, } from '@waha/structures/groups.dto'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; +import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { ReplyToMessage } from '@waha/structures/message.dto'; import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto'; import { @@ -926,6 +927,38 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { await contact.unblock(); } + /** + * Lid to Phone Number methods + */ + public async getAllLids( + pagination: PaginationParams, + ): Promise> { + return this.whatsapp.getAllLids(pagination); + } + + public async getLidsCount(): Promise { + return this.whatsapp.getLidsCount(); + } + + public async findPNByLid(lid: string): Promise { + const phoneNumber = await this.whatsapp.findPNByLid(lid); + return { + lid: lid, + pn: phoneNumber, + }; + } + + public async findLIDByPhoneNumber( + phoneNumber: string, + ): Promise { + const pn = toCusFormat(phoneNumber); + const lid = await this.whatsapp.findLIDByPhoneNumber(pn); + return { + lid: lid, + pn: pn, + }; + } + /** * Group methods */ diff --git a/src/core/storage/sql/SqlKVRepository.ts b/src/core/storage/sql/SqlKVRepository.ts index 8befc83f..0f29643b 100644 --- a/src/core/storage/sql/SqlKVRepository.ts +++ b/src/core/storage/sql/SqlKVRepository.ts @@ -95,11 +95,11 @@ export class SqlKVRepository { const sql = `INSERT INTO "${this.table}" (${columns.join(', ')}) VALUES ${data .map(() => `(${columns.map(() => '?').join(', ')})`) - .join(', ')} - ON CONFLICT(id) DO UPDATE - SET ${columns - .map((column) => `${column} = excluded.${column}`) - .join(', ')}`; + .join(', ')} ON CONFLICT(id) DO + UPDATE + SET ${columns + .map((column) => `${column} = excluded.${column}`) + .join(', ')}`; try { await this.raw(sql, values); } catch (err) { @@ -119,6 +119,15 @@ export class SqlKVRepository { return this.all(query); } + async getCount(): Promise { + const query = this.select().count({ count: 'id' }); + const row = await this.engine.get(query); + if (!row) { + return 0; + } + return parseInt(row.count, 10); + } + async getAllByIds(ids: string[]) { const entitiesMap = await this.getEntitiesByIds(ids); return Array.from(entitiesMap.values()).filter( diff --git a/src/structures/lids.dto.ts b/src/structures/lids.dto.ts new file mode 100644 index 00000000..361595d0 --- /dev/null +++ b/src/structures/lids.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { LimitOffsetParams } from '@waha/structures/pagination.dto'; + +export class LidToPhoneNumber { + @ApiProperty({ + description: 'Linked ID for the user', + example: '1111111@lid', + }) + lid?: string; + + @ApiProperty({ + description: 'Phone number (chat id) for the user', + example: '3333333@c.us', + }) + pn?: string; +} + +export class LidsListQueryParams extends LimitOffsetParams { + limit?: number = 100; + offset?: number = 0; +}