Compare commits

...
41 Commits
Author SHA1 Message Date
devlikepro 76307dec12 [core] 2025.9.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-09-29 09:09:47 +07:00
devlikepro 0b441054ae [core] Up dashboard 2025-09-29 09:09:46 +07:00
devlikepro 2e8b78aebc [core] ChatWoot fix duplicating messages for transcribe feature
fix #1371
2025-09-29 09:09:45 +07:00
devlikepro d01b221a24 [core] Add ChatWootAppConfig to openapi
fix #1416
2025-09-29 09:09:44 +07:00
devlikepro 61bb111768 [core] ChatWoot - conversation selector
fix #1216
fix #1357
fix #1237
close #1343
close #1213
2025-09-29 09:09:43 +07:00
devlikepro de47d6e445 [core] ChatWoot - list custom attributes once on upsert 2025-09-29 09:09:41 +07:00
devlikepro 59fe4b30f0 [core] ChatWoot - use Service suffix 2025-09-29 09:09:40 +07:00
devlikepro 166dae8e27 [core] ChatWoot - fix brazil numbers
fix #1261
2025-09-29 09:09:39 +07:00
devlikepro 5ce64fd30d [core] ChatWoot - check JID format 2025-09-29 09:09:37 +07:00
devlikepro 1a4513f437 [core] GOWS - do not fail in case of wrong numbers
fix #1173
2025-09-29 09:09:37 +07:00
devlikepro 59be4a780f [core] ChatWoot - check attributes before updating
fix #1173
2025-09-29 09:09:37 +07:00
devlikepro 74001c2b49 [core] ChatWoot - get group name
fix #1245
2025-09-29 09:09:37 +07:00
devlikepro a9ce0fb2d9 [core] ChatWoot - fix /jobs auth
fix #1398
2025-09-29 09:09:36 +07:00
devlikepro 4c3fd1c214 [core] ChatWoot - do not send STOPPED if server restarts 2025-09-29 09:09:34 +07:00
devlikepro a9459fe746 [core] ChatWoot - logging 2025-09-29 09:09:32 +07:00
devlikepro 5a3228f05b [core] ChatWoot - safe get avatar from WhatsApp 2025-09-29 09:09:31 +07:00
devlikepro befc238db9 [core] ChatWoot - fix 500 Error - use Account API to fetch contact conversations list
fix #1389
2025-09-29 09:09:31 +07:00
devlikepro ae75a1f8d4 [core] ChatWoot - use JobLink for success 2025-09-29 09:09:30 +07:00
devlikepro d1c50bfd73 [core] ChatWoot - add WAHA_PUBLIC_URL 2025-09-29 09:09:30 +07:00
devlikepro 6f540366f4 [core] ChatWoot - add RACK_TIMEOUT_SERVICE_TIMEOUT 60s
mention #1389
2025-09-29 09:09:29 +07:00
devlikepro 151fc760fe [core] ChatWoot log additional errors 2025-09-29 09:09:29 +07:00
devlikepro be4391a4b6 [core] ChatWoot fix reply if not found 2025-09-29 09:09:29 +07:00
devlikepro 76ae3e0f6b [core] 2025.9.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-09-23 15:51:01 +07:00
devlikepro f37b6edaa6 [core] Up Dashboard 2025-09-23 15:51:00 +07:00
devlikepro 7095f77f93 [core] GOWS - v1.0.15 2025-09-23 15:51:00 +07:00
devlikepro 5853b778f2 [core] GOWS - fix "typing" after message
fix #1364
2025-09-23 15:51:00 +07:00
devlikepro 6405725ce6 [core] WEBJS - send vcard
fix #1341
2025-09-23 15:50:59 +07:00
devlikepro 111d6141aa [core] WEBJS - fix login via code on new version 2025-09-23 15:50:58 +07:00
devlikepro 5560b3c8e6 [core] NOWEB - login via phone
fix #1385
2025-09-23 15:50:58 +07:00
devlikepro d528d541ea [core] NOWEB - fix fromMe in group messages
fix #1350
2025-09-23 15:50:57 +07:00
devlikepro ab6b51cc1f [core] NOWEB - normalized lid 2025-09-23 15:50:57 +07:00
devlikepro 35aa4f2658 [core] WEBJS - fix status messages 2025-09-23 15:50:56 +07:00
devlikepro 36db3c37e8 [core] WEBJS - fix channel role 2025-09-23 15:50:55 +07:00
devlikepro b0c9232153 [core] Update WEBJS 2025-09-23 15:50:55 +07:00
devlikepro 0070fb9e5c [core] Update NOWEB, use ESM wrapper 2025-09-23 15:50:53 +07:00
devlikepro aa33ac4b1d [core] NOWEB - handle lid-mapping.update 2025-09-23 15:50:52 +07:00
devlikepro 1c2bc5b395 [core] NOWEB - no clean storage on history sync 2025-09-23 15:50:51 +07:00
devlikepro f2fe18a4c5 [core] NOWEB Handle lottie stickers using .zip 2025-09-23 15:50:51 +07:00
devlikepro 87fdcbebf6 [core] NOWEB Use directPath for newsletters 2025-09-23 15:50:51 +07:00
devlikepro 68a4f651ea [core] pin postgres:17.6
fix #1380
2025-09-23 15:50:50 +07:00
devlikepro 243caa9626 [core] ChatWoot - group participant at the bottom
fix #1369
2025-09-23 15:50:50 +07:00
108 changed files with 1503 additions and 641 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://localhost:3000
# WAHA_PUBLIC_URL=http://localhost:3000
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
+2 -2
View File
@@ -35,13 +35,13 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-07-04
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-09-21
up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
+1 -1
View File
@@ -36,7 +36,7 @@ services:
# NOTE: Only if you're using PostgreSQL to save sessions
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
# postgres:
# image: postgres:17
# image: postgres:17.6
# restart: always
# environment:
# POSTGRES_USER: postgres
+4
View File
@@ -1,6 +1,10 @@
# Learn about the various environment variables at
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
# Increase rack default timeout (30s) to avoid timeout errors
# Workaround for https://github.com/devlikeapro/waha/issues/1389
RACK_TIMEOUT_SERVICE_TIMEOUT=60
# Used to verify the integrity of signed cookies. so ensure a secure value is set
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# Use `rake secret` to generate this variable
+1
View File
@@ -8,6 +8,7 @@
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://waha:3000
WAHA_PUBLIC_URL=http://localhost:3000
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
+2 -2
View File
@@ -26,7 +26,7 @@
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-07-04",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-09-21",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -101,7 +101,7 @@
"sqlite3": "^5.1.7",
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10",
"write-file-atomic": "^6.0.0",
"yaml": "^2.7.1"
},
-1
View File
@@ -1,4 +1,3 @@
import { isJidGroup } from '@adiwajshing/baileys';
import {
Body,
Controller,
+1 -1
View File
@@ -1,4 +1,3 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
Controller,
Get,
@@ -22,6 +21,7 @@ import {
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { isLidUser } from '@waha/core/utils/jids';
@ApiSecurity('api_key')
@Controller('api/:session/lids')
+10 -3
View File
@@ -51,10 +51,17 @@ export function HasBeenRetried(job: Job): boolean {
return attemptsMade > 1;
}
let base =
process.env.WAHA_PUBLIC_URL ||
process.env.WAHA_BASE_URL ||
'http://localhost:3000';
// cut / at the end
base = base.replace(/\/+$/, '');
export function JobLink(job: Job): { text: string; url: string } {
const text = `${job.queueName} => ${job.id}`;
const url = `http://localhost:3000/jobs/queue/${encodeURIComponent(
job.queueName,
)}/${job.id}`;
const url = `${base}/jobs/queue/${encodeURIComponent(job.queueName)}/${
job.id
}`;
return { text: text, url: url };
}
+4 -4
View File
@@ -4,12 +4,12 @@ import { parseBool } from '@waha/helpers';
import * as basicAuth from 'express-basic-auth';
export function BullAuthMiddleware() {
let username = process.env.WAHA_DASHBOARD_USERNAME;
let password = process.env.WAHA_DASHBOARD_PASSWORD;
let username = process.env.WAHA_DASHBOARD_USERNAME || '';
let password = process.env.WAHA_DASHBOARD_PASSWORD || '';
if (process.env.WAHA_DASHBOARD_ENABLED) {
const enabled = parseBool(process.env.WAHA_DASHBOARD_ENABLED);
if (!enabled) {
// Generate a random uuid4 username /password to prevent access
// Generate a random uuid4 username/password to effectively disable access
username = 'admin';
password = crypto.randomUUID();
}
@@ -18,7 +18,7 @@ export function BullAuthMiddleware() {
return basicAuth({
challenge: true,
users: {
[username]: password,
[String(username)]: String(password),
},
});
}
+2 -1
View File
@@ -7,7 +7,7 @@ import {
IsString,
ValidateNested,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
export type AllowedAppConfig = ChatWootAppConfig;
@@ -15,6 +15,7 @@ export enum AppName {
chatwoot = 'chatwoot',
}
@ApiExtraModels(ChatWootAppConfig)
export class App<T extends AllowedAppConfig = any> {
@IsString()
id: string;
@@ -89,6 +89,7 @@ export class ChatwootWebhookController {
return { success: true };
default:
// Ignore other events
await this.chatWootQueueService.addJobToQueue(body.event, data);
return { success: true };
}
}
+4 -5
View File
@@ -1,8 +1,7 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import * as NodeCache from 'node-cache';
import { IConversationCache } from './IConversationCache';
import { ConversationId, IConversationCache } from './IConversationCache';
const cache: NodeCache = new NodeCache({
stdTTL: 24 * 60 * 60, // 1 day
@@ -16,7 +15,7 @@ export function CacheForConfig(config: ChatWootAPIConfig): ConversationCache {
class ConversationCache implements IConversationCache {
constructor(private prefix: string) {}
fullKey(key: string) {
private fullKey(key: string) {
return `${this.prefix}.${key}`;
}
@@ -25,7 +24,7 @@ class ConversationCache implements IConversationCache {
cache.del(fullKey);
}
get(key: string): public_conversation | null {
get(key: string): ConversationId | null {
const fullKey = this.fullKey(key);
return cache.get(fullKey) || null;
}
@@ -35,7 +34,7 @@ class ConversationCache implements IConversationCache {
return cache.has(fullKey);
}
set(key: string, value: public_conversation): void {
set(key: string, value: ConversationId): void {
const fullKey = this.fullKey(key);
cache.set(fullKey, value);
}
+3 -3
View File
@@ -1,8 +1,8 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
export type ConversationId = number;
export interface IConversationCache {
get(key: string): public_conversation | null;
set(key: string, value: public_conversation): void;
get(key: string): ConversationId | null;
set(key: string, value: ConversationId): void;
has(key: string): boolean;
delete(key: string): void;
clean(): void;
+13
View File
@@ -25,6 +25,8 @@ import { WAHASessionStatusConsumer } from './consumers/waha/session.status';
import { ChatWootQueueService } from './services/ChatWootQueueService';
import { ChatWootScheduleService } from './services/ChatWootScheduleService';
import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService';
import { ChatWootConversationCreatedConsumer } from './consumers/inbox/conversation_created';
import { ChatWootConversationStatusChangedConsumer } from '@waha/apps/chatwoot/consumers/inbox/conversation_status_changed';
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
@@ -65,6 +67,14 @@ const IMPORTS = lodash.flatten([
name: QueueName.INBOX_MESSAGE_UPDATED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_CONVERSATION_CREATED,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_CONVERSATION_STATUS_CHANGED,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_MESSAGE_DELETED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
@@ -79,6 +89,9 @@ const PROVIDERS = [
ChatWootInboxMessageCreatedConsumer,
ChatWootInboxMessageUpdatedConsumer,
ChatWootInboxMessageDeletedConsumer,
// Conversation events
ChatWootConversationCreatedConsumer,
ChatWootConversationStatusChangedConsumer,
ChatWootInboxCommandsConsumer,
WAHASessionStatusConsumer,
WAHAMessageAnyConsumer,
@@ -1,18 +1,20 @@
import ChatwootClient, {
ApiError,
ApiError as ChatWootAPIError,
public_contact_create_update_payload,
public_conversation,
} from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { ContactService } from '@waha/apps/chatwoot/client/ContactService';
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import { ConversationService } from '@waha/apps/chatwoot/client/ConversationService';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import { InboxContactInfo } from '@waha/apps/chatwoot/contacts/InboxContactInfo';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { CacheForConfig } from '../cache/ConversationCache';
import { IConversationCache } from '../cache/IConversationCache';
import {
ConversationId,
IConversationCache,
} from '../cache/IConversationCache';
export interface ContactInfo {
ChatId(): string;
@@ -29,8 +31,8 @@ export class ContactConversationService {
constructor(
private config: ChatWootAPIConfig,
private contactAPI: ContactAPI,
private conversationAPI: ConversationAPI,
private contactService: ContactService,
private conversationService: ConversationService,
private accountAPI: ChatwootClient,
private logger: ILogger,
private l: Locale,
@@ -40,7 +42,7 @@ export class ContactConversationService {
private async upsertByContactInfo(
contactInfo: ContactInfo,
): Promise<public_conversation> {
): Promise<ConversationId> {
const chatId = contactInfo.ChatId();
// Check cache for chat id
@@ -51,61 +53,70 @@ export class ContactConversationService {
//
// Find or create contact
//
let contact = await this.contactAPI.searchByAnyID(chatId);
let contact = await this.contactService.searchByAnyID(chatId);
if (!contact) {
const request = await contactInfo.PublicContactCreate();
contact = await this.contactAPI.create(chatId, request);
contact = await this.contactService.create(chatId, request);
}
// Update custom attributes - always
const attributes = await contactInfo.Attributes();
this.logger.info(
`Updating contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`,
`Updating if required contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`,
);
await this.contactAPI.updateCustomAttributes(contact.data, attributes);
await this.contactService.upsertCustomAttributes(contact.data, attributes);
// Update Avatar if nothing, but keep the original one if any
if (!contact.data.thumbnail) {
const avatarUrl = await contactInfo.AvatarUrl();
const avatarUrl = await contactInfo.AvatarUrl().catch((err) => {
this.logger.warn(
`Error getting avatar for chat.id from WhatsApp: ${chatId}`,
);
this.logger.warn(err);
return null;
});
if (avatarUrl) {
this.contactAPI.updateAvatarUrlSafe(contact.data.id, avatarUrl);
this.contactService.updateAvatarUrlSafe(contact.data.id, avatarUrl);
}
}
this.logger.info(
`Using contact for chat.id: ${chatId}, contact.id: ${contact.sourceId}`,
`Using contact for chat.id: ${chatId}, contact.id: ${contact.data.id}, contact.sourceId: ${contact.sourceId}`,
);
//
// Get or create a conversation for this inbox
//
const conversation = await this.conversationAPI.upsert(contact.sourceId);
const conversation = await this.conversationService.upsert({
id: contact.data.id,
sourceId: contact.sourceId,
});
this.logger.info(
`Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${contact.sourceId}`,
);
// Save to cache
this.cache.set(chatId, conversation);
return conversation;
this.cache.set(chatId, conversation.id);
return conversation.id;
}
public async ConversationByContact(
contactInfo: ContactInfo,
): Promise<Conversation> {
const chatId = contactInfo.ChatId();
const publicConversation = await this.upsertByContactInfo(contactInfo);
const conversationId = await this.upsertByContactInfo(contactInfo);
const conversation = new Conversation(
this.accountAPI,
this.config.accountId,
publicConversation.id,
conversationId,
);
conversation.onError = (err) => {
if (err instanceof ApiError) {
if (err instanceof ChatWootAPIError) {
// invalidate cache
this.cache.delete(chatId);
this.logger.error(`ApiError: ${err.message}`);
this.logger.error(
`ApiError occurred, invalidating cache for chat.id: ${chatId}, conversation.id: ${publicConversation.id}`,
`ApiError occurred, invalidating cache for chat.id: ${chatId}, conversation.id: ${conversationId}`,
);
}
};
@@ -127,4 +138,26 @@ export class ContactConversationService {
public async InboxNotifications() {
return this.ConversationByContact(new InboxContactInfo(this.l));
}
public ResetCache(chatIds: Array<string>) {
this.logger.info(`Resetting cache chat ids: ${chatIds.join(', ')}`);
for (const chatId of chatIds) {
this.cache.delete(chatId);
}
}
public ResetMismatchedCache(chatIds: Array<string>, value: ConversationId) {
for (const chatId of chatIds) {
if (!this.cache.has(chatId)) {
continue;
}
const current = this.cache.get(chatId);
if (current !== value) {
this.logger.info(
`Resetting cache for chat id: ${chatId}, value changed from ${current} to ${value}`,
);
this.cache.delete(chatId);
}
}
}
}
@@ -13,13 +13,14 @@ import { isJidCusFormat } from '@waha/utils/wa';
import * as lodash from 'lodash';
import { AttributeKey } from '../const';
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
export interface ContactResponse {
data: generic_id & contact;
sourceId: string;
}
export class ContactAPI {
export class ContactService {
constructor(
private config: ChatWootAPIConfig,
private accountAPI: ChatwootClient,
@@ -64,8 +65,8 @@ export class ContactAPI {
if (isJidCusFormat(chatId)) {
// Search by phone
let phone_number = chatId.split('@')[0];
phone_number = phone_number.replace('+', '');
const phoneNumberE164 = E164Parser.fromJid(chatId);
const phone_number = phoneNumberE164.replace('+', '');
payload[payload.length - 1].query_operator = 'OR';
payload.push({
attribute_key: 'phone_number',
@@ -96,18 +97,22 @@ export class ContactAPI {
};
}
public updateCustomAttributes(
public async upsertCustomAttributes(
contact: generic_id & contact,
attributes: any,
) {
): Promise<boolean> {
if (lodash.isEqual(attributes, contact.custom_attributes)) {
return false;
}
const update: contact_update = {
custom_attributes: { ...contact.custom_attributes, ...attributes },
};
return this.accountAPI.contacts.update({
await this.accountAPI.contacts.update({
id: contact.id,
accountId: this.config.accountId,
data: update,
});
return true;
}
public async create(
@@ -142,7 +147,7 @@ export class ContactAPI {
})
.catch((e) => {
this.logger.warn(
'Error updating avatar_url for contact.id: ' + contactId,
`Error updating avatar_url for contact.id: ${contactId}`,
);
this.logger.warn(e);
});
@@ -1,52 +0,0 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
export class ConversationAPI {
constructor(
private config: ChatWootAPIConfig,
private inboxAPI: ChatWootInboxAPI,
private logger: ILogger,
) {}
private async find(
contactIdentifier: string,
): Promise<public_conversation | null> {
const request = {
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
};
const conversations = await this.inboxAPI.conversations.list(request);
if (conversations.length != 0) {
return conversations[0];
}
return null;
}
private async create(
contactIdentifier: string,
): Promise<public_conversation> {
const conversation = await this.inboxAPI.conversations.create({
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
});
this.logger.info(
`Created conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
async upsert(contactIdentifier: string): Promise<public_conversation> {
let conversation = await this.find(contactIdentifier);
if (!conversation) {
conversation = await this.create(contactIdentifier);
}
this.logger.info(
`Using conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
}
@@ -0,0 +1,57 @@
import ChatwootClient, { contact_conversations } from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
import type { conversation } from '@figuro/chatwoot-sdk/dist/models/conversation';
import { ConversationSelector } from '@waha/apps/chatwoot/services/ConversationSelector';
export type ConversationResult = Pick<conversation, 'id' | 'account_id'>;
export interface ContactIds {
id: number;
sourceId: string;
}
export class ConversationService {
constructor(
private config: ChatWootAPIConfig,
private accountAPI: ChatwootClient,
private inboxAPI: ChatWootInboxAPI,
private selector: ConversationSelector,
private logger: ILogger,
) {}
private async find(contact: ContactIds): Promise<ConversationResult | null> {
const result: { payload: contact_conversations } =
(await this.accountAPI.contacts.listConversations({
accountId: this.config.accountId,
id: contact.id,
})) as any;
const conversations = result.payload;
return this.selector.select(conversations);
}
private async create(contact: ContactIds): Promise<ConversationResult> {
const conversation = await this.inboxAPI.conversations.create({
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contact.sourceId,
});
this.logger.info(
`Created conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
);
return conversation;
}
async upsert(contact: ContactIds): Promise<ConversationResult> {
let conversation = await this.find(contact);
if (!conversation) {
conversation = await this.create(contact);
}
this.logger.info(
`Using conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
);
return conversation;
}
}
@@ -1,53 +0,0 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { ChatWootAccountAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import {
CustomAttributeModel,
CustomAttributeType,
} from '@waha/apps/chatwoot/client/types';
export interface CustomAttribute {
key: string;
name: string;
type: CustomAttributeType;
description: string;
model: CustomAttributeModel;
}
export class CustomAttributesAPI {
constructor(
private config: ChatWootAccountAPIConfig,
private accountAPI: ChatwootClient,
) {}
async upsert(attribute: CustomAttribute): Promise<void> {
const attributes = await this.accountAPI.customAttributes.list({
accountId: this.config.accountId,
attributeModel: String(attribute.model) as '0' | '1',
});
const existing = attributes.find((a) => a.attribute_key === attribute.key);
if (existing) {
await this.accountAPI.customAttributes.update({
accountId: this.config.accountId,
id: existing.id,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
},
});
} else {
await this.accountAPI.customAttributes.create({
accountId: this.config.accountId,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
attribute_model: attribute.model,
},
});
}
}
}
@@ -0,0 +1,68 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { ChatWootAccountAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import {
CustomAttributeModel,
CustomAttributeType,
} from '@waha/apps/chatwoot/client/types';
import type { custom_attribute } from '@figuro/chatwoot-sdk/dist/models/custom_attribute';
export interface CustomAttribute {
key: string;
name: string;
type: CustomAttributeType;
description: string;
model: CustomAttributeModel;
}
export class CustomAttributesService {
constructor(
private config: ChatWootAccountAPIConfig,
private accountAPI: ChatwootClient,
) {}
async upsert(attributes: Array<CustomAttribute>): Promise<void> {
const current: Record<CustomAttributeModel, Array<custom_attribute>> = {
[CustomAttributeModel.CONVERSATION]: [],
[CustomAttributeModel.CONTACT]: [],
};
const models = [
CustomAttributeModel.CONVERSATION,
CustomAttributeModel.CONTACT,
];
for (const model of models) {
current[model] = await this.accountAPI.customAttributes.list({
accountId: this.config.accountId,
attributeModel: String(model) as '0' | '1',
});
}
for (const attribute of attributes) {
const existing = current[attribute.model].find(
(a) => a.attribute_key === attribute.key,
);
if (existing) {
await this.accountAPI.customAttributes.update({
accountId: this.config.accountId,
id: existing.id,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
},
});
} else {
await this.accountAPI.customAttributes.create({
accountId: this.config.accountId,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
attribute_model: attribute.model,
},
});
}
}
}
}
+11 -1
View File
@@ -1,7 +1,7 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
import { isLidUser } from '@waha/core/utils/jids';
export function GetJID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_JID];
@@ -15,6 +15,16 @@ export function GetChatID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_CHAT_ID];
}
export function GetAllChatIDs(contact: any): Array<string> {
const attrs = contact?.custom_attributes || {};
const ids = [
attrs[AttributeKey.WA_CHAT_ID],
attrs[AttributeKey.WA_JID],
attrs[AttributeKey.WA_LID],
];
return ids.filter(Boolean);
}
export function FindChatID(contact: any): string | null {
if (GetJID(contact)) {
return GetJID(contact);
+7
View File
@@ -51,3 +51,10 @@ export enum CustomAttributeModel {
CONVERSATION = 0,
CONTACT = 1,
}
export enum ConversationStatus {
OPEN = 'open',
PENDING = 'pending',
SNOOZED = 'snoozed',
RESOLVED = 'resolved',
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesService';
import { CustomAttributeModel, CustomAttributeType } from './client/types';
+2
View File
@@ -18,6 +18,8 @@ export enum QueueName {
//
INBOX_MESSAGE_CREATED = 'chatwoot.inbox | message_created',
INBOX_MESSAGE_UPDATED = 'chatwoot.inbox | message_updated',
INBOX_CONVERSATION_CREATED = 'chatwoot.inbox | conversation_created',
INBOX_CONVERSATION_STATUS_CHANGED = 'chatwoot.inbox | conversation_status_changed',
//
// ChatWoot Events - Artificial
//
+8 -1
View File
@@ -58,13 +58,20 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
job: Job,
): Promise<any>;
protected GetConversationID(body) {
return body.conversation.id;
}
/**
* Process the job
* This method is called by the queue processor
*/
async processJob(job: Job<InboxData, any, EventName>): Promise<any> {
const body = job.data.body;
const key = ChatWootConversationKey(job.data.app, body.conversation.id);
const key = ChatWootConversationKey(
job.data.app,
this.GetConversationID(body),
);
return await this.withMutex(job, key, () =>
this.ProcessAndReportStatus(job),
);
@@ -0,0 +1,57 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { ChatWootConversationKey } from '@waha/apps/chatwoot/consumers/mutex';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { Job } from 'bullmq';
import { GetAllChatIDs } from '@waha/apps/chatwoot/client/ids';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { PinoLogger } from 'nestjs-pino';
import { RMutexService } from '@waha/modules/rmutex';
@Processor(QueueName.INBOX_CONVERSATION_CREATED, {
concurrency: JOB_CONCURRENCY,
})
export class ChatWootConversationCreatedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootConversationCreatedConsumer');
}
protected ErrorHeaderKey(): TKey | null {
return null;
}
protected GetConversationID(body) {
return body.id;
}
protected async Process(
container: DIContainer,
body: any,
job: Job,
): Promise<any> {
const handler = new ConversationCreatedHandler(
container.ContactConversationService(),
);
return handler.handle(body);
}
}
class ConversationCreatedHandler {
constructor(private service: ContactConversationService) {}
async handle(body: any) {
const ids = GetAllChatIDs(body?.meta?.sender);
if (!ids || ids.length === 0) {
return;
}
this.service.ResetMismatchedCache(ids, body.id);
}
}
@@ -0,0 +1,62 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { Job } from 'bullmq';
import { DIContainer } from '../../di/DIContainer';
import { TKey } from '../../i18n/templates';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { ConversationSelector } from '@waha/apps/chatwoot/services/ConversationSelector';
import { GetAllChatIDs } from '@waha/apps/chatwoot/client/ids';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { PinoLogger } from 'nestjs-pino';
import { RMutexService } from '@waha/modules/rmutex';
@Processor(QueueName.INBOX_CONVERSATION_STATUS_CHANGED, {
concurrency: JOB_CONCURRENCY,
})
export class ChatWootConversationStatusChangedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootConversationStatusChangedConsumer');
}
protected ErrorHeaderKey(): TKey | null {
return null;
}
protected GetConversationID(body) {
return body.id;
}
protected async Process(
container: DIContainer,
body: any,
job: Job,
): Promise<any> {
const handler = new ConversationStatusChangedHandler(
container.ContactConversationService(),
container.ConversationSelector(),
);
return await handler.handle(body);
}
}
class ConversationStatusChangedHandler {
constructor(
private service: ContactConversationService,
private selector: ConversationSelector,
) {}
async handle(body) {
if (!this.selector.hasStatusFilter()) {
return;
}
const ids = GetAllChatIDs(body.meta?.sender);
this.service.ResetCache(ids);
}
}
@@ -12,6 +12,7 @@ import { EngineHelper } from '@waha/apps/chatwoot/session';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import {
ChatwootMessage,
MessageMapping,
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/text';
@@ -88,29 +89,48 @@ export class MessageHandler {
const content = MarkdownToWhatsApp(message.content);
const results = [];
let part = 1;
let part = 0; // Start from 0 and increment before each send/check
const replyTo = await this.getReplyTo(message).catch((err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
this.logger.error(
`ChatWoot => WhatsApp: error getting reply to message ID: ${err}`,
);
return undefined;
});
// Send text
// Send text (Part 1 if present)
const attachments = message.attachments || [];
if (content && attachments.length !== 1) {
const textTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_TEXT);
const text = textTemplate.render({
content: content,
chatwoot: body,
});
const msg = await this.sendTextMessage(chatId, text, replyTo);
results.push(msg);
part = await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
const sendText = content && attachments.length !== 1;
if (sendText) {
part += 1; // Text is the first possible part
const exists = await this.getMapping(message, part);
if (exists) {
this.logger.warn(
`Skip part ${part}: mapping exists for Chatwoot message ${message.id}`,
);
} else {
const textTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_TEXT);
const text = textTemplate.render({
content: content,
chatwoot: body,
});
const msg = await this.sendTextMessage(chatId, text, replyTo);
results.push(msg);
await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
}
}
// Send files
const captionTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_MEDIA_CAPTION);
for (const file of attachments) {
part += 1; // Increment before each attachment send/check
const exists = await this.getMapping(message, part);
if (exists) {
this.logger.warn(
`Skip part ${part}: mapping exists for Chatwoot message ${message.id}`,
);
continue;
}
const caption = captionTemplate.render({
content: content,
chatwoot: body,
@@ -121,7 +141,7 @@ export class MessageHandler {
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
);
results.push(msg);
part = await this.saveMapping(message, msg, part);
await this.saveMapping(message, msg, part);
}
return results;
}
@@ -130,15 +150,27 @@ export class MessageHandler {
chatwootMessage: any,
whatsappMessage: any,
part: number,
): Promise<number> {
): Promise<void> {
const chatwoot: Omit<ChatwootMessage, 'id'> = {
timestamp: new Date(chatwootMessage.created_at),
conversation_id: chatwootMessage.conversation.id,
message_id: chatwootMessage.id,
};
const whatsapp = EngineHelper.WhatsAppMessageKeys(whatsappMessage);
await this.mappingService.map(chatwoot, whatsapp, 1);
return part + 1;
await this.mappingService.map(chatwoot, whatsapp, part);
}
private async getMapping(
message: any,
part: number,
): Promise<MessageMapping | null> {
return this.mappingService.getMappingByChatwootCombinedKeyAndPart(
{
conversation_id: message.conversation.id,
message_id: message.id,
},
part,
);
}
async getReplyTo(message): Promise<string | undefined> {
+5 -5
View File
@@ -1,4 +1,3 @@
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
import { conversation_message_create } from '@figuro/chatwoot-sdk';
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
import type { message } from '@figuro/chatwoot-sdk/dist/models/message';
@@ -22,7 +21,6 @@ import {
WhatsAppMessage,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
@@ -36,7 +34,7 @@ import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { isJidBroadcast, isJidGroup, toCusFormat } from '@waha/core/utils/jids';
export function ListenEventsForChatWoot() {
return [
@@ -301,7 +299,9 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
}
const replyTo = await this.getReplyToChatWootMessageID(payload).catch(
(err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
this.logger.error(
`WhatsApp => ChatWoot - error getting reply to message ID: ${err}`,
);
return undefined;
},
);
@@ -329,6 +329,6 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
chat_id: payload.from,
message_id: replyToWhatsAppID,
});
return chatwoot.message_id;
return chatwoot?.message_id;
}
}
@@ -1,3 +1,4 @@
import type { proto } from '@adiwajshing/baileys';
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
@@ -21,7 +22,6 @@ import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { JobLink } from '@waha/apps/app_sdk/JobUtils';
import { proto } from '@adiwajshing/baileys';
import * as lodash from 'lodash';
import { parseVCardV3, SimpleVCardInfo } from '@waha/core/vcard';
@@ -16,13 +16,13 @@ import {
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAHAWebhookMessageRevoked } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { toCusFormat } from '@waha/core/utils/jids';
@Processor(QueueName.WAHA_MESSAGE_REVOKED, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageRevokedConsumer extends ChatWootWAHABaseConsumer {
@@ -20,7 +20,6 @@ import { WAHAWebhookSessionStatus } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { sleep } from '@nestjs/terminus/dist/utils';
import { waitUntil } from '@waha/utils/promiseTimeout';
@Processor(QueueName.WAHA_SESSION_STATUS, { concurrency: JOB_CONCURRENCY })
@@ -103,6 +102,20 @@ export class SessionStatusHandler {
});
break;
case WAHASessionStatus.STOPPED:
try {
// Check the session is yet in STOPPED status.
// In case of server restarts,
// the consumer can process STOPPED after STARTING/WORKING
const response = await this.waha.get(data.session);
if (response.status != WAHASessionStatus.STOPPED) {
return;
}
} catch (_) {}
text = this.l.key(TKey.APP_SESSION_STATUS_ERROR).r();
text += '\n\n';
text += this.l.key(TKey.APP_HELP_REMINDER).r();
text += '\n\n';
break;
case WAHASessionStatus.FAILED:
text = this.l.key(TKey.APP_SESSION_STATUS_ERROR).r();
text += '\n\n';
@@ -1,16 +1,4 @@
import {
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
} from '@adiwajshing/baileys';
import {
isJidBroadcast,
isLidUser,
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
public_contact_create_update_payload,
public_contact_create_update_payload as Contact,
} from '@figuro/chatwoot-sdk';
import { public_contact_create_update_payload as Contact } from '@figuro/chatwoot-sdk';
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
@@ -18,6 +6,16 @@ import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { Channel } from '@waha/structures/channels.dto';
import { CacheAsync } from '@waha/utils/Cache';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import {
isJidBroadcast,
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
isLidUser,
isPnUser,
} from '@waha/core/utils/jids';
import { UnknownJIDFormat } from '@waha/apps/chatwoot/errors';
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
/**
* Base WhatsApp contact info class
@@ -71,8 +69,7 @@ class JidContactInfo extends ChatContactInfo {
const contact: any = await this.session.getContact(this.chatId);
const name =
contact?.name || contact?.pushName || contact?.pushname || this.chatId;
const phoneNumber = this.chatId.split('@')[0];
const phoneNumberE164 = '+' + phoneNumber;
const phoneNumberE164 = E164Parser.fromJid(this.chatId);
const result: Contact = {
name: name,
@@ -154,6 +151,7 @@ class GroupContactInfo extends ChatContactInfo {
const group: any = await this.session?.getGroup(this.chatId);
if (group) {
name = group.subject || group.name || group.topic || name;
name = group.Name || name;
const suffix = this.locale
.key(TKey.WHATSAPP_CONTACT_GROUP_SUFFIX)
.render();
@@ -271,6 +269,9 @@ export function WhatsAppContactInfo(
return new BroadcastContactInfo(session, chatId, locale);
} else if (isLidUser(chatId)) {
return new LidContactInfo(session, chatId, locale);
} else if (isPnUser(chatId)) {
return new JidContactInfo(session, chatId, locale);
} else {
throw new UnknownJIDFormat(chatId);
}
return new JidContactInfo(session, chatId, locale);
}
+94 -91
View File
@@ -2,10 +2,10 @@ import ChatwootClient from '@figuro/chatwoot-sdk';
import * as lodash from 'lodash';
import { AxiosLogging } from '@waha/apps/app_sdk/AxiosLogging';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { ContactService } from '@waha/apps/chatwoot/client/ContactService';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import { CustomAttributesAPI } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { ConversationService } from '@waha/apps/chatwoot/client/ConversationService';
import { CustomAttributesService } from '@waha/apps/chatwoot/client/CustomAttributesService';
import { ChatWootInboxAPI } from '@waha/apps/chatwoot/client/interfaces';
import {
ChatWootAppConfig,
@@ -25,21 +25,17 @@ import {
import { Job } from 'bullmq';
import { Knex } from 'knex';
import { i18n } from '@waha/apps/chatwoot/i18n';
import { CacheSync } from '@waha/utils/Cache';
import {
ConversationSelector,
ConversationSort,
} from '@waha/apps/chatwoot/services/ConversationSelector';
/**
* Dependency Injection Container for ChatWoot
* Manages the creation and caching of various clients and repositories
*/
export class DIContainer {
private accountAPI: ChatwootClient;
private inboxAPI: ChatWootInboxAPI;
private contactAPI: ContactAPI;
private conversationAPI: ConversationAPI;
private contactConversationService: ContactConversationService;
private locale: Locale;
private messageMappingService: MessageMappingService;
private wahaSelf: WAHASelf;
/**
* Creates a new DIContainer with the given configuration
*/
@@ -68,122 +64,124 @@ export class DIContainer {
return this.knex;
}
@CacheSync()
public Locale(): Locale {
if (!this.locale) {
this.locale = i18n.locale(this.config.locale || DEFAULT_LOCALE);
this.locale = this.locale.override(this.ChatWootConfig().templates);
}
return this.locale;
let locale = i18n.locale(this.config.locale || DEFAULT_LOCALE);
locale = locale.override(this.ChatWootConfig().templates);
return locale;
}
/**
* Gets the AccountAPI client
* @returns ChatwootClient instance
*/
@CacheSync()
public AccountAPI(): ChatwootClient {
if (!this.accountAPI) {
this.accountAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.accountToken,
},
});
}
return this.accountAPI;
return new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.accountToken,
},
});
}
/**
* Gets the InboxAPI client
* @returns ChatWootInboxAPI instance
*/
@CacheSync()
public InboxAPI(): ChatWootInboxAPI {
if (!this.inboxAPI) {
const chatwootClientAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.inboxIdentifier,
},
});
this.inboxAPI = chatwootClientAPI.client as ChatWootInboxAPI;
}
return this.inboxAPI;
const chatwootClientAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.inboxIdentifier,
},
});
return chatwootClientAPI.client as ChatWootInboxAPI;
}
/**
* Gets the ContactAPI
* @returns ContactAPI instance
* Gets the ContactService
* @returns ContactService instance
*/
private ContactAPI(): ContactAPI {
if (!this.contactAPI) {
this.contactAPI = new ContactAPI(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.logger,
);
}
return this.contactAPI;
@CacheSync()
private ContactService(): ContactService {
return new ContactService(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.logger,
);
}
@CacheSync()
public ConversationSelector() {
const config = this.ChatWootConfig();
return new ConversationSelector({
sort: config.conversations.sort,
status: config.conversations.status,
inboxId: this.config.inboxId,
});
}
/**
* Gets the ConversationAPI
* @returns ConversationAPI instance
* Gets the ConversationService
* @returns ConversationService instance
*/
private ConversationAPI(): ConversationAPI {
if (!this.conversationAPI) {
this.conversationAPI = new ConversationAPI(
this.config,
this.InboxAPI(),
this.logger,
);
}
return this.conversationAPI;
@CacheSync()
private ConversationService(): ConversationService {
return new ConversationService(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.ConversationSelector(),
this.logger,
);
}
/**
* Gets the ContactConversationService
* @returns ContactConversationService instance
*/
@CacheSync()
public ContactConversationService(): ContactConversationService {
if (!this.contactConversationService) {
this.contactConversationService = new ContactConversationService(
this.config,
this.ContactAPI(),
this.ConversationAPI(),
this.AccountAPI(),
this.logger,
this.Locale(),
);
}
return this.contactConversationService;
return new ContactConversationService(
this.config,
this.ContactService(),
this.ConversationService(),
this.AccountAPI(),
this.logger,
this.Locale(),
);
}
@CacheSync()
private ChatwootMessageRepository(): ChatwootMessageRepository {
return new ChatwootMessageRepository(this.Knex(), this.AppPk());
}
@CacheSync()
private WhatsAppMessageRepository(): WhatsAppMessageRepository {
return new WhatsAppMessageRepository(this.Knex(), this.AppPk());
}
@CacheSync()
private MessageMappingRepository(): MessageMappingRepository {
return new MessageMappingRepository(this.Knex(), this.AppPk());
}
@CacheSync()
public MessageMappingService(): MessageMappingService {
if (!this.messageMappingService) {
this.messageMappingService = new MessageMappingService(
this.Knex(),
this.WhatsAppMessageRepository(),
this.ChatwootMessageRepository(),
this.MessageMappingRepository(),
);
}
return this.messageMappingService;
return new MessageMappingService(
this.Knex(),
this.WhatsAppMessageRepository(),
this.ChatwootMessageRepository(),
this.MessageMappingRepository(),
);
}
public ChatWootErrorReporter(job: Job): ChatWootErrorReporter {
@@ -194,19 +192,20 @@ export class DIContainer {
* Gets the WAHASelf instance
* @returns WAHASelf instance
*/
@CacheSync()
public WAHASelf(): WAHASelf {
if (!this.wahaSelf) {
this.wahaSelf = new WAHASelf();
const logging = new AxiosLogging(this.Logger());
logging.applyTo(this.wahaSelf.client);
}
return this.wahaSelf;
const self = new WAHASelf();
const logging = new AxiosLogging(this.Logger());
logging.applyTo(self.client);
return self;
}
public CustomAttributesAPI() {
return new CustomAttributesAPI(this.config, this.AccountAPI());
@CacheSync()
public CustomAttributesService() {
return new CustomAttributesService(this.config, this.AccountAPI());
}
@CacheSync()
public ChatWootConfig(): ChatWootConfig {
const defaults: ChatWootConfig = {
templates: {},
@@ -214,6 +213,10 @@ export class DIContainer {
commands: {
server: true,
},
conversations: {
sort: ConversationSort.created_newest,
status: null,
},
};
return lodash.defaults({}, this.config, defaults);
}
+21
View File
@@ -9,6 +9,11 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
import {
ConversationSelectorConfig,
ConversationSort,
} from '@waha/apps/chatwoot/services/ConversationSelector';
import { ConversationStatus } from '@waha/apps/chatwoot/client/types';
export const DEFAULT_LOCALE = 'en-US';
@@ -23,10 +28,20 @@ export enum LinkPreview {
HQ = 'HG',
}
export class ChatWootConversationsConfig {
@IsEnum(ConversationSort)
sort: ConversationSort;
@IsOptional()
@IsEnum(ConversationStatus, { each: true })
status: Array<ConversationStatus> | null;
}
export interface ChatWootConfig {
templates: Record<string, string>;
linkPreview: LinkPreview;
commands: ChatWootCommandsConfig;
conversations: ChatWootConversationsConfig;
}
export class ChatWootAppConfig implements ChatWootAPIConfig {
@@ -56,7 +71,13 @@ export class ChatWootAppConfig implements ChatWootAPIConfig {
@IsDynamicObject()
templates?: Record<string, string>;
@IsOptional()
@ValidateNested()
@Type(() => ChatWootCommandsConfig)
commands?: ChatWootCommandsConfig;
@IsOptional()
@ValidateNested()
@Type(() => ChatWootConversationsConfig)
conversations?: ChatWootConversationsConfig;
}
@@ -11,8 +11,10 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { Job } from 'bullmq';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
const renderer: ErrorRenderer = new ErrorRenderer();
export class ChatWootErrorReporter {
private errorRenderer: ErrorRenderer = new ErrorRenderer();
private errorRenderer: ErrorRenderer = renderer;
constructor(
private logger: ILogger,
@@ -27,8 +29,14 @@ export class ChatWootErrorReporter {
error: any,
replyTo?: number,
) {
const errorText = this.errorRenderer.renderError(error);
const errorText = this.errorRenderer.text(error);
this.logger.error(errorText);
try {
const data = this.errorRenderer.data(error);
this.logger.error(JSON.stringify(data, null, 2));
} catch (err) {
this.logger.error(`Error occurred while login details for error: ${err}`);
}
const template = this.l.key(TKey.JOB_REPORT_ERROR);
const nextDelay = NextAttemptDelayInWholeSeconds(this.job);
const attempts = {
@@ -69,9 +77,6 @@ export class ChatWootErrorReporter {
type: MessageType,
replyTo?: number,
): Promise<void> {
const jobUrl = `http://localhost:3000/jobs/queue/${encodeURIComponent(
this.job.queueName,
)}/${this.job.id}`;
const template = this.l.key(TKey.JOB_REPORT_SUCCEEDED);
const attempts = {
current: this.job.attemptsMade + 1,
@@ -79,10 +84,7 @@ export class ChatWootErrorReporter {
};
const content = template.render({
details: {
text: `${this.job.queueName} => ${this.job.id}`,
url: jobUrl,
},
details: JobLink(this.job),
attempts: attempts,
});
+105 -40
View File
@@ -1,45 +1,28 @@
import { ApiError } from '@figuro/chatwoot-sdk/dist/core/ApiError';
import { AxiosError } from 'axios';
import { ApiError as ChatWootAPIError } from '@figuro/chatwoot-sdk/dist/core/ApiError';
import type { AxiosError } from 'axios';
/**
* Class responsible for rendering error information based on error type
*/
export class ErrorRenderer {
interface IErrorRender<T> {
/**
* Renders error information based on error type
* @param error The error to render
* @returns Formatted error text
* Get an error text to show to the user
*/
public renderError(error: any): string {
// Handle Axios errors
if (error.isAxiosError) {
return this.renderAxiosError(error as AxiosError);
}
// Handle Chatwoot SDK ApiError
if (error instanceof ApiError) {
return this.renderApiError(error);
}
// For all other errors
return this.renderGenericError(error);
}
text(error: T): string;
/**
* Renders Axios error information
* @param error The Axios error to render
* @returns Formatted error text
* Structured error data for logging/inspection
*/
private renderAxiosError(error: AxiosError): string {
let errorText = `Axios Error: ${error.message}`;
data(error: T): Record<string, any>;
}
class AxiosErrorRenderer implements IErrorRender<AxiosError> {
text(error: AxiosError): string {
let errorText = `API Error: ${error.message}`;
if (!error.response?.data) {
return errorText;
}
// Add response data if it exists and is JSON
try {
const data = error.response.data as any;
let json: undefined;
let json: any;
if (Buffer.isBuffer(data)) {
json = JSON.parse(data.toString());
} else if (typeof data === 'object') {
@@ -55,16 +38,35 @@ export class ErrorRenderer {
return errorText;
}
/**
* Renders Chatwoot SDK ApiError information
* @param error The ApiError to render
* @returns Formatted error text
*/
private renderApiError(error: ApiError): string {
data(error: AxiosError): Record<string, any> {
const { config, response, request } = error as any;
return {
class: 'APIError',
request: {
method: request?.method || config?.method,
url: request?.path || config?.url,
params: config?.params,
body: request?.body || config?.data,
},
response: {
status: response?.status,
statusText: response?.statusText,
headers: response?.headers,
body: response?.data,
},
message: error.message,
code: (error as any)?.code,
stack: error.stack,
};
}
}
class ChatWootAPIErrorRenderer implements IErrorRender<ChatWootAPIError> {
text(error: ChatWootAPIError): string {
let errorText = `ChatWoot API Error: ${error.message}`;
errorText += `\nStatus: ${error.status}`;
// Add body if it exists
if (error.body) {
try {
const body =
@@ -78,12 +80,75 @@ export class ErrorRenderer {
return errorText;
}
data(error: ChatWootAPIError): Record<string, any> {
return {
class: 'ChatWootAPIError',
request: {
method: error.request?.method,
url: error.request?.url,
query: error.request?.query,
path: error.request?.path,
body: error.request?.body,
},
response: {
status: error.status,
statusText: error.statusText,
body: error.body,
},
message: error.message,
stack: error.stack,
};
}
}
class GenericErrorRenderer implements IErrorRender<any> {
text(error: any): string {
return error?.toString?.() ?? String(error);
}
data(error: any): Record<string, any> {
return {
class: error?.name ?? error?.constructor?.name ?? typeof error,
message: error?.message ?? String(error),
stack: error.stack,
};
}
}
/**
* Class responsible for rendering error information based on error type
*/
export class ErrorRenderer implements IErrorRender<any> {
private readonly axiosRenderer = new AxiosErrorRenderer();
private readonly chatwootRenderer = new ChatWootAPIErrorRenderer();
private readonly genericRenderer = new GenericErrorRenderer();
/**
* Renders generic error information
* Renders error information based on error type
* @param error The error to render
* @returns Formatted error text
*/
private renderGenericError(error: any): string {
return error.toString();
public text(error: any): string {
if (error?.isAxiosError) {
return this.axiosRenderer.text(error as AxiosError);
}
if (error instanceof ChatWootAPIError) {
return this.chatwootRenderer.text(error);
}
return this.genericRenderer.text(error);
}
public data(error: any): Record<string, any> {
if (error?.isAxiosError) {
return this.axiosRenderer.data(error as AxiosError);
}
if (error instanceof ChatWootAPIError) {
return this.chatwootRenderer.data(error);
}
return this.genericRenderer.data(error);
}
}
+6
View File
@@ -9,3 +9,9 @@ export class PhoneNumberNotFoundInWhatsAppError extends Error {
super(`Phone number not found in WhatsApp: ${phone}`);
}
}
export class UnknownJIDFormat extends Error {
constructor(public jid: string) {
super(`WhatsApp Chat ID is not recognized: ${jid}`);
}
}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *تم تعديلها في واتساب*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*تفاعل* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *হোয়াটসঅ্যাপে সম্পাদিত*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*প্রতিক্রিয়া দিয়েছেন* {{{emoji}}}
+2 -2
View File
@@ -52,10 +52,10 @@ message.edited.in.whatsapp: |-
✏️ *In WhatsApp bearbeitet*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reagiert mit* {{{emoji}}}
+2 -2
View File
@@ -50,10 +50,10 @@ message.edited.in.whatsapp: |-
✏️ *Edited in WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reacted* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *Editado en WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reaccionó con* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *ویرایش شده در واتساپ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*واکنش* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *Modifié dans WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*A réagi avec* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *נערך בוואטסאפ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*הגיב* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *व्हाट्सएप में संपादित*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*प्रतिक्रिया दी* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *Diedit di WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Bereaksi* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *ਵਟਸਐਪ ਵਿੱਚ ਸੰਪਾਦਿਤ ਕੀਤਾ ਗਿਆ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*ਪ੍ਰਤੀਕਿਰਿਆ ਕੀਤੀ* {{{emoji}}}
+2 -2
View File
@@ -52,10 +52,10 @@ message.edited.in.whatsapp: |-
✏️ *Editado no WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reagiu com* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *Отредактировано в WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Реакция* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *WhatsApp'ta düzenlendi*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Tepki verdi* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *Відредаговано у WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Реакція* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *واٹس ایپ میں ترمیم کی گئی*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*ردعمل دیا* {{{emoji}}}
+2 -2
View File
@@ -49,10 +49,10 @@ message.edited.in.whatsapp: |-
✏️ *已在 WhatsApp 中编辑*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*已反应* {{{emoji}}}
+1 -1
View File
@@ -1,6 +1,6 @@
import type { proto } from '@adiwajshing/baileys';
import { ChatWootCommandsConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { WAMessage } from '@waha/structures/responses.dto';
import { proto } from '@adiwajshing/baileys';
import { SimpleVCardInfo } from '@waha/core/vcard';
export enum TKey {
@@ -129,10 +129,8 @@ export class ChatWootAppService implements IAppService {
private async setupCustomAttributes(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const api = di.CustomAttributesAPI();
for (const attr of CHATWOOT_CUSTOM_ATTRIBUTES) {
await api.upsert(attr);
}
const service = di.CustomAttributesService();
await service.upsert(CHATWOOT_CUSTOM_ATTRIBUTES);
}
private cleanCache(app: App<ChatWootAppConfig>) {
@@ -19,6 +19,10 @@ export class ChatWootQueueService {
private readonly messageUpdatedQueue: Queue,
@InjectQueue(QueueName.INBOX_MESSAGE_DELETED)
private readonly messageDeletedQueue: Queue,
@InjectQueue(QueueName.INBOX_CONVERSATION_CREATED)
private readonly conversationCreatedQueue: Queue,
@InjectQueue(QueueName.INBOX_CONVERSATION_STATUS_CHANGED)
private readonly conversationStatusChanged: Queue,
@InjectQueue(QueueName.INBOX_COMMANDS)
private readonly commandsQueue: Queue,
) {}
@@ -41,10 +45,14 @@ export class ChatWootQueueService {
*/
private getQueueForEvent(event: string): Queue | null {
switch (event) {
case EventName.CONVERSATION_CREATED:
return this.conversationCreatedQueue;
case EventName.MESSAGE_CREATED:
return this.messageCreatedQueue;
case EventName.MESSAGE_UPDATED:
return this.messageUpdatedQueue;
case EventName.CONVERSATION_STATUS_CHANGED:
return this.conversationStatusChanged;
case 'message_deleted':
return this.messageDeletedQueue;
case 'commands':
@@ -95,9 +103,9 @@ export class ChatWootQueueService {
*/
async addJobToQueue(event: string, data: InboxData): Promise<any> {
const queue = this.getQueueForEvent(event);
if (queue) {
return await this.add(queue, event, data);
if (!queue) {
return;
}
return { ignored: true, event };
await this.add(queue, event, data);
}
}
@@ -0,0 +1,72 @@
import { ConversationStatus } from '@waha/apps/chatwoot/client/types';
import { contact_conversations } from '@figuro/chatwoot-sdk/dist/models/contact_conversations';
import * as lodash from 'lodash';
import type { conversation } from '@figuro/chatwoot-sdk/dist/models/conversation';
export enum ConversationSort {
activity_newest = 'activity_newest',
created_newest = 'created_newest',
created_oldest = 'created_oldest',
activity_oldest = 'activity_oldest',
}
export type ConversationSelectorConfig = {
sort: ConversationSort;
status?: Array<ConversationStatus>;
inboxId: number;
};
export type ConversationResult = Pick<conversation, 'id' | 'account_id'>;
export class ConversationSelector {
constructor(private config: ConversationSelectorConfig) {}
hasStatusFilter() {
return this.config.status;
}
select(conversations: contact_conversations): ConversationResult | null {
conversations = this.filter(conversations);
conversations = this.sort(conversations);
return conversations[0] || null;
}
private filter(conversations: contact_conversations): contact_conversations {
// Filter by inbox id
conversations = lodash.filter(conversations, {
inbox_id: this.config.inboxId,
}) as contact_conversations;
// Filter by status
if (this.config.status && this.config.status.length > 0) {
conversations = lodash.filter(conversations, (conversation) => {
return this.config.status.includes(
conversation.status as ConversationStatus,
);
});
}
return conversations;
}
private sort(conversations: contact_conversations): contact_conversations {
let field = null;
let dir = null;
switch (this.config.sort) {
case ConversationSort.activity_newest:
[field, dir] = ['last_activity_at', 'desc'];
break;
case ConversationSort.created_newest:
[field, dir] = ['created_at', 'desc'];
break;
case ConversationSort.created_oldest:
[field, dir] = ['created_at', 'asc'];
break;
case ConversationSort.activity_oldest:
[field, dir] = ['last_activity_at', 'asc'];
}
if (!field || !dir) {
return conversations;
}
return lodash.orderBy(conversations, [field], [dir]);
}
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { WAHAEngine } from '@waha/structures/enums.dto';
import { getEngineName } from '@waha/version';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import { toCusFormat } from '@waha/core/utils/jids';
interface IEngineHelper {
WhatsAppMessageKeys(message: any): WhatsAppMessage;
@@ -74,4 +74,17 @@ export class MessageMappingRepository {
})
.first();
}
async getByChatwootMessageIdAndPart(
id: number,
part: number,
): Promise<MessageMapping | null> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
chatwoot_message_id: id,
part: part,
})
.first();
}
}
@@ -123,4 +123,26 @@ export class MessageMappingService {
}
return whatsapp;
}
async getMappingByChatwootCombinedKeyAndPart(
chatwoot: ChatWootCombinedKey,
part: number,
): Promise<MessageMapping | null> {
const chatwootMessages =
await this.chatwootMessageRepository.getByCombinedKey(chatwoot);
if (!chatwootMessages || chatwootMessages.length === 0) {
return null;
}
for (const cw of chatwootMessages) {
const mapping =
await this.messageMappingRepository.getByChatwootMessageIdAndPart(
cw.id,
part,
);
if (mapping) {
return mapping;
}
}
return null;
}
}
+1 -1
View File
@@ -1,4 +1,3 @@
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
CoreMediaConverter,
IMediaConverter,
@@ -7,6 +6,7 @@ import { MessagesForRead } from '@waha/core/utils/convertors';
import {
IgnoreJidConfig,
isJidNewsletter,
isJidBroadcast,
JidFilter,
} from '@waha/core/utils/jids';
import {
+1 -1
View File
@@ -11,7 +11,7 @@ export function BasicAuthFunction(username, password, exclude: string[] = []) {
const auth = basicAuth({
challenge: true,
users: {
[username]: password,
[String(username)]: String(password),
},
});
auth(req, res, next);
+1 -1
View File
@@ -1,7 +1,7 @@
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import * as gows from './types';
import { toCusFormat } from '@waha/core/utils/jids';
/**
* Converts a GOWS LabelEdit event to a Label DTO
+73 -30
View File
@@ -1,14 +1,4 @@
import {
aggregateMessageKeysNotFromMe,
getContentType,
getUrlFromDirectPath,
isJidGroup,
jidNormalizedUser,
normalizeMessageContent,
proto,
WAMessageKey,
} from '@adiwajshing/baileys';
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import type { proto, WAMessageKey } from '@adiwajshing/baileys';
import * as grpc from '@grpc/grpc-js';
import { connectivityState } from '@grpc/grpc-js';
import { UnprocessableEntityException } from '@nestjs/common';
@@ -37,7 +27,6 @@ import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFacto
import {
extractBody,
getDestination,
toCusFormat,
} from '@waha/core/engines/noweb/session.noweb.core';
import { extractMediaContent } from '@waha/core/engines/noweb/utils';
import {
@@ -48,7 +37,12 @@ import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { toJID } from '@waha/core/utils/jids';
import {
isJidBroadcast,
isJidGroup,
toCusFormat,
toJID,
} from '@waha/core/utils/jids';
import {
Channel,
ChannelListResult,
@@ -148,6 +142,7 @@ import {
retry,
share,
} from 'rxjs';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { promisify } from 'util';
@@ -170,7 +165,7 @@ import {
isLabelChatAddedEvent,
isLabelUpsertEvent,
} from './labels.gows';
import IMessageKey = proto.IMessageKey;
import esm from '@waha/vendor/esm';
enum WhatsMeowEvent {
CONNECTED = 'gows.ConnectedEventData',
@@ -218,6 +213,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
public session: messages.Session;
protected presences: any;
private local$ = new Subject<EnginePayload>();
public constructor(config) {
super(config);
this.qr = new QR();
@@ -304,7 +301,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
);
// Retry on error with delay
this.all$ = this.stream$.pipe(retry({ delay: 2_000 }), share());
// Accept locally re-issued events as well
this.all$ = merge(this.stream$, this.local$).pipe(
retry({ delay: 2_000 }),
share(),
);
}
subscribeEvents() {
@@ -314,9 +315,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
events.on(WhatsMeowEvent.CONNECTED, (data) => {
this.status = WAHASessionStatus.WORKING;
this.me = {
id: toCusFormat(jidNormalizedUser(data.ID)),
id: toCusFormat(esm.b.jidNormalizedUser(data.ID)),
pushName: data.PushName,
lid: jidNormalizedUser(data.LID),
lid: esm.b.jidNormalizedUser(data.LID),
};
// @ts-ignore
this.me.jid = data.ID;
@@ -390,6 +391,48 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// add new value
this.presences.set(Chat, [...filtered, event]);
});
//
// Fix for "typing" after sending a message
// Re-issue a synthetic ChatPresence(PAUSED) to cancel COMPOSING state
// Works for both DM and Group chats
//
events.on(WhatsMeowEvent.MESSAGE, (message: any) => {
const chat = message?.Info?.Chat;
if (!this.jids.include(chat)) {
return;
}
if (message?.Info?.IsFromMe) {
return;
}
const sender = message?.Info?.Sender || chat;
if (!chat || !sender) {
return;
}
const stored: Array<gows.Presence | gows.ChatPresence> =
this.presences.get(chat) || [];
const composing = stored.find(
(presence: any) =>
(presence?.Sender === sender || presence?.From === sender) &&
presence?.State === gows.ChatPresenceState.COMPOSING,
) as gows.ChatPresence | undefined;
if (!composing) {
return;
}
const presence: gows.ChatPresence = {
Chat: chat,
Sender: sender,
IsFromMe: false,
IsGroup: !!isJidGroup(chat),
State: gows.ChatPresenceState.PAUSED,
Media: (composing as any)?.Media ?? gows.ChatPresenceMedia.TEXT,
} as any;
this.local$.next({
event: WhatsMeowEvent.CHAT_PRESENCE,
data: presence,
} as any);
});
events.start();
}
@@ -908,8 +951,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
const data = response.toObject();
const info = data.infos[0];
return {
numberExists: info.registered,
chatId: toCusFormat(info.jid),
numberExists: info?.registered || false,
chatId: toCusFormat(info?.jid || null),
};
}
@@ -961,7 +1004,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
if (keys.length === 0) {
return;
}
const receipts = aggregateMessageKeysNotFromMe(keys);
const receipts = esm.b.aggregateMessageKeysNotFromMe(keys);
for (const receipt of receipts) {
if (receipt.messageIds.length === 0) {
return;
@@ -1403,11 +1446,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
(newsletter.role?.toUpperCase() as ChannelRole) || ChannelRole.GUEST;
let picture = newsletter.picture;
if (picture.startsWith('/')) {
picture = getUrlFromDirectPath(picture);
picture = esm.b.getUrlFromDirectPath(picture);
}
let preview = newsletter.preview;
if (preview.startsWith('/')) {
preview = getUrlFromDirectPath(preview);
preview = esm.b.getUrlFromDirectPath(preview);
}
return {
id: newsletter.id,
@@ -1913,8 +1956,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// Ignore protocol messages
if (message.Message.protocolMessage) return;
const normalizedContent = normalizeMessageContent(message.Message);
const contentType = getContentType(normalizedContent);
const normalizedContent = esm.b.normalizeMessageContent(message.Message);
const contentType = esm.b.getContentType(normalizedContent);
// Ignore device sent message
if (contentType == 'deviceSentMessage') {
return;
@@ -2001,13 +2044,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
private toPollVotePayload(event: any): PollVotePayload {
// Extract event creation message key from the message
const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey;
const pollKey: IMessageKey = {
const pollKey: proto.IMessageKey = {
remoteJid: creationKey.remoteJID,
fromMe: creationKey.fromMe,
id: creationKey.ID,
participant: creationKey.participant,
};
const voteKey: IMessageKey = {
const voteKey: proto.IMessageKey = {
id: event.Info.ID,
remoteJid: event.Info.Chat,
participant: event.IsGroup ? event.Info.Sender : null,
@@ -2074,7 +2117,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
}
protected extractReplyTo(message): ReplyToMessage | null {
const msgType = getContentType(message);
const msgType = esm.b.getContentType(message);
const contextInfo = message[msgType]?.contextInfo;
if (!contextInfo) {
return null;
@@ -2334,10 +2377,10 @@ export function getMessageIdFromSerialized(serialized: string): string | null {
*/
function fixPollCreationKey(
vote: IMessageKey,
poll: IMessageKey,
vote: proto.IMessageKey,
poll: proto.IMessageKey,
me: MeInfo,
): IMessageKey {
): proto.IMessageKey {
// If the vote is from me, the pollCreationKey is already in my perspective
if (vote?.fromMe) {
return poll;
+5 -4
View File
@@ -1,11 +1,10 @@
import { GroupMetadata, jidNormalizedUser } from '@adiwajshing/baileys';
import type { GroupMetadata } from '@adiwajshing/baileys';
import type { Contact } from '@adiwajshing/baileys/lib/Types/Contact';
import {
import type {
GroupParticipant as NOWEBGroupParticipant,
ParticipantAction,
} from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { getGroupInviteLink } from '@waha/core/abc/session.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import {
GroupInfo,
GroupParticipant,
@@ -18,6 +17,8 @@ import {
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
import { toCusFormat } from '@waha/core/utils/jids';
import esm from '@waha/vendor/esm';
export function ToGroupInfo(group: Partial<GroupMetadata>): GroupInfo {
let participants: GroupParticipant[] = undefined;
@@ -128,7 +129,7 @@ export function ToGroupV2LeaveEvent(
if (!me) {
return null;
}
const meId = jidNormalizedUser(me.id);
const meId = esm.b.jidNormalizedUser(me.id);
if (!update.participants.includes(meId)) {
return null;
}
@@ -0,0 +1,4 @@
export enum LabelAssociationType {
Chat = 'label_jid',
Message = 'label_message',
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { generateWAMessageFromContent, proto } from '@adiwajshing/baileys';
import { Button, ButtonType } from '@waha/structures/chatting.buttons.dto';
import esm from '@waha/vendor/esm';
function toName(type: ButtonType) {
switch (type) {
@@ -96,8 +96,8 @@ export async function sendButtonMessage(
};
}
const msg = proto.Message.fromObject(data);
const fullMessage = generateWAMessageFromContent(chatId, msg, {
const msg = esm.b.proto.Message.create(data);
const fullMessage = esm.b.generateWAMessageFromContent(chatId, msg, {
userJid: sock?.user?.id,
});
await sock.relayMessage(chatId, fullMessage.message, {
+23 -16
View File
@@ -11,7 +11,7 @@ import makeWASocket, {
getContentType,
getKeyAuthor,
isJidGroup,
isJidUser,
isPnUser,
isRealMessage,
jidNormalizedUser,
makeCacheableSignalKeyStore,
@@ -24,6 +24,7 @@ import makeWASocket, {
WAMessageKey,
WAMessageUpdate,
} from '@adiwajshing/baileys';
import type { WABrowserDescription } from '@adiwajshing/baileys';
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
import { BaileysEventMap } from '@adiwajshing/baileys/lib/Types/Events';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
@@ -308,9 +309,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
getSocketConfig(agent, state): Partial<SocketConfig> {
const fullSyncEnabled = this.sessionConfig?.noweb?.store?.fullSync || false;
const browser = fullSyncEnabled
? Browsers.ubuntu('Desktop')
: Browsers.ubuntu('Chrome');
const browser = ['Ubuntu', 'Chrome', '20.0.04'] as WABrowserDescription;
let markOnlineOnConnect = this.sessionConfig?.noweb?.markOnline;
if (markOnlineOnConnect == undefined) {
markOnlineOnConnect = true;
@@ -410,9 +409,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
await this.ensureStore();
this.sock = await this.makeSocket();
this.fixMessages();
this.issueMessageUpdateOnEdits();
this.issueMessageUpdateOnPoll();
this.fixMessageUpsertStatus();
this.issuePresenceUpdateOnMessageUpsert();
if (this.isDebugEnabled()) {
this.listenEngineEventsInDebugMode();
@@ -439,7 +438,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
key: WAMessageKey,
): Promise<WAMessageContent | undefined> {
if (!this.store) {
return proto.Message.fromObject({});
return proto.Message.create({});
}
const msg = await this.store.loadMessage(key.remoteJid, key.id);
return msg?.message || undefined;
@@ -586,12 +585,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
await this.store?.close();
}
private fixMessageUpsertStatus() {
// If no status - set it to WAMessageAck.DEVICE
private fixMessages() {
this.sock.ev.on('messages.upsert', ({ messages }) => {
for (const message of messages) {
if (message.status == null) {
message.status = AckToStatus(WAMessageAck.DEVICE);
// If no status - set it to WAMessageAck.DEVICE
message.status = message.status ?? AckToStatus(WAMessageAck.DEVICE);
// Fix fromMe in @lid addressed groups
// https://github.com/devlikeapro/waha/issues/1350
if (message.key.participant === this.getSessionMeInfo()?.lid) {
message.key.fromMe = true;
}
}
});
@@ -737,7 +740,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return {
id: toCusFormat(meId),
pushName: me.name,
lid: me.lid,
lid: jidNormalizedUser(me.lid),
};
}
@@ -2141,7 +2144,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.logger.debug({ jid: update.id }, 'Profile picture updated');
const url = await this.refreshProfilePicture(update.id);
if (isJidUser(update.id)) {
if (isPnUser(update.id) || isLidUser(update.id)) {
// update 123@c.us and 123 profiles as well
const cus = toCusFormat(update.id);
this.profilePictures.set(cus, url);
@@ -2577,11 +2580,16 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
async getMediaBuffer(message: any): Promise<Buffer | null> {
const content = extractMediaContent(message.message);
const url = content.url;
// Fix Stickers
// https://github.com/devlikeapro/waha/issues/504
const url = content.url;
// Set it to null so the engine handles it right
if (!hasPath(url)) {
// Set it to null so the engine handles it right
content.url = null;
}
// Fix Newsletter
// directPath has the unencrypted path
if (isJidNewsletter(message.key.remoteJid) && content.directPath) {
content.url = null;
}
@@ -2594,8 +2602,7 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
reuploadRequest: this.session.sock.updateMediaMessage,
},
).finally(() => {
// Fix Stickers - set url back, just to have it in the response
// https://github.com/devlikeapro/waha/issues/504
// Set url back in case we removed it
content.url = url;
})) as Buffer;
}
@@ -1,4 +1,4 @@
import { Chat } from '@adiwajshing/baileys';
import type { Chat } from '@adiwajshing/baileys';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -1,4 +1,4 @@
import { Contact } from '@adiwajshing/baileys';
import type { Contact } from '@adiwajshing/baileys';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IContactRepository {
@@ -1,4 +1,4 @@
import { GroupMetadata } from '@adiwajshing/baileys';
import type { GroupMetadata } from '@adiwajshing/baileys';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IGroupRepository {
@@ -1,4 +1,4 @@
import {
import type {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
@@ -1,5 +1,4 @@
import { Contact } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
export interface ILabelsRepository {
getById(id: string): Promise<Label | null>;
+3 -3
View File
@@ -1,11 +1,11 @@
import {
import type {
BaileysEventEmitter,
Chat,
Contact,
proto,
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import type { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
GetChatMessagesFilter,
OverviewFilter,
@@ -1,10 +1,6 @@
import makeWASocket, {
Chat,
Contact,
GroupMetadata,
proto,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import type { Chat, Contact, GroupMetadata, proto } from '@adiwajshing/baileys';
import type makeWASocket from '@adiwajshing/baileys';
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException } from '@nestjs/common';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
@@ -1,31 +1,25 @@
import makeWASocket, {
areJidsSameUser,
import type makeWASocket from '@adiwajshing/baileys';
import type {
BaileysEventEmitter,
Chat,
ChatUpdate,
Contact,
GroupParticipant,
isJidUser,
isRealMessage,
jidNormalizedUser,
Contact,
ParticipantAction,
proto,
updateMessageWithReaction,
updateMessageWithReceipt,
WAMessage,
} from '@adiwajshing/baileys';
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import type { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
import type { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
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 { JidFilter } from '@waha/core/utils/jids';
import {
isLidUser,
isPnUser,
JidFilter,
jidsFromKey,
} from '@waha/core/utils/jids';
import {
GetChatMessagesFilter,
OverviewFilter,
@@ -41,7 +35,6 @@ import { waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
import { filter } from 'rxjs';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
@@ -49,6 +42,8 @@ import { IMessagesRepository } from './IMessagesRepository';
import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository';
import { INowebStorage } from './INowebStorage';
import { INowebStore } from './INowebStore';
import { LabelAssociationType } from '../labels/LabelAssociationType';
import esm from '@waha/vendor/esm';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
@@ -118,18 +113,20 @@ export class NowebPersistentStore implements INowebStore {
if (!message.key) {
return null;
}
// Phone
let pn = message.key.senderPn || message.key.participantPn;
const jids = jidsFromKey(message.key);
if (!jids) {
return null;
}
let { lid, pn } = jids;
// 123 => 123@s.whatsapp.net
if (pn && !pn.includes('@')) {
pn = `${pn}@s.whatsapp.net`;
}
// 123@c.us => 123@s.whatsapp.net
if (pn && !isJidUser(pn)) {
pn = jidNormalizedUser(pn);
if (pn && !isPnUser(pn)) {
pn = esm.b.jidNormalizedUser(pn);
}
// 999@lid
const lid = message.key.senderLid || message.key.participantLid;
return {
id: message.key.remoteJid,
lid: lid,
@@ -185,6 +182,16 @@ export class NowebPersistentStore implements INowebStore {
),
);
// Lids
ev.on('lid-mapping.update', (data) => {
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates([data]);
this.logger.debug(
`lid-mapping.update - '${lids.length}' synced lid to pn mapping`,
);
});
});
// Contacts
ev.on('contacts.upsert', (data) => {
this.withLock('contacts', () => this.onContactsUpsert(data));
@@ -221,17 +228,7 @@ export class NowebPersistentStore implements INowebStore {
}
private async onMessagingHistorySet(history) {
const { contacts, chats, messages, isLatest } = history;
if (isLatest) {
this.logger.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()),
]);
}
const { contacts, chats, messages } = history;
await Promise.all([
this.withLock('contacts', async () => {
@@ -250,7 +247,7 @@ export class NowebPersistentStore implements INowebStore {
}
private async syncMessagesHistory(messages) {
const realMessages = messages.filter(isRealMessage);
const realMessages = messages.filter(esm.b.isRealMessage);
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
await this.messagesRepo.upsert(realMessages);
this.logger.info(
@@ -266,7 +263,7 @@ export class NowebPersistentStore implements INowebStore {
}
let messages = update.messages;
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
const realMessages = messages.filter(isRealMessage);
const realMessages = messages.filter(esm.b.isRealMessage);
await this.messagesRepo.upsert(realMessages);
this.logger.debug(
`messages.upsert - ${messages.length} got messages, ${realMessages.length} real messages`,
@@ -276,7 +273,7 @@ export class NowebPersistentStore implements INowebStore {
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 jid = esm.b.jidNormalizedUser(update.key.remoteJid!);
if (!this.jids.include(jid)) {
continue;
}
@@ -319,7 +316,8 @@ export class NowebPersistentStore implements INowebStore {
// 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;
esm.b.isRealMessage(message, this.socket?.authState?.creds?.me?.id) ||
false;
if (isYetRealMessage) {
await this.messagesRepo.upsertOne(message);
} else {
@@ -333,7 +331,7 @@ export class NowebPersistentStore implements INowebStore {
await this.messagesRepo.deleteAllByJid(item.jid);
return;
}
const jid = jidNormalizedUser(item.keys[0].remoteJid);
const jid = esm.b.jidNormalizedUser(item.keys[0].remoteJid);
const ids = item.keys.map((key) => key.id);
await this.messagesRepo.deleteByJidByIds(jid, ids);
}
@@ -383,7 +381,7 @@ export class NowebPersistentStore implements INowebStore {
// Remove the group if the current user is removed
const myJid = this.socket?.authState?.creds?.me?.id;
const participantsIncludesMe = lodash.find(participants, (p) =>
areJidsSameUser(p, myJid),
esm.b.areJidsSameUser(p, myJid),
);
if (participantsIncludesMe) {
await this.groupRepo.deleteById(id);
@@ -534,7 +532,7 @@ export class NowebPersistentStore implements INowebStore {
);
continue;
}
updateMessageWithReaction(msg, reaction);
esm.b.updateMessageWithReaction(msg, reaction);
await this.messagesRepo.upsertOne(msg);
}
}
@@ -553,7 +551,7 @@ export class NowebPersistentStore implements INowebStore {
);
continue;
}
updateMessageWithReceipt(msg, receipt);
esm.b.updateMessageWithReceipt(msg, receipt);
await this.messagesRepo.upsertOne(msg);
}
}
@@ -596,7 +594,7 @@ export class NowebPersistentStore implements INowebStore {
if (!data) {
return null;
}
return proto.WebMessageInfo.fromObject(data);
return esm.b.proto.WebMessageInfo.create(data);
}
getMessagesByJid(
@@ -702,23 +700,23 @@ export class NowebPersistentStore implements INowebStore {
let lids: LidToPN[] = [];
for (const contact of contacts) {
// contact.id = pn, contact.lid = lid
if (isJidUser(contact.id) && isLidUser(contact.lid)) {
if (isPnUser(contact.id) && isLidUser(contact.lid)) {
lids.push({
pn: contact.id,
id: contact.lid,
});
}
// contact.pn = pn, contact.lid = lid
else if (isJidUser(contact.jid) && isLidUser(contact.lid)) {
// contact.phoneNumber = pn, contact.lid = lid
else if (isPnUser(contact.phoneNumber) && isLidUser(contact.lid)) {
lids.push({
pn: contact.jid,
pn: contact.phoneNumber,
id: contact.lid,
});
}
// contact.pn = pn, contact.id = lid
else if (isJidUser(contact.jid) && isLidUser(contact.id)) {
// contact.phoneNumber = pn, contact.id = lid
else if (isPnUser(contact.phoneNumber) && isLidUser(contact.id)) {
lids.push({
pn: contact.jid,
pn: contact.phoneNumber,
id: contact.id,
});
}
@@ -11,28 +11,19 @@ import type {
WAMessageCursor,
WAMessageKey,
} from '@adiwajshing/baileys/lib/Types';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
import * as lodash from 'lodash';
import type {
LabelAssociation,
LabelAssociationType,
MessageLabelAssociation,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import {
md5,
toNumber,
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys/lib/Utils';
import {
jidDecode,
jidNormalizedUser,
} from '@adiwajshing/baileys/lib/WABinary';
import { proto } from '@adiwajshing/baileys/WAProto';
import type { Comparable } from '@adiwajshing/keyed-db/lib/Types';
import type { Logger } from 'pino';
import makeOrderedDictionary from './make-ordered-dictionary';
import { ObjectRepository } from './object-repository';
import { LabelAssociationType } from '@waha/core/engines/noweb/labels/LabelAssociationType';
import esm from '@waha/vendor/esm';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const KeyedDB = require('@adiwajshing/keyed-db').default;
@@ -137,7 +128,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
isLatest,
syncType,
}) => {
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
if (syncType === esm.b.proto.HistorySync.HistorySyncType.ON_DEMAND) {
return; // FOR NOW,
//TODO: HANDLE
}
@@ -187,10 +178,11 @@ export default (config: BaileysInMemoryStoreConfig) => {
} else {
const contactHashes = await Promise.all(
Object.keys(contacts).map(async (contactId) => {
const { user } = jidDecode(contactId)!;
const { user } = esm.b.jidDecode(contactId)!;
return [
contactId,
(await md5(Buffer.from(user + 'WA_ADD_NOTIF', 'utf8')))
esm.b
.md5(Buffer.from(user + 'WA_ADD_NOTIF', 'utf8'))
.toString('base64')
.slice(0, 3),
];
@@ -279,7 +271,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
case 'append':
case 'notify':
for (const msg of newMessages) {
const jid = jidNormalizedUser(msg.key.remoteJid!);
const jid = esm.b.jidNormalizedUser(msg.key.remoteJid!);
const list = assertMessageList(jid);
list.upsert(msg, 'append');
@@ -287,7 +279,9 @@ export default (config: BaileysInMemoryStoreConfig) => {
ev.emit('chats.upsert', [
{
id: jid,
conversationTimestamp: toNumber(msg.messageTimestamp as any),
conversationTimestamp: lodash.toNumber(
msg.messageTimestamp as any,
),
unreadCount: 1,
},
]);
@@ -299,7 +293,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
});
ev.on('messages.update', (updates) => {
for (const { update, key } of updates) {
const list = assertMessageList(jidNormalizedUser(key.remoteJid!));
const list = assertMessageList(esm.b.jidNormalizedUser(key.remoteJid!));
if (update?.status) {
const listStatus = list.get(key.id!)?.status;
if (listStatus && update?.status <= listStatus) {
@@ -382,7 +376,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
const obj = messages[key.remoteJid!];
const msg = obj?.get(key.id!);
if (msg) {
updateMessageWithReceipt(msg, receipt);
esm.b.updateMessageWithReceipt(msg, receipt);
}
}
});
@@ -392,7 +386,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
const obj = messages[key.remoteJid!];
const msg = obj?.get(key.id!);
if (msg) {
updateMessageWithReaction(msg, reaction);
esm.b.updateMessageWithReaction(msg, reaction);
}
}
});
@@ -420,7 +414,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
for (const jid in json.messages) {
const list = assertMessageList(jid);
for (const msg of json.messages[jid]) {
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
list.upsert(esm.b.proto.WebMessageInfo.create(msg), 'append');
}
}
};
+2 -2
View File
@@ -1,5 +1,5 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import type { WAMessage } from '@adiwajshing/baileys';
import type { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export const NowebMessagesMetadata = new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
@@ -1,4 +1,4 @@
import { Chat } from '@adiwajshing/baileys';
import type { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -1,8 +1,6 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import type { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { LabelAssociationType } from '@waha/core/engines/noweb/labels/LabelAssociationType';
export class SqlLabelAssociationsMethods {
constructor(private repository: SqlKVRepository<any>) {}
@@ -1,9 +1,9 @@
import { BufferJSON } from '@adiwajshing/baileys/lib/Utils';
import {
convertProtobufToPlainObject,
replaceLongsWithNumber,
} from '@waha/core/engines/noweb/utils';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
import esm from '@waha/vendor/esm';
/**
* Key value repository with extra metadata
@@ -13,11 +13,11 @@ export class NOWEBSqlite3KVRepository<
Entity,
> extends Sqlite3KVRepository<Entity> {
protected stringify(data: any): string {
return JSON.stringify(data, BufferJSON.replacer);
return JSON.stringify(data, esm.b.BufferJSON.replacer);
}
protected parse(row: any): any {
return JSON.parse(row.data, BufferJSON.reviver);
return JSON.parse(row.data, esm.b.BufferJSON.reviver);
}
protected dump(entity: Entity) {
@@ -1,4 +1,4 @@
import { Chat } from '@adiwajshing/baileys';
import type { Chat } from '@adiwajshing/baileys';
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
import { OverviewFilter } from '@waha/structures/chats.dto';
@@ -1,4 +1,4 @@
import { Contact } from '@adiwajshing/baileys';
import type { Contact } from '@adiwajshing/baileys';
import { NowebContactSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -1,4 +1,4 @@
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import type { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { NowebGroupsSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -1,4 +1,4 @@
import {
import type {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
@@ -1,4 +1,4 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { NowebLabelsSchema } from '@waha/core/engines/noweb/store/schemas';
@@ -1,11 +1,10 @@
import {
import type {
AuthenticationCreds,
AuthenticationState,
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink } from 'fs/promises';
import { join } from 'path';
import esm from '@waha/vendor/esm';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
@@ -43,7 +42,7 @@ export const useMultiFileAuthState = async (
return fileLock.acquire(filePath, () =>
writeFileAtomic(
join(filePath),
JSON.stringify(data, BufferJSON.replacer),
JSON.stringify(data, esm.b.BufferJSON.replacer),
),
);
};
@@ -54,7 +53,7 @@ export const useMultiFileAuthState = async (
const data = await fileLock.acquire(filePath, () =>
readFile(filePath, { encoding: 'utf-8' }),
);
return JSON.parse(data, BufferJSON.reviver);
return JSON.parse(data, esm.b.BufferJSON.reviver);
} catch (error) {
return null;
}
@@ -84,7 +83,7 @@ export const useMultiFileAuthState = async (
file?.replace(/\//g, '__')?.replace(/:/g, '-') || '';
const creds: AuthenticationCreds =
(await readData('creds.json')) || initAuthCreds();
(await readData('creds.json')) || esm.b.initAuthCreds();
return {
state: {
@@ -96,7 +95,7 @@ export const useMultiFileAuthState = async (
ids.map(async (id) => {
let value = await readData(`${type}-${id}.json`);
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.fromObject(value);
value = esm.b.proto.Message.AppStateSyncKeyData.create(value);
}
data[id] = value;
+3 -2
View File
@@ -1,9 +1,10 @@
import { extractMessageContent, proto } from '@adiwajshing/baileys';
import type { proto } from '@adiwajshing/baileys';
import esm from '@waha/vendor/esm';
export function extractMediaContent(
content: any | proto.IMessage | null | undefined,
) {
content = extractMessageContent(content);
content = esm.b.extractMessageContent(content);
const mediaContent =
content?.documentMessage ||
content?.imageMessage ||
+1 -1
View File
@@ -223,7 +223,7 @@ export class WebjsClientCore extends Client {
// @ts-ignore
await window.Store.SendStatus.sendStatusTextMsgAction(status);
// @ts-ignore
const meUser = window.Store.User.getMeUser();
const meUser = window.Store.User.getMaybeMePnUser();
// @ts-ignore
const myStatus = window.Store.Status.getModelsArray().findLast(
(x) => x.id == meUser,
+14 -21
View File
@@ -1,14 +1,7 @@
import {
areJidsSameUser,
BinaryNode,
getBinaryNodeChildren,
getStatusFromReceiptType,
isJidGroup,
isJidStatusBroadcast,
jidEncode,
jidNormalizedUser,
proto,
} from '@adiwajshing/baileys';
import type { proto } from '@adiwajshing/baileys';
import type { BinaryNode } from '@adiwajshing/baileys';
import { isJidGroup, isJidStatusBroadcast } from '@waha/core/utils/jids';
import esm from '@waha/vendor/esm';
export interface ReceiptEvent {
key: proto.IMessageKey;
@@ -39,7 +32,7 @@ export function jid(field: any) {
? 's.whatsapp.net'
: 'lid';
}
return jidEncode(data.user, server, data.device);
return esm.b.jidEncode(data.user, server, data.device);
}
export function TagReceiptNodeToReceiptEvent(
@@ -47,17 +40,17 @@ export function TagReceiptNodeToReceiptEvent(
me: Me,
): ReceiptEvent[] {
const { attrs, content } = node;
const status = getStatusFromReceiptType(attrs.type);
const status = esm.b.getStatusFromReceiptType(attrs.type);
if (status == null) {
return [];
}
const from = jidNormalizedUser(jid(attrs.from));
const participant = jidNormalizedUser(jid(attrs.participant));
const recipient = jidNormalizedUser(jid(attrs.recipient));
const from = esm.b.jidNormalizedUser(jid(attrs.from));
const participant = esm.b.jidNormalizedUser(jid(attrs.participant));
const recipient = esm.b.jidNormalizedUser(jid(attrs.recipient));
const isLid = from.includes('lid');
const isNodeFromMe = areJidsSameUser(
const isNodeFromMe = esm.b.areJidsSameUser(
participant || from,
isLid ? me?.lid : me?.id,
);
@@ -66,7 +59,7 @@ export function TagReceiptNodeToReceiptEvent(
// basically, we only want to know when a message from us has been delivered to/read by the other person
// or another device of ours has read some messages
if (status < proto.WebMessageInfo.Status.SERVER_ACK && isNodeFromMe) {
if (status < esm.b.proto.WebMessageInfo.Status.SERVER_ACK && isNodeFromMe) {
return [];
}
@@ -78,7 +71,7 @@ export function TagReceiptNodeToReceiptEvent(
const ids = [attrs.id];
if (Array.isArray(content)) {
const items = getBinaryNodeChildren(content[0], 'item');
const items = esm.b.getBinaryNodeChildren(content[0], 'item');
ids.push(...items.map((i) => i.attrs.id));
}
@@ -134,12 +127,12 @@ function handleGroupedReceipts(
const participantKey = participants.attrs?.key;
if (!participantKey) continue;
const users = getBinaryNodeChildren(participants, 'user');
const users = esm.b.getBinaryNodeChildren(participants, 'user');
for (const user of users) {
const userAttrs = user.attrs;
if (!userAttrs) continue;
const userJid = jidNormalizedUser(jid(userAttrs.jid));
const userJid = esm.b.jidNormalizedUser(jid(userAttrs.jid));
if (!userJid) continue;
key.participant = fromMe ? (isLid ? me.lid : me.id) : userJid;
+2 -2
View File
@@ -1,9 +1,9 @@
import { BinaryNode, WAPresence } from '@adiwajshing/baileys';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import type { BinaryNode, WAPresence } from '@adiwajshing/baileys';
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
import { WAHAChatPresences } from '@waha/structures/presence.dto';
import { jid } from './ack.webjs';
import { toCusFormat } from '@waha/core/utils/jids';
export function TagPresenceToPresence(node: BinaryNode): WAHAChatPresences {
const { attrs } = node;
+33 -6
View File
@@ -1,13 +1,9 @@
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
import { UnprocessableEntityException } from '@nestjs/common';
import {
getChannelInviteLink,
WhatsappSession,
} from '@waha/core/abc/session.abc';
import {
getFromToParticipant,
toCusFormat,
} from '@waha/core/engines/noweb/session.noweb.core';
import { getFromToParticipant } from '@waha/core/engines/noweb/session.noweb.core';
import {
ReceiptEvent,
TagReceiptNodeToReceiptEvent,
@@ -68,6 +64,7 @@ import {
CheckNumberStatusQuery,
EditMessageRequest,
MessageButtonReply,
MessageContactVcardRequest,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
@@ -80,6 +77,7 @@ import {
SendSeenRequest,
WANumberExistResult,
} from '@waha/structures/chatting.dto';
import { toVcardV3 } from '@waha/core/vcard';
import {
ContactQuery,
ContactRequest,
@@ -149,6 +147,11 @@ import {
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
import {
isJidGroup,
isJidStatusBroadcast,
toCusFormat,
} from '@waha/core/utils/jids';
export interface WebJSConfig {
webVersion?: string;
@@ -662,6 +665,28 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return message.edit(request.text, options);
}
async sendContactVCard(request: MessageContactVcardRequest) {
const chatId = this.ensureSuffix(request.chatId);
const vcards = request.contacts.map((el) => toVcardV3(el as any));
const options = this.getMessageOptions(request);
// Single vCard: pass raw vcard text as a message body.
// WEBJS will detect BEGIN:VCARD when parseVCards=true and send as a contact card.
if (vcards.length <= 1) {
const vcard = vcards[0] || '';
return this.whatsapp.sendMessage(chatId, vcard, options);
}
// Multiple vCards: send as a single multi_vcard message using extra options.
const extra = {
type: 'multi_vcard',
vcardList: vcards.map((v) => ({ vcard: v })),
body: null,
} as any;
return this.whatsapp.sendMessage(chatId, '', { ...options, extra });
}
reply(request: MessageReplyRequest) {
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(
@@ -1239,9 +1264,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected ChannelMetadataToChannel(metadata: any): Channel {
let role = metadata.membershipType.toUpperCase();
let role = metadata.membershipType?.toUpperCase();
if (role === 'VIEWER') {
role = ChannelRole.GUEST;
} else if (!role) {
role = ChannelRole.GUEST;
}
return {
id: metadata.id,
+4 -2
View File
@@ -62,7 +62,10 @@ export class MediaManager implements IMediaManager {
return null;
}
const extension = mime.extension(mimetype);
let extension = mime.extension(mimetype);
if (mimetype == 'application/was' && !extension) {
extension = 'zip';
}
const mediaData: MediaData = {
session: session,
message: {
@@ -125,7 +128,6 @@ export class MediaManager implements IMediaManager {
try {
media.filename = processor.getFilename(message);
media.mimetype = processor.getMimetype(message);
media.filename = processor.getFilename(message);
const data = await this.processMediaInternal(processor, message, session);
media = { ...media, ...data };
} catch (err) {
+36
View File
@@ -0,0 +1,36 @@
import { E164Parser } from './PhoneJidNormalizer';
describe('E164Parser.fromJid', () => {
it('parses basic JID to +number', () => {
expect(E164Parser.fromJid('14155552671@s.whatsapp.net')).toBe(
'+14155552671',
);
});
it('returns null for empty or missing local part', () => {
expect(E164Parser.fromJid('')).toBeNull();
expect(E164Parser.fromJid('@s.whatsapp.net')).toBeNull();
});
it('applies Brazil DDD + add-9 rule when local has 8 digits and not starting with 9', () => {
// +55 <DDD:2> <local:8> and local does not start with 9
expect(E164Parser.fromJid('553188888888@s.whatsapp.net')).toBe(
'+5531988888888',
);
});
it('does not add 9 when local already starts with 9 (Brazil)', () => {
expect(E164Parser.fromJid('553198888888@s.whatsapp.net')).toBe(
'+553198888888',
);
expect(E164Parser.fromJid('553199999999@s.whatsapp.net')).toBe(
'+553199999999',
);
});
it('leaves non-Brazil numbers unchanged', () => {
expect(E164Parser.fromJid('447911123456@s.whatsapp.net')).toBe(
'+447911123456',
);
});
});
+66
View File
@@ -0,0 +1,66 @@
type RewriteRule = {
name: string;
re: RegExp;
replace: string;
};
/**
* Convert JID to Phone Number if possible
* Applies some formatting rules
*/
export class PhoneJidNormalizer {
constructor(private rules: RewriteRule[] = []) {}
/**
* jid like "553188888888@s.whatsapp.net" → "+553188888888"
*/
private parseFromJid(jid: string): string | null {
if (!jid) {
return null;
}
const local = jid.split('@', 1)[0] ?? '';
if (!local) {
return null;
}
return `+${local}`;
}
/**
* Apply rewrite rules
*/
private rewrite(number: string): string {
for (const rule of this.rules) {
if (rule.re.test(number)) {
number = number.replace(rule.re, rule.replace);
return number;
}
}
return number;
}
/**
* Converts a JID (Jabber ID) into an E.164 formatted phone number string.
* Applies rules if any
*/
fromJid(jid: string): string | null {
let number = this.parseFromJid(jid);
if (!number) {
return null;
}
number = this.rewrite(number);
return number;
}
}
const RULES = [
// Brazil - add 9 before Direct Distance Dialing
// +55 <DDD:2> <local:8>, not already starting with 9
// +553188888888 => +5531988888888
{
name: 'br-add-9-after-ddd',
re: /^\+55(\d{2})(?!9)(\d{8})$/,
replace: '+55$19$2',
},
];
export const E164Parser = new PhoneJidNormalizer(RULES);
+2 -2
View File
@@ -1,6 +1,6 @@
import { isJidGroup, WAMessageKey } from '@adiwajshing/baileys';
import type { WAMessageKey } from '@adiwajshing/baileys';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { toJID } from '@waha/core/utils/jids';
import { isJidGroup, toJID } from '@waha/core/utils/jids';
import {
GetChatMessagesFilter,
GetChatMessagesQuery,
+1 -1
View File
@@ -1,4 +1,4 @@
import { WAMessageKey } from '@adiwajshing/baileys';
import type { WAMessageKey } from '@adiwajshing/baileys';
import { toJID } from '@waha/core/utils/jids';
/**
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more