diff --git a/README.md b/README.md index 16ebdddb..2584c4c0 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ * [Sessions](#sessions) * [Security](#security) * [Updates](#updates) - * [Groups [soon]](#groups-soon) - * [Contacts [soon]](#contacts-soon) + * [Groups](#groups) + * [Contacts](#contacts) - [Quick start](#quick-start) * [Requirements](#requirements) * [Send your first message](#send-your-first-message) @@ -64,22 +64,19 @@ - One month delay in bug fixes and updates. -## Groups [soon] -- Join groups by invite +## Groups +- Create a group - Get invite for group - Modify group info (subject, description) -- Modify group settings (send messages, edit info) - Add group participants - Kick group participants - Promote/demote group participants -## Contacts [soon] -- Mention users +## Contacts - Mute/unmute chats - Block/unblock contacts - Get contact info - Get profile pictures -- Set user status message # Quick start diff --git a/src/api/groups.controller.ts b/src/api/groups.controller.ts new file mode 100644 index 00000000..e27075e0 --- /dev/null +++ b/src/api/groups.controller.ts @@ -0,0 +1,165 @@ +import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common'; +import {ApiOperation, ApiSecurity, ApiTags} from "@nestjs/swagger"; +import {SessionManager} from "../core/abc/manager.abc"; +import {CreateGroupRequest, DescriptionRequest, ParticipantsRequest, SubjectRequest} from "../structures/groups.dto"; +import {WhatsappSession} from "../core/abc/session.abc"; +import {SessionApiParam, SessionParam} from "./helpers"; + +@ApiSecurity('api_key') +@Controller('api/:session/groups') +@ApiTags('groups') +export class GroupsController { + constructor(private manager: SessionManager) { + } + + @Post('') + @SessionApiParam + @ApiOperation({summary: "Create a new group."}) + createGroup( + @SessionParam session: WhatsappSession, + @Body() request: CreateGroupRequest, + ) { + return session.createGroup(request) + } + + @Get('') + @SessionApiParam + @ApiOperation({summary: "Get all groups."}) + getGroups( + @SessionParam session: WhatsappSession, + ) { + return session.getGroups() + } + + @Get(':id') + @SessionApiParam + @ApiOperation({summary: "Get the group."}) + getGroup( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ) { + return session.getGroup(id) + } + + @Delete(':id') + @SessionApiParam + @ApiOperation({summary: "Delete the group."}) + deleteGroup( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ) { + return session.deleteGroup(id) + } + + @Post(':id/leave') + @SessionApiParam + @ApiOperation({summary: "Leave the group."}) + leaveGroup( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ) { + return session.leaveGroup(id) + } + + @Put(':id/description') + @ApiOperation({ + summary: "Updates the group description.\n" + + "Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n" + }) + @SessionApiParam + setDescription( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: DescriptionRequest + ) { + return session.setDescription(id, request.description) + } + + @Put(':id/subject') + @SessionApiParam + @ApiOperation({ + summary: "Updates the group subject.\n" + + "Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n" + }) + setSubject( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: SubjectRequest + ) { + return session.setSubject(id, request.subject) + } + + @Get(':id/invite-code') + @SessionApiParam + @ApiOperation({summary: "Gets the invite code for a specific group."}) + getInviteCode( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ): Promise { + return session.getInviteCode(id) + } + + @Post(':id/invite-code/revoke') + @SessionApiParam + @ApiOperation({summary: "Invalidates the current group invite code and generates a new one."}) + revokeInviteCode( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ): Promise { + return session.revokeInviteCode(id) + } + + @Get(':id/participants/') + @SessionApiParam + @ApiOperation({summary: 'Get a list of participants by in the group.'}) + getParticipants( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + ) { + return session.getParticipants(id) + } + + @Post(':id/participants/add') + @SessionApiParam + @ApiOperation({summary: 'Adds a list of participants by ID to the group.'}) + addParticipants( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: ParticipantsRequest + ) { + return session.addParticipants(id, request) + } + + @Post(':id/participants/remove') + @SessionApiParam + @ApiOperation({summary: 'Removes a list of participants by ID to the group.'}) + removeParticipants( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: ParticipantsRequest + ) { + return session.removeParticipants(id, request) + } + + @Post(':id/admin/promote') + @SessionApiParam + @ApiOperation({summary: 'Promote participants to admin users.'}) + promoteToAdmin( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: ParticipantsRequest + ) { + return session.promoteParticipantsToAdmin(id, request) + } + + @Post(':id/admin/demote') + @SessionApiParam + @ApiOperation({summary: 'Demotes participants by to regular users.'}) + demoteToAdmin( + @SessionParam session: WhatsappSession, + @Param('id') id: string, + @Body() request: ParticipantsRequest + ) { + return session.demoteParticipantsToUser(id, request) + } +} diff --git a/src/api/helpers.ts b/src/api/helpers.ts new file mode 100644 index 00000000..30d5d6d2 --- /dev/null +++ b/src/api/helpers.ts @@ -0,0 +1,35 @@ +import {ApiParam} from "@nestjs/swagger"; +import {Injectable, Param, PipeTransform} from "@nestjs/common"; +import {SessionManager} from "../core/abc/manager.abc"; +import {WhatsappSession} from "../core/abc/session.abc"; + +/** + * Get session name and return Whatsapp session back (if exists) + * use it as + @Param('session', SessionPipe) session: WhatsappSession, + */ +@Injectable() +export class SessionPipe implements PipeTransform { + constructor(private manager: SessionManager) { + } + + async transform(value: any) { + return this.manager.getSession(value) + } +} + +/** + * Decorator for a method that uses SessionPipe above + */ +export const SessionApiParam = ApiParam({ + name: 'session', + required: true, + type: "string", + description: "WhatsApp session name", +}) + +/** + * Session param + @SessionParam session: WhatsappSession, + */ +export const SessionParam = Param('session', SessionPipe) diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index bece051f..3833e41a 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -18,6 +18,7 @@ import {MediaStorage} from "./storage.abc"; import {MessageId} from "whatsapp-web.js"; import {ContactQuery, ContactRequest} from "../../structures/contacts.dto"; import {NotImplementedByEngineError} from "../exceptions"; +import {CreateGroupRequest, ParticipantsRequest} from "../../structures/groups.dto"; export abstract class WhatsappSession { public status: WhatsappStatus; @@ -132,6 +133,65 @@ export abstract class WhatsappSession { throw new NotImplementedByEngineError() } + /** + * Group methods + */ + public createGroup(request: CreateGroupRequest) { + throw new NotImplementedByEngineError() + } + + public getGroups() { + throw new NotImplementedByEngineError() + } + + public getGroup(id) { + throw new NotImplementedByEngineError() + } + + public deleteGroup(id) { + throw new NotImplementedByEngineError() + } + + public leaveGroup(id) { + throw new NotImplementedByEngineError() + } + + public setDescription(id, description) { + throw new NotImplementedByEngineError() + } + + public setSubject(id, description) { + throw new NotImplementedByEngineError() + } + + public getInviteCode(id): Promise { + throw new NotImplementedByEngineError() + } + + public revokeInviteCode(id): Promise { + throw new NotImplementedByEngineError() + } + + public getParticipants(id) { + throw new NotImplementedByEngineError() + } + + public addParticipants(id, request: ParticipantsRequest) { + throw new NotImplementedByEngineError() + } + + public removeParticipants(id, request: ParticipantsRequest) { + throw new NotImplementedByEngineError() + } + + public promoteParticipantsToAdmin(id, request: ParticipantsRequest) { + throw new NotImplementedByEngineError() + } + + public demoteParticipantsToUser(id, request: ParticipantsRequest) { + throw new NotImplementedByEngineError() + } + /** * END - Methods for API diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index d10da38e..71eef11d 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -10,6 +10,7 @@ import {VersionController} from "../api/version.controller"; import {PassportModule} from "@nestjs/passport"; import {SessionManager} from "./abc/manager.abc"; import {SessionManagerCore} from "./manager.core"; +import {GroupsController} from "../api/groups.controller"; export const IMPORTS = [ ConfigModule.forRoot({ @@ -32,6 +33,7 @@ export const CONTROLLERS = [ SessionsController, ChattingController, ContactsController, + GroupsController, ScreenshotController, VersionController, ] diff --git a/src/core/session.webjs.core.ts b/src/core/session.webjs.core.ts index 68b03740..690952fa 100644 --- a/src/core/session.webjs.core.ts +++ b/src/core/session.webjs.core.ts @@ -1,5 +1,5 @@ import {UnprocessableEntityException} from "@nestjs/common/exceptions/unprocessable-entity.exception"; -import {Buttons, Chat, Client, Contact, Events, Location, Message} from "whatsapp-web.js"; +import {Buttons, Chat, Client, Contact, Events, GroupChat, Location, Message} from "whatsapp-web.js"; import {Message as MessageInstance} from "whatsapp-web.js/src/structures" import {WAEvents, WhatsappStatus} from "../structures/enums.dto"; import {WhatsappSession} from "./abc/session.abc"; @@ -20,6 +20,7 @@ import { } from "../structures/chatting.dto"; import {AvailableInPlusVersion, NotImplementedByEngineError} from "./exceptions"; import {ContactQuery, ContactRequest} from "../structures/contacts.dto"; +import {CreateGroupRequest, ParticipantsRequest} from "../structures/groups.dto"; // eslint-disable-next-line @typescript-eslint/no-var-requires const qrcode = require('qrcode-terminal'); @@ -184,6 +185,82 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { await contact.unblock() } + /** + * Group methods + */ + public createGroup(request: CreateGroupRequest) { + const participantIds = request.participants.map(participant => participant.id) + return this.whatsapp.createGroup(request.name, participantIds) + } + + public getGroups() { + return this.whatsapp.getChats().then(chats => chats.filter(chat => chat.isGroup)) + } + + public getGroup(id) { + return this.whatsapp.getChatById(id) + } + + public async deleteGroup(id) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.delete() + } + + public async leaveGroup(id) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.leave() + } + + public async setDescription(id, description) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.setDescription(description) + } + + public async setSubject(id, subject) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.setSubject(subject) + } + + public async getInviteCode(id): Promise { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.getInviteCode() + } + + public async revokeInviteCode(id): Promise { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + await groupChat.revokeInvite() + return groupChat.getInviteCode() + } + + public async getParticipants(id) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + return groupChat.participants + } + + public async addParticipants(id, request: ParticipantsRequest) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + const participantIds = request.participants.map(participant => participant.id) + return groupChat.addParticipants(participantIds) + } + + public async removeParticipants(id, request: ParticipantsRequest) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + const participantIds = request.participants.map(participant => participant.id) + return groupChat.removeParticipants(participantIds) + } + + public async promoteParticipantsToAdmin(id, request: ParticipantsRequest) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + const participantIds = request.participants.map(participant => participant.id) + return groupChat.promoteParticipants(participantIds) + } + + public async demoteParticipantsToUser(id, request: ParticipantsRequest) { + const groupChat = await this.whatsapp.getChatById(id) as GroupChat + const participantIds = request.participants.map(participant => participant.id) + return groupChat.demoteParticipants(participantIds) + } + /** * END - Methods for API */ diff --git a/src/core/swagger.module.core.ts b/src/core/swagger.module.core.ts index f593e515..c9f65749 100644 --- a/src/core/swagger.module.core.ts +++ b/src/core/swagger.module.core.ts @@ -20,6 +20,10 @@ export class SwaggerModuleCore { Use phone number (without +) or phone number and \`@c.us\` at the end as \`contactId\`.
'E.g: \`12312312310\` OR \`12312312310@c.us\`
` ) + .addTag( + 'groups', + `Groups methods.
` + ) .addTag('other', 'Other methods') .addApiKey({ type: 'apiKey', diff --git a/src/structures/contacts.dto.ts b/src/structures/contacts.dto.ts index af418984..283f91e1 100644 --- a/src/structures/contacts.dto.ts +++ b/src/structures/contacts.dto.ts @@ -1,5 +1,5 @@ import {IsString} from "class-validator"; -import {SessionBaseRequest, SessionQuery, WHATSAPP_DEFAULT_SESSION_NAME} from "./base.dto"; +import {SessionBaseRequest, SessionQuery} from "./base.dto"; /** diff --git a/src/structures/groups.dto.ts b/src/structures/groups.dto.ts new file mode 100644 index 00000000..4f50c724 --- /dev/null +++ b/src/structures/groups.dto.ts @@ -0,0 +1,40 @@ +import {IsArray, IsString} from "class-validator"; + +/** + * Structures + */ +export class Participant { + @IsString() + id: string +} + +/** + * Queries + */ + + +/** + * Requests + */ + +export class ParticipantsRequest { + @IsArray() + participants: Array +} + +export class DescriptionRequest { + @IsString() + description: string +} +export class SubjectRequest { + @IsString() + subject: string +} + +export class CreateGroupRequest { + @IsString() + name: string + + @IsArray() + participants: Array +}