mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
feat: Integrate Wavoip for voice call functionality
- Added Wavoip integration to support voice calls within the application. - Introduced `wavoipToken` in various DTOs and models to manage authentication. - Updated `ChannelStartupService` to handle Wavoip settings and events. - Enhanced `BaileysStartupService` to utilize Wavoip for call signaling. - Updated CHANGELOG.md to version 1.8.7.
This commit is contained in:
parent
8e65526ce9
commit
5043ce8405
@ -1,4 +1,10 @@
|
||||
# 1.8.6 (develop)
|
||||
# 1.8.7
|
||||
|
||||
### Features
|
||||
|
||||
* Wavoip integration
|
||||
|
||||
# 1.8.6
|
||||
|
||||
### Features
|
||||
|
||||
|
@ -84,6 +84,7 @@
|
||||
"redis": "^4.6.5",
|
||||
"sharp": "^0.32.2",
|
||||
"socket.io": "^4.7.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socks-proxy-agent": "^8.0.1",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
|
@ -78,6 +78,7 @@ export class InstanceController {
|
||||
read_messages,
|
||||
read_status,
|
||||
sync_full_history,
|
||||
wavoipToken,
|
||||
websocket_enabled,
|
||||
websocket_events,
|
||||
rabbitmq_enabled,
|
||||
@ -401,6 +402,7 @@ export class InstanceController {
|
||||
read_messages: read_messages || false,
|
||||
read_status: read_status || false,
|
||||
sync_full_history: sync_full_history ?? false,
|
||||
wavoipToken: wavoipToken ?? '',
|
||||
};
|
||||
|
||||
this.logger.verbose('settings: ' + JSON.stringify(settings));
|
||||
|
@ -21,6 +21,7 @@ export class InstanceDto {
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
wavoipToken?: string;
|
||||
chatwoot_account_id?: string;
|
||||
chatwoot_token?: string;
|
||||
chatwoot_url?: string;
|
||||
|
@ -6,4 +6,5 @@ export class SettingsDto {
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
wavoipToken?: string;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export class SettingsRaw {
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
wavoipToken?: string;
|
||||
}
|
||||
|
||||
const settingsSchema = new Schema<SettingsRaw>({
|
||||
@ -22,6 +23,7 @@ const settingsSchema = new Schema<SettingsRaw>({
|
||||
read_messages: { type: Boolean, required: true },
|
||||
read_status: { type: Boolean, required: true },
|
||||
sync_full_history: { type: Boolean, required: true },
|
||||
wavoipToken: { type: String, required: true },
|
||||
});
|
||||
|
||||
export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings');
|
||||
|
@ -181,6 +181,9 @@ export class ChannelStartupService {
|
||||
this.localSettings.sync_full_history = data?.sync_full_history;
|
||||
this.logger.verbose(`Settings sync_full_history: ${this.localSettings.sync_full_history}`);
|
||||
|
||||
this.localSettings.wavoipToken = data?.wavoipToken;
|
||||
this.logger.verbose(`Settings wavoipToken: ${this.localSettings.wavoipToken}`);
|
||||
|
||||
this.logger.verbose('Settings loaded');
|
||||
}
|
||||
|
||||
@ -194,6 +197,7 @@ export class ChannelStartupService {
|
||||
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
|
||||
this.logger.verbose(`Settings read_status: ${data.read_status}`);
|
||||
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
|
||||
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
|
||||
Object.assign(this.localSettings, data);
|
||||
this.logger.verbose('Settings set');
|
||||
}
|
||||
@ -214,6 +218,7 @@ export class ChannelStartupService {
|
||||
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
|
||||
this.logger.verbose(`Settings read_status: ${data.read_status}`);
|
||||
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
|
||||
this.logger.verbose(`Settings wavoipToken: ${data.wavoipToken}`);
|
||||
return {
|
||||
reject_call: data.reject_call,
|
||||
msg_call: data.msg_call,
|
||||
@ -222,6 +227,7 @@ export class ChannelStartupService {
|
||||
read_messages: data.read_messages,
|
||||
read_status: data.read_status,
|
||||
sync_full_history: data.sync_full_history,
|
||||
wavoipToken: data.wavoipToken,
|
||||
};
|
||||
}
|
||||
|
||||
@ -719,7 +725,12 @@ export class ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) {
|
||||
public async sendDataWebhook<T = any>(
|
||||
event: Events,
|
||||
data: T,
|
||||
local = true,
|
||||
integration = ['websocket', 'rabbitmq', 'sqs', 'webhook'],
|
||||
) {
|
||||
const webhookGlobal = this.configService.get<Webhook>('WEBHOOK');
|
||||
const webhookLocal = this.localWebhook.events;
|
||||
const websocketLocal = this.localWebsocket.events;
|
||||
@ -739,7 +750,7 @@ export class ChannelStartupService {
|
||||
const tokenStore = await this.repository.auth.find(this.instanceName);
|
||||
const instanceApikey = tokenStore?.apikey || 'Apikey not found';
|
||||
|
||||
if (rabbitmqEnabled) {
|
||||
if (rabbitmqEnabled && integration.includes('rabbitmq')) {
|
||||
const amqp = getAMQP();
|
||||
if (this.localRabbitmq.enabled && amqp) {
|
||||
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
|
||||
@ -884,7 +895,7 @@ export class ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.localSqs.enabled) {
|
||||
if (this.localSqs.enabled && integration.includes('sqs')) {
|
||||
const sqs = getSQS();
|
||||
|
||||
if (sqs) {
|
||||
@ -954,7 +965,7 @@ export class ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
|
||||
if (this.configService.get<Websocket>('WEBSOCKET')?.ENABLED && integration.includes('websocket')) {
|
||||
this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name);
|
||||
const io = getIO();
|
||||
|
||||
@ -1028,7 +1039,7 @@ export class ChannelStartupService {
|
||||
const globalApiKey = this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
|
||||
|
||||
if (local) {
|
||||
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
|
||||
if (Array.isArray(webhookLocal) && webhookLocal.includes(we) && integration.includes('webhook')) {
|
||||
this.logger.verbose('Sending data to webhook local');
|
||||
let baseURL: string;
|
||||
|
||||
@ -1096,7 +1107,7 @@ export class ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (webhookGlobal.GLOBAL?.ENABLED) {
|
||||
if (webhookGlobal.GLOBAL?.ENABLED && integration.includes('webhook')) {
|
||||
if (webhookGlobal.EVENTS[we]) {
|
||||
this.logger.verbose('Sending data to webhook global');
|
||||
const globalWebhook = this.configService.get<Webhook>('WEBHOOK').GLOBAL;
|
||||
|
78
src/api/services/channels/voiceCalls/transport.type.ts
Normal file
78
src/api/services/channels/voiceCalls/transport.type.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
|
||||
|
||||
export interface ServerToClientEvents {
|
||||
withAck: (d: string, callback: (e: number) => void) => void;
|
||||
onWhatsApp: onWhatsAppType;
|
||||
profilePictureUrl: ProfilePictureUrlType;
|
||||
assertSessions: AssertSessionsType;
|
||||
createParticipantNodes: CreateParticipantNodesType;
|
||||
getUSyncDevices: GetUSyncDevicesType;
|
||||
generateMessageTag: GenerateMessageTagType;
|
||||
sendNode: SendNodeType;
|
||||
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
|
||||
}
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
init: (
|
||||
me: Contact | undefined,
|
||||
account: proto.IADVSignedDeviceIdentity | undefined,
|
||||
status: WAConnectionState,
|
||||
) => void;
|
||||
'CB:call': (packet: any) => void;
|
||||
'CB:ack,class:call': (packet: any) => void;
|
||||
'connection.update:status': (
|
||||
me: Contact | undefined,
|
||||
account: proto.IADVSignedDeviceIdentity | undefined,
|
||||
status: WAConnectionState,
|
||||
) => void;
|
||||
'connection.update:qr': (qr: string) => void;
|
||||
}
|
||||
|
||||
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
|
||||
export type onWhatsAppCallback = (
|
||||
response: {
|
||||
exists: boolean;
|
||||
jid: string;
|
||||
}[],
|
||||
) => void;
|
||||
|
||||
export type ProfilePictureUrlType = (
|
||||
jid: string,
|
||||
type: 'image' | 'preview',
|
||||
timeoutMs: number | undefined,
|
||||
callback: ProfilePictureUrlCallback,
|
||||
) => void;
|
||||
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
|
||||
|
||||
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
|
||||
export type AssertSessionsCallback = (response: boolean) => void;
|
||||
|
||||
export type CreateParticipantNodesType = (
|
||||
jids: string[],
|
||||
message: any,
|
||||
extraAttrs: any,
|
||||
callback: CreateParticipantNodesCallback,
|
||||
) => void;
|
||||
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
|
||||
|
||||
export type GetUSyncDevicesType = (
|
||||
jids: string[],
|
||||
useCache: boolean,
|
||||
ignoreZeroDevices: boolean,
|
||||
callback: GetUSyncDevicesTypeCallback,
|
||||
) => void;
|
||||
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
|
||||
|
||||
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
|
||||
export type GenerateMessageTagTypeCallback = (response: string) => void;
|
||||
|
||||
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
|
||||
export type SendNodeTypeCallback = (response: boolean) => void;
|
||||
|
||||
export type SignalRepositoryDecryptMessageType = (
|
||||
jid: string,
|
||||
type: 'pkmsg' | 'msg',
|
||||
ciphertext: Buffer,
|
||||
callback: SignalRepositoryDecryptMessageCallback,
|
||||
) => void;
|
||||
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;
|
181
src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts
Normal file
181
src/api/services/channels/voiceCalls/useVoiceCallsBaileys.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
|
||||
|
||||
let baileys_connection_state: WAConnectionState = 'close';
|
||||
|
||||
export const useVoiceCallsBaileys = async (
|
||||
wavoip_token: string,
|
||||
baileys_sock: WASocket,
|
||||
status?: WAConnectionState,
|
||||
logger?: boolean,
|
||||
) => {
|
||||
baileys_connection_state = status ?? 'close';
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
|
||||
transports: ['websocket'],
|
||||
path: `/${wavoip_token}/websocket`,
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (logger) console.log('[*] - Wavoip connected', socket.id);
|
||||
|
||||
socket.emit(
|
||||
'init',
|
||||
baileys_sock.authState.creds.me,
|
||||
baileys_sock.authState.creds.account,
|
||||
baileys_connection_state,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
if (logger) console.log('[*] - Wavoip disconnect');
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
if (socket.active) {
|
||||
if (logger)
|
||||
console.log(
|
||||
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
if (logger) console.log('[*] - Wavoip connection error', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('onWhatsApp', async (jid, callback) => {
|
||||
try {
|
||||
const response: any = await baileys_sock.onWhatsApp(jid);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call onWhatsApp function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('assertSessions', async (jids, force, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.assertSessions(jids, force);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call assertSessions function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call assertSessions function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
|
||||
|
||||
callback(response, true);
|
||||
|
||||
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('generateMessageTag', async (callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.generateMessageTag();
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call generateMessageTag function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call generateMessageTag function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('sendNode', async (stanza, callback) => {
|
||||
try {
|
||||
console.log('sendNode', JSON.stringify(stanza));
|
||||
const response = await baileys_sock.sendNode(stanza);
|
||||
|
||||
callback(true);
|
||||
|
||||
if (logger) console.log('[*] Success on call sendNode function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call sendNode function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.signalRepository.decryptMessage({
|
||||
jid: jid,
|
||||
type: type,
|
||||
ciphertext: ciphertext,
|
||||
});
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
|
||||
}
|
||||
});
|
||||
|
||||
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
|
||||
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
|
||||
const { connection } = update;
|
||||
|
||||
if (connection) {
|
||||
baileys_connection_state = connection;
|
||||
socket
|
||||
.timeout(1000)
|
||||
.emit(
|
||||
'connection.update:status',
|
||||
baileys_sock.authState.creds.me,
|
||||
baileys_sock.authState.creds.account,
|
||||
connection,
|
||||
);
|
||||
}
|
||||
|
||||
if (update.qr) {
|
||||
socket.timeout(1000).emit('connection.update:qr', update.qr);
|
||||
}
|
||||
});
|
||||
|
||||
baileys_sock.ws.on('CB:call', (packet) => {
|
||||
if (logger) console.log('[*] Signling received');
|
||||
socket.volatile.timeout(1000).emit('CB:call', packet);
|
||||
});
|
||||
|
||||
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
|
||||
if (logger) console.log('[*] Signling ack received');
|
||||
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
|
||||
});
|
||||
|
||||
return socket;
|
||||
};
|
@ -131,6 +131,7 @@ import { waMonitor } from '../../server.module';
|
||||
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
|
||||
import { CacheService } from './../cache.service';
|
||||
import { ChannelStartupService } from './../channel.service';
|
||||
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
||||
|
||||
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
|
||||
|
||||
@ -669,10 +670,32 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
this.logger.verbose('Socket created');
|
||||
|
||||
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
|
||||
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
|
||||
}
|
||||
|
||||
this.eventHandler();
|
||||
|
||||
this.logger.verbose('Socket event handler initialized');
|
||||
|
||||
this.client.ws.on('CB:call', (packet) => {
|
||||
console.log('CB:call', packet);
|
||||
const payload = {
|
||||
event: 'CB:call',
|
||||
packet: packet,
|
||||
};
|
||||
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
|
||||
});
|
||||
|
||||
this.client.ws.on('CB:ack,class:call', (packet) => {
|
||||
console.log('CB:ack,class:call', packet);
|
||||
const payload = {
|
||||
event: 'CB:ack,class:call',
|
||||
packet: packet,
|
||||
};
|
||||
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
|
||||
});
|
||||
|
||||
this.phoneNumber = number;
|
||||
|
||||
return this.client;
|
||||
@ -1030,7 +1053,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
await this.contactHandle['contacts.upsert'](
|
||||
contacts
|
||||
.filter((c) => !!c.notify ?? !!c.name)
|
||||
.filter((c) => !!c.notify || !!c.name)
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name ?? c.notify,
|
||||
@ -1117,13 +1140,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const contentMsg = received?.message[getContentType(received.message)] as any;
|
||||
|
||||
if (
|
||||
(
|
||||
this.localWebhook.webhook_base64 === true ||
|
||||
(
|
||||
this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
|
||||
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true
|
||||
)
|
||||
) &&
|
||||
(this.localWebhook.webhook_base64 === true ||
|
||||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
|
||||
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
|
||||
isMedia
|
||||
) {
|
||||
const buffer = await downloadMediaMessage(
|
||||
@ -1975,13 +1994,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
console.log('isMedia', isMedia);
|
||||
|
||||
if (
|
||||
(
|
||||
this.localWebhook.webhook_base64 === true ||
|
||||
(
|
||||
this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
|
||||
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true
|
||||
)
|
||||
) &&
|
||||
(this.localWebhook.webhook_base64 === true ||
|
||||
(this.configService.get<Websocket>('WEBSOCKET').GLOBAL_EVENTS === true &&
|
||||
this.configService.get<Websocket>('WEBSOCKET').ENABLED === true)) &&
|
||||
isMedia
|
||||
) {
|
||||
const buffer = await downloadMediaMessage(
|
||||
|
@ -83,6 +83,7 @@ export declare namespace wa {
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
wavoipToken?: string;
|
||||
};
|
||||
|
||||
export type LocalWebsocket = {
|
||||
|
@ -1002,9 +1002,26 @@ export const settingsSchema: JSONSchema7 = {
|
||||
read_messages: { type: 'boolean', enum: [true, false] },
|
||||
read_status: { type: 'boolean', enum: [true, false] },
|
||||
sync_full_history: { type: 'boolean', enum: [true, false] },
|
||||
wavoipToken: { type: 'string' },
|
||||
},
|
||||
required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'],
|
||||
...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status', 'sync_full_history'),
|
||||
required: [
|
||||
'reject_call',
|
||||
'groups_ignore',
|
||||
'always_online',
|
||||
'read_messages',
|
||||
'read_status',
|
||||
'sync_full_history',
|
||||
'wavoipToken',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'reject_call',
|
||||
'groups_ignore',
|
||||
'always_online',
|
||||
'read_messages',
|
||||
'read_status',
|
||||
'sync_full_history',
|
||||
'wavoipToken',
|
||||
),
|
||||
};
|
||||
|
||||
export const websocketSchema: JSONSchema7 = {
|
||||
|
Loading…
Reference in New Issue
Block a user