[core] NOWEB labels

This commit is contained in:
devlikepro committed 2024-08-04 10:09:32 +07:00
1 parent 1aeaf996bc
commit df31fe2a8b
18 files changed
+485 -19

No files matched your search

+102 -4
View File
@@ -11,7 +11,6 @@ import makeWASocket, {
isRealMessage,
jidNormalizedUser,
makeCacheableSignalKeyStore,
MinimalMessage,
NewsletterMetadata,
normalizeMessageContent,
PresenceData,
@@ -20,6 +19,8 @@ import makeWASocket, {
WAMessageKey,
} from '@adiwajshing/baileys';
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
import { Label as NOWEBLabel } from '@adiwajshing/baileys/lib/Types/Label';
import { LabelAssociationType } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { Logger as BaileysLogger } from '@adiwajshing/baileys/node_modules/pino';
import { UnprocessableEntityException } from '@nestjs/common';
@@ -35,7 +36,11 @@ import {
} from '@waha/structures/channels.dto';
import { GetChatsQuery } from '@waha/structures/chats.dto';
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
import { Label } from '@waha/structures/labels.dto';
import {
Label,
LabelChatAssociation,
LabelID,
} from '@waha/structures/labels.dto';
import {
PollVote,
PollVotePayload,
@@ -728,8 +733,47 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
* Labels methods
*/
public getLabels(): Promise<Label[]> {
throw new NotImplementedByEngineError();
public async getLabels(): Promise<Label[]> {
const labels = await this.store.getLabels();
return labels.map(this.toLabel);
}
public async getChatsByLabelId(labelId: string) {
const chats = await this.store.getChatsByLabelId(labelId);
// Remove unreadCount, it's not ready yet
chats.forEach((chat) => delete chat.unreadCount);
return chats;
}
public async getChatLabels(chatId: string): Promise<Label[]> {
const jid = toJID(chatId);
const labels = await this.store.getChatLabels(jid);
return labels.map(this.toLabel);
}
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
const jid = toJID(chatId);
const labelsIds = labels.map((label) => label.id);
const currentLabels = await this.store.getChatLabels(jid);
const currentLabelsIds = currentLabels.map((label) => label.id);
const addLabelsIds = lodash.difference(labelsIds, currentLabelsIds);
const removeLabelsIds = lodash.difference(currentLabelsIds, labelsIds);
for (const labelId of addLabelsIds) {
await this.sock.addChatLabel(jid, labelId);
}
for (const labelId of removeLabelsIds) {
await this.sock.removeChatLabel(jid, labelId);
}
}
protected toLabel(label: NOWEBLabel): Label {
const color = label.color;
return {
id: label.id,
name: label.name,
color: color,
colorHex: Label.toHex(color),
};
}
/**
@@ -1084,6 +1128,60 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
});
return true;
case WAHAEvents.LABEL_UPSERT:
this.sock.ev.on('labels.edit', (data: NOWEBLabel) => {
if (data.deleted) {
return;
}
const body = this.toLabel(data);
handler(body);
});
return true;
case WAHAEvents.LABEL_REMOVED:
this.sock.ev.on('labels.edit', (data: NOWEBLabel) => {
if (!data.deleted) {
return;
}
const body = this.toLabel(data);
handler(body);
});
return true;
case WAHAEvents.LABEL_CHAT_ADDED:
this.sock.ev.on('labels.association', async ({ association, type }) => {
if (type !== 'add') {
return;
}
if (association.type !== LabelAssociationType.Chat) {
return;
}
const labelData = await this.store.getLabelById(association.labelId);
const label = labelData ? this.toLabel(labelData) : null;
const body: LabelChatAssociation = {
labelId: association.labelId,
chatId: association.chatId,
label: label,
};
handler(body);
});
return true;
case WAHAEvents.LABEL_CHAT_REMOVED:
this.sock.ev.on('labels.association', async ({ association, type }) => {
if (type !== 'remove') {
return;
}
if (association.type !== LabelAssociationType.Chat) {
return;
}
const labelData = await this.store.getLabelById(association.labelId);
const label = labelData ? this.toLabel(labelData) : null;
const body: LabelChatAssociation = {
labelId: association.labelId,
chatId: association.chatId,
label: label,
};
handler(body);
});
return true;
default:
return false;
}
@@ -3,6 +3,8 @@ import { Chat } from '@adiwajshing/baileys';
export interface IChatRepository {
getAll(): Promise<Chat[]>;
getAllByIds(ids: string[]): Promise<Chat[]>;
getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
@@ -0,0 +1,19 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export interface ILabelAssociationRepository {
deleteOne(association: LabelAssociation): Promise<void>;
save(association: LabelAssociation): Promise<void>;
deleteByLabelId(labelId: string): Promise<void>;
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]>;
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]>;
}
@@ -0,0 +1,14 @@
import { Contact } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
export interface ILabelsRepository {
getById(id: string): Promise<Label | null>;
getAll(): Promise<Label[]>;
getAllByIds(ids: string[]): Promise<Label[]>;
deleteById(id: string): Promise<void>;
save(label: Label): Promise<void>;
}
+31 -6
View File
@@ -1,15 +1,40 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
export interface INowebStorage {
init(): Promise<void>;
export abstract class INowebStorage {
abstract init(): Promise<void>;
close(): Promise<void>;
abstract close(): Promise<void>;
getContactsRepository(): IContactRepository;
abstract getContactsRepository(): IContactRepository;
getChatRepository(): IChatRepository;
abstract getChatRepository(): IChatRepository;
getMessagesRepository(): IMessagesRepository;
abstract getMessagesRepository(): IMessagesRepository;
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
protected getMessagesMetadata(): Map<string, any> {
return new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
}
protected getLabelAssociationMetadata() {
return new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
}
}
@@ -4,6 +4,7 @@ import {
Contact,
proto,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
export interface INowebStore {
presences: any;
@@ -23,4 +24,12 @@ export interface INowebStore {
getContacts(): Promise<Contact[]>;
getContactById(jid: string): Promise<Contact>;
getLabels(): Promise<Label[]>;
getLabelById(labelId: string): Promise<Label | null>;
getChatsByLabelId(labelId: string): Promise<Chat[]>;
getChatLabels(chatId: string): Promise<Label[]>;
}
@@ -1,4 +1,5 @@
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException, ConsoleLogger } from '@nestjs/common';
import { INowebStore } from './INowebStore';
@@ -51,4 +52,20 @@ export class NowebInMemoryStore implements INowebStore {
getContactById(jid: string): Promise<Contact> {
throw new BadRequestException(this.errorMessage);
}
getLabels(): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
getLabelById(labelId: string): Promise<Label | null> {
throw new BadRequestException(this.errorMessage);
}
getChatsByLabelId(labelId: string): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
getChatLabels(chatId: string): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
}
@@ -9,7 +9,13 @@ import {
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys';
import { ConsoleLogger } from '@nestjs/common';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
@@ -28,6 +34,8 @@ export class NowebPersistentStore implements INowebStore {
private chatRepo: IChatRepository;
private contactRepo: IContactRepository;
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
public presences: any;
private lock: any;
@@ -39,6 +47,8 @@ export class NowebPersistentStore implements INowebStore {
this.chatRepo = storage.getChatRepository();
this.contactRepo = storage.getContactsRepository();
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
this.presences = {};
this.lock = new AsyncLock({ maxPending: Infinity });
}
@@ -83,6 +93,10 @@ export class NowebPersistentStore implements INowebStore {
ev.on('contacts.update', (data) =>
this.withLock('contacts', () => this.onContactUpdate(data)),
);
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
ev.on('labels.association', ({ association, type }) =>
this.onLabelsAssociation(association, type),
);
// Presence
ev.on('presence.update', (data) => this.onPresenceUpdate(data));
this.socket = socket;
@@ -275,6 +289,26 @@ export class NowebPersistentStore implements INowebStore {
}
}
private async onLabelsEdit(label: Label) {
if (label.deleted) {
await this.labelsRepo.deleteById(label.id);
await this.labelAssociationsRepo.deleteByLabelId(label.id);
} else {
await this.labelsRepo.save(label);
}
}
private async onLabelsAssociation(
association: LabelAssociation,
type: 'add' | 'remove',
) {
if (type === 'remove') {
await this.labelAssociationsRepo.deleteOne(association);
} else {
await this.labelAssociationsRepo.save(association);
}
}
private async onPresenceUpdate({ id, presences: update }) {
this.presences[id] = this.presences[id] || {};
Object.assign(this.presences[id], update);
@@ -303,4 +337,29 @@ export class NowebPersistentStore implements INowebStore {
getContacts() {
return this.contactRepo.getAll();
}
getLabels(): Promise<Label[]> {
return this.labelsRepo.getAll();
}
getLabelById(labelId: string): Promise<Label | null> {
return this.labelsRepo.getById(labelId);
}
async getChatsByLabelId(labelId: string): Promise<Chat[]> {
const associations =
await this.labelAssociationsRepo.getAssociationsByLabelId(
labelId,
LabelAssociationType.Chat,
);
const ids = associations.map((association) => association.chatId);
return await this.chatRepo.getAllByIds(ids);
}
async getChatLabels(chatId: string): Promise<Label[]> {
const associations =
await this.labelAssociationsRepo.getAssociationsByChatId(chatId);
const ids = associations.map((association) => association.labelId);
return await this.labelsRepo.getAllByIds(ids);
}
}
+22
View File
@@ -53,4 +53,26 @@ export const NOWEB_STORE_SCHEMA = [
new Index('timestamp_index', ['messageTimestamp']),
],
),
new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
),
new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
),
];
@@ -38,6 +38,15 @@ export class Sqlite3KVRepository<Entity> {
return this.all(this.select());
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
protected getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
protected async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
@@ -0,0 +1,40 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3LabelAssociationsRepository
extends Sqlite3KVRepository<LabelAssociation>
implements ILabelAssociationRepository
{
async deleteOne(association: LabelAssociation): Promise<void> {
await this.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.deleteBy({ labelId: labelId });
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.getAllBy({
type: type,
labelId: labelId,
});
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
}
}
@@ -0,0 +1,8 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3LabelsRepository
extends Sqlite3KVRepository<Label>
implements ILabelsRepository {}
@@ -1,4 +1,9 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { INowebStorage } from '../INowebStorage';
import { Field, Index, NOWEB_STORE_SCHEMA, Schema } from '../Schema';
@@ -10,11 +15,12 @@ import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class Sqlite3Storage implements INowebStorage {
export class Sqlite3Storage extends INowebStorage {
private readonly db: any;
private readonly tables: Schema[];
constructor(filePath: string) {
super();
this.db = new Database(filePath);
this.tables = NOWEB_STORE_SCHEMA;
}
@@ -71,6 +77,33 @@ export class Sqlite3Storage implements INowebStorage {
this.db.exec(
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
);
//
// Labels
//
this.db.exec(
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
);
// Label associations
this.db.exec(
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
);
}
async close() {
@@ -85,11 +118,21 @@ export class Sqlite3Storage implements INowebStorage {
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
}
getLabelAssociationRepository(): ILabelAssociationRepository {
const metadata = this.getLabelAssociationMetadata();
return new Sqlite3LabelAssociationsRepository(
this.db,
this.getSchema('labelAssociations'),
metadata,
);
}
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);
const metadata = this.getMessagesMetadata();
return new Sqlite3MessagesRepository(
this.db,
this.getSchema('messages'),
+3 -1
View File
@@ -509,10 +509,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected toLabel(label: WEBJSLabel): Label {
const color = label.colorIndex;
return {
id: label.id,
name: label.name,
color: 0,
color: color,
colorHex: Label.toHex(color),
};
}
+4
View File
@@ -17,6 +17,10 @@ export enum WAHAEvents {
CALL_RECEIVED = 'call.received',
CALL_ACCEPTED = 'call.accepted',
CALL_REJECTED = 'call.rejected',
LABEL_UPSERT = 'label.upsert',
LABEL_REMOVED = 'label.removed',
LABEL_CHAT_ADDED = 'label.chat.added',
LABEL_CHAT_REMOVED = 'label.chat.removed',
}
export enum WAHASessionStatus {
+52
View File
@@ -1,4 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { ChatIdProperty } from '@waha/structures/properties.dto';
const Colors = [
'#ff9485',
'#64c4ff',
'#ffd429',
'#dfaef0',
'#99b6c1',
'#55ccb3',
'#ff9dff',
'#d3a91d',
'#6d7cce',
'#d7e752',
'#00d0e2',
'#ffc5c7',
'#93ceac',
'#f74848',
'#00a0f2',
'#83e422',
'#ffaf04',
'#b5ebff',
'#9ba6ff',
'#9368cf',
];
export class Label {
@ApiProperty({
@@ -18,6 +42,19 @@ export class Label {
description: 'Internal color number, not hex',
})
color: number;
@ApiProperty({
example: '#ff9485',
description: 'Color in hex',
})
colorHex: string;
static toHex(color: number) {
if (color >= Colors.length) {
return '#000000';
}
return Colors[color];
}
}
export class LabelID {
@@ -31,3 +68,18 @@ export class LabelID {
export class SetLabelsRequest {
labels: LabelID[];
}
export class LabelChatAssociation {
@ApiProperty({
example: '1',
description: 'Label ID',
})
labelId: string;
label: Label | null;
@ChatIdProperty({
description: 'Chat ID',
})
chatId: string;
}
+43
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { CallData } from '@waha/structures/calls.dto';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import { ChatArchiveEvent } from './chats.dto';
import { MessageDestination } from './chatting.dto';
@@ -89,6 +90,7 @@ export class WAMessageRevokedBody {
after?: WAMessage;
before?: WAMessage;
}
export class WASessionStatusBody {
@ApiProperty({
example: 'default',
@@ -120,6 +122,7 @@ export class WAHAWebhook {
// eslint-disable-next-line @typescript-eslint/ban-types
| object;
}
class WAHAWebhookSessionStatus extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when the session status changes.',
@@ -272,6 +275,42 @@ class WAHAWebhookCallRejected extends WAHAWebhook {
payload: CallData;
}
class WAHAWebhookLabelUpsert extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when a label is created or updated',
})
event = WAHAEvents.LABEL_UPSERT;
payload: Label;
}
class WAHAWebhookLabelRemoved extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when a label is removed',
})
event = WAHAEvents.LABEL_REMOVED;
payload: Label;
}
class WAHAWebhookLabelChatAdded extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when a label is added to a chat',
})
event = WAHAEvents.LABEL_CHAT_ADDED;
payload: LabelChatAssociation;
}
class WAHAWebhookLabelChatRemoved extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when a label is removed from a chat',
})
event = WAHAEvents.LABEL_CHAT_REMOVED;
payload: LabelChatAssociation;
}
const WAHA_WEBHOOKS = [
WAHAWebhookSessionStatus,
WAHAWebhookMessage,
@@ -289,5 +328,9 @@ const WAHA_WEBHOOKS = [
WAHAWebhookCallReceived,
WAHAWebhookCallAccepted,
WAHAWebhookCallRejected,
WAHAWebhookLabelUpsert,
WAHAWebhookLabelRemoved,
WAHAWebhookLabelChatAdded,
WAHAWebhookLabelChatRemoved,
];
export { WAHA_WEBHOOKS };
+2 -2
View File
@@ -13037,7 +13037,7 @@ __metadata:
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels":
version: 1.24.0
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=ed4e24ce1af3bd8196fc64b1c192e9350a4de863"
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=ec31dbb7207599b602169c70923f114dbc7c526e"
dependencies:
"@pedroslopez/moduleraid": ^5.0.2
adm-zip: ^0.5.10
@@ -13056,7 +13056,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 79e215af485616ed254c9da263532bb95ad1af25e5f2147424267758e7946c5c2d2c1b49459ee6ef65957cdec4da472917e5334c6d4120a2881c3177fcd5be5f
checksum: 65dc78c2e057ef5c366f7e4d2d9940017e5a37eb740c70aaabb1bcf2fc54b8886d41a925cfad87757922c7966bcca328c483438934725958a92e4519e11b6197
languageName: node
linkType: hard