[core][noweb] Add presence endpoints and webhook

This commit is contained in:
allburov committed 2023-06-14 12:27:39 +07:00
1 parent 642079ab1f
commit 374199905d
14 files changed
+290 -1

No files matched your search

+1
View File
@@ -75,6 +75,7 @@
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
# Quick start
@@ -6,7 +6,7 @@ date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: [ ]
weight: 801
weight: 899
---
This page provides useful information to assist you in deploying the project.
@@ -0,0 +1,107 @@
---
title: "Presence"
description: "Presence"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: [ ]
weight: 801
---
You can get presence information (online, offline with last seen, typing status) for a contact if they share their
presence information.
{{< alert icon="👉" text="Presence endpoints and webhook is available only in **NOWEB** engine." />}}
[Read more about engines to choose right for you ->]({{< relref "/docs/how-to/engines" >}})
Here's few notes about fields:
- `chatId` - either contact id (`213213213@c.us`) or group chat id (`1111111111111@g.us`).
- `lastSeen` - contains Unix timestamps indicating when a participant was last online
- `lastKnownPresence` - contains the last known presence status, which can be
`offline`, `online`, `typing`, `recording`, or `paused`
## Endpoints
### Get all presence
You can get all presence information available for a session by calling `GET /api/{session}/presence/`.
It returns both groups' and personal chats' presence information.
```json
[
{
"id": "2132132130@c.us",
"presences": [
{
"participant": "2132132130@c.us",
"lastKnownPresence": "offline",
"lastSeen": 1686719326
}
]
},
{
"id": "11111111111111111111111@g.us",
"presences": [
{
"participant": "11111111111111111111111@g.us",
"lastKnownPresence": "online",
"lastSeen": null
},
{
"participant": "2132132130@c.us",
"lastKnownPresence": "offline",
"lastSeen": 1686719326
}
]
}
]
```
### Get chat presence
To get presence information for a single chat - call `GET /api/{session}/presence/{chatId}`.
For a group, you'll get participants' statuses.
```json
{
"id": "2132132130@c.us",
"presences": [
{
"participant": "2132132130@c.us",
"lastKnownPresence": "online",
"lastSeen": null
}
]
}
```
### Subscribe to presence
You can subscribe to presence information by calling `POST /api/{session}/presence/{chatId}/subscribe` request
(no body required).
You can get later presence information for the chat with above `GET` endpoints or by listening to `presence.update`
webhook.
## Webhook
You can subscribe to `presence.update` webhook event to get the most recent presence information.
```json
{
"event": "presence.update",
"session": "default",
"payload": {
"id": "111111111111111111@g.us",
"presences": [
{
"participant": "11111111111@c.us",
"lastKnownPresence": "typing",
"lastSeen": null
}
]
},
"engine": "NOWEB"
}
```
@@ -16,6 +16,8 @@ toc: true
July 2023
- Added [proxy configuration](https://waha.devlike.pro/docs/how-to/config/#proxy) with supporting proxy authentication.
Thanks **puntolino** for the contribution!
- Added [presence information](https://waha.devlike.pro/docs/how-to/presence) - now you can get online status for
a contact by calling endpoints or receiving a webhook event!
- Now you can mention contact in groups by settings `mentions` field in `POST /api/sendText`
[read more about it in Send Messages ->]({{< relref "/docs/how-to/send-messages" >}})
@@ -72,6 +72,7 @@ Here's the available features:
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
### Sessions
- Multi Device
+1
View File
@@ -4,6 +4,7 @@ import {
ChatRequest,
CheckNumberStatusQuery,
GetMessageQuery,
GetPresenceQuery,
MessageContactVcardRequest,
MessageFileRequest,
MessageImageRequest,
+47
View File
@@ -0,0 +1,47 @@
import { Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { SessionApiParam, SessionParam } from './helpers';
import { WAHAChatPresences } from '../structures/presence.dto';
@ApiSecurity('api_key')
@Controller('api/:session/presence')
@ApiTags('presence')
export class PresenceController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all subscribed presence information.' })
getPresenceAll(
@SessionParam session: WhatsappSession,
): Promise<WAHAChatPresences[]> {
return session.getPresences();
}
@Get(':chatId')
@SessionApiParam
@ApiOperation({
summary:
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
})
getPresence(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<WAHAChatPresences> {
return session.getPresence(chatId);
}
@Post(':chatId/subscribe')
@SessionApiParam
@ApiOperation({
summary: 'Subscribe to presence events for the chat.',
})
subscribe(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<void> {
return session.subscribePresence(chatId);
}
}
+13
View File
@@ -32,6 +32,7 @@ import * as fs from 'fs';
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
import { EventEmitter } from 'events';
import { WAHAChatPresences } from '../../structures/presence.dto';
export function getBrowserExecutablePath() {
if (fs.existsSync(CHROME_PATH)) {
@@ -256,6 +257,18 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getPresences(): Promise<WAHAChatPresences[]> {
throw new NotImplementedByEngineError();
}
public getPresence(id: string): Promise<WAHAChatPresences> {
throw new NotImplementedByEngineError();
}
public subscribePresence(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
/**
* END - Methods for API
*/
+2
View File
@@ -11,6 +11,7 @@ import { PassportModule } from '@nestjs/passport';
import { SessionManager } from './abc/manager.abc';
import { SessionManagerCore } from './manager.core';
import { GroupsController } from '../api/groups.controller';
import { PresenceController } from '../api/presence.controller';
export const IMPORTS = [
ConfigModule.forRoot({
@@ -36,6 +37,7 @@ export const CONTROLLERS = [
ChattingController,
ContactsController,
GroupsController,
PresenceController,
ScreenshotController,
VersionController,
];
+77
View File
@@ -1,6 +1,7 @@
import {
SECOND,
WAEvents,
WAHAPresenceStatus,
WhatsappEngine,
WhatsappStatus,
} from '../structures/enums.dto';
@@ -8,6 +9,7 @@ import makeWASocket, {
DisconnectReason,
isJidGroup,
makeInMemoryStore,
PresenceData,
useMultiFileAuthState,
} from '@adiwajshing/baileys';
@@ -49,6 +51,11 @@ import { Message } from 'whatsapp-web.js';
import * as fs from 'fs';
import { Agent } from 'https';
import { createAgentProxy } from './helpers.proxy';
import {
WAHAPresenceData,
WAHAChatPresences,
} from '../structures/presence.dto';
import * as lodash from 'lodash';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
@@ -60,6 +67,15 @@ export const BaileysEvents = {
CREDS_UPDATE: 'creds.update',
MESSAGES_UPSERT: 'messages.upsert',
GROUPS_UPSERT: 'groups.upsert',
PRESENCE_UPDATE: 'presence.update',
};
const PresenceStatuses = {
unavailable: WAHAPresenceStatus.OFFLINE,
available: WAHAPresenceStatus.ONLINE,
composing: WAHAPresenceStatus.TYPING,
recording: WAHAPresenceStatus.RECORDING,
paused: WAHAPresenceStatus.PAUSED,
};
export class WhatsappSessionNoWebCore extends WhatsappSession {
@@ -114,6 +130,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.store.bind(this.sock.ev);
}
resubscribeToKnownPresences() {
for (const jid in this.store.presences) {
this.sock.presenceSubscribe(jid);
}
}
async buildClient() {
this.sock = await this.makeSocket();
this.connectStore();
@@ -122,6 +144,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (connection === 'connecting') {
this.qr.save('');
this.status = WhatsappStatus.WORKING;
this.resubscribeToKnownPresences();
return;
} else if (connection === 'close') {
const shouldReconnect =
@@ -388,6 +411,32 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.groupParticipantsUpdate(id, participants, 'demote');
}
public async getPresences(): Promise<WAHAChatPresences[]> {
const result: WAHAChatPresences[] = [];
for (const remoteJid in this.store.presences) {
const storedPresences = this.store.presences[remoteJid];
result.push(this.toWahaPresences(remoteJid, storedPresences));
}
return result;
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
await this.subscribePresence(chatId);
const remoteJid = toJID(chatId);
const storedPresences = this.store.presences[remoteJid];
return this.toWahaPresences(remoteJid, storedPresences || []);
}
public async subscribePresence(chatId: string): Promise<void> {
const remoteJid = toJID(chatId);
if (this.store.presences[remoteJid]) {
return;
}
// Have no info - subscribe to listen for events
await this.sock.presenceSubscribe(remoteJid);
return;
}
/**
* END - Methods for API
*/
@@ -405,6 +454,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler);
} else if (event === WAEvents.GROUP_JOIN) {
return this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler);
} else if (event === WAEvents.PRESENCE_UPDATE) {
return this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) =>
handler(this.toWahaPresences(data.id, data.presences)),
);
} else {
throw new NotImplementedByEngineError(
`Engine does not support webhook event: ${event}`,
@@ -465,6 +518,30 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
});
}
private toWahaPresences(
remoteJid,
storedPresences: PresenceData[],
): WAHAChatPresences {
const presences: WAHAPresenceData[] = [];
for (const participant in storedPresences) {
const data: PresenceData = storedPresences[participant];
const lastKnownPresence = lodash.get(
PresenceStatuses,
data.lastKnownPresence,
data.lastKnownPresence,
);
const presence: WAHAPresenceData = {
participant: toCusFormat(participant),
// @ts-ignore
lastKnownPresence: lastKnownPresence,
lastSeen: data.lastSeen || null,
};
presences.push(presence);
}
const chatId = toCusFormat(remoteJid);
return { id: chatId, presences: presences };
}
protected async downloadMedia(message: Message) {
if (!message.hasMedia) {
return message;
+4
View File
@@ -23,6 +23,10 @@ export class SwaggerModuleCore {
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
)
.addTag('groups', `Groups methods.<br>`)
.addTag(
'presence',
`Presence information. Available in <b>NOWEB</b> engine only.<br>`,
)
.addTag('other', 'Other methods')
.addApiKey({
type: 'apiKey',
+2
View File
@@ -29,6 +29,8 @@ export class GetMessageQuery extends ChatQuery {
limit: number;
}
export class GetPresenceQuery extends ChatQuery {}
/**
* Requests
*/
+9
View File
@@ -7,6 +7,7 @@ export enum WAEvents {
STATE_CHANGE = 'state.change',
GROUP_JOIN = 'group.join',
GROUP_LEAVE = 'group.leave',
PRESENCE_UPDATE = 'presence.update',
}
export enum WhatsappStatus {
@@ -22,3 +23,11 @@ export enum WhatsappEngine {
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
}
export enum WAHAPresenceStatus {
OFFLINE = 'offline',
ONLINE = 'online',
TYPING = 'typing',
RECORDING = 'recording',
PAUSED = 'paused',
}
+23
View File
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAPresenceStatus } from './enums.dto';
export class WAHAPresenceData {
@ApiProperty({
description: 'Chat ID - participant or contact id',
example: '11111111111@c.us',
})
participant: string;
lastKnownPresence: WAHAPresenceStatus;
@ApiProperty({
example: 1686568773,
})
lastSeen?: number;
}
export class WAHAChatPresences {
@ApiProperty({
description: 'Chat ID - either group id or contact id',
example: '11111111111@c.us',
})
id: string;
presences: WAHAPresenceData[];
}