diff --git a/README.md b/README.md index 72279386..78ff9898 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ - Block/unblock contacts - Get contact info - Get profile pictures +- Get presence (online\offline\typing) status # Quick start diff --git a/docs/site/content/en/docs/how-to/deploy/index.md b/docs/site/content/en/docs/how-to/deploy/index.md index 9b537637..3946db51 100644 --- a/docs/site/content/en/docs/how-to/deploy/index.md +++ b/docs/site/content/en/docs/how-to/deploy/index.md @@ -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. diff --git a/docs/site/content/en/docs/how-to/presence/index.md b/docs/site/content/en/docs/how-to/presence/index.md new file mode 100644 index 00000000..aa1f08b9 --- /dev/null +++ b/docs/site/content/en/docs/how-to/presence/index.md @@ -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" +} +``` diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index 4485f4cb..80a0f9de 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -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" >}}) diff --git a/docs/site/content/en/docs/overview/introduction.md b/docs/site/content/en/docs/overview/introduction.md index b27ece45..99584945 100644 --- a/docs/site/content/en/docs/overview/introduction.md +++ b/docs/site/content/en/docs/overview/introduction.md @@ -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 diff --git a/src/api/chatting.controller.ts b/src/api/chatting.controller.ts index c0c1df39..a7f8ee64 100644 --- a/src/api/chatting.controller.ts +++ b/src/api/chatting.controller.ts @@ -4,6 +4,7 @@ import { ChatRequest, CheckNumberStatusQuery, GetMessageQuery, + GetPresenceQuery, MessageContactVcardRequest, MessageFileRequest, MessageImageRequest, diff --git a/src/api/presence.controller.ts b/src/api/presence.controller.ts new file mode 100644 index 00000000..042c7a80 --- /dev/null +++ b/src/api/presence.controller.ts @@ -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 { + 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 { + 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 { + return session.subscribePresence(chatId); + } +} diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index 84d2e51e..547fa562 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -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 { + throw new NotImplementedByEngineError(); + } + + public getPresence(id: string): Promise { + throw new NotImplementedByEngineError(); + } + + public subscribePresence(id: string): Promise { + throw new NotImplementedByEngineError(); + } + /** * END - Methods for API */ diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index ba784cae..d4b2d82a 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -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, ]; diff --git a/src/core/session.noweb.core.ts b/src/core/session.noweb.core.ts index e0cf44f4..90ef5edd 100644 --- a/src/core/session.noweb.core.ts +++ b/src/core/session.noweb.core.ts @@ -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 { + 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 { + 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 { + 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; diff --git a/src/core/swagger.module.core.ts b/src/core/swagger.module.core.ts index 8de7e1ac..aa53f463 100644 --- a/src/core/swagger.module.core.ts +++ b/src/core/swagger.module.core.ts @@ -23,6 +23,10 @@ export class SwaggerModuleCore { 'E.g: \`12312312310\` OR \`12312312310@c.us\`
`, ) .addTag('groups', `Groups methods.
`) + .addTag( + 'presence', + `Presence information. Available in NOWEB engine only.
`, + ) .addTag('other', 'Other methods') .addApiKey({ type: 'apiKey', diff --git a/src/structures/chatting.dto.ts b/src/structures/chatting.dto.ts index e05fd662..7645d609 100644 --- a/src/structures/chatting.dto.ts +++ b/src/structures/chatting.dto.ts @@ -29,6 +29,8 @@ export class GetMessageQuery extends ChatQuery { limit: number; } +export class GetPresenceQuery extends ChatQuery {} + /** * Requests */ diff --git a/src/structures/enums.dto.ts b/src/structures/enums.dto.ts index 558598f8..ec8475f1 100644 --- a/src/structures/enums.dto.ts +++ b/src/structures/enums.dto.ts @@ -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', +} diff --git a/src/structures/presence.dto.ts b/src/structures/presence.dto.ts new file mode 100644 index 00000000..a29ecac7 --- /dev/null +++ b/src/structures/presence.dto.ts @@ -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[]; +}