Browse Source

feat: approval requested participant group (#276)

* feat: Add group participant request management functionality

- Introduced new endpoints for handling requested participants in groups:
  - GET /group/participants/requested to retrieve requested participants.
  - POST /group/participants/requested/approve to approve participant requests.
  - POST /group/participants/requested/reject to reject participant requests.

- Added corresponding methods in the group service to manage participant requests.
- Implemented validation for new request types to ensure proper data handling.

* feat: Update API version and enhance group participant request management

- Bumped API version from 5.3.0 to 5.4.0.
- Added new endpoints for managing group participant requests:
  - GET /group/participant-requests to retrieve pending requests.
  - POST /group/participant-requests/approve to approve requests.
  - POST /group/participant-requests/reject to reject requests.
- Updated group service methods to handle new request types and responses.
- Enhanced front-end components to support requested member management.

* chore: Update dependency versions in go.mod and go.sum

- Bumped versions for several dependencies:
  - github.com/PuerkitoBio/goquery from v1.10.2 to v1.10.3
  - github.com/mattn/go-sqlite3 from v1.14.27 to v1.14.28
  - go.mau.fi/whatsmeow to a new version
  - golang.org/x/image from v0.25.0 to v0.26.0
  - golang.org/x/net from v0.38.0 to v0.39.0
  - github.com/pelletier/go-toml/v2 from v2.2.3 to v2.2.4
  - golang.org/x/crypto from v0.36.0 to v0.37.0
  - Added new indirect dependencies for petermattis/goid and updated existing ones.

* refactor: Change receiver type for ChangePushName method in userService

* refactor: Rename group participant request endpoints and streamline request handling

- Updated API endpoints for managing group participant requests:
  - Changed GET /group/participants/requested to GET /group/participant-requests
  - Changed POST /group/participants/requested/approve to POST /group/participant-requests/approve
  - Changed POST /group/participants/requested/reject to POST /group/participant-requests/reject
- Refactored front-end methods to handle the new endpoint structure and consolidate approval/rejection logic.

* feat: Add validation for empty Group ID in participant request listing

- Implemented a check to ensure Group ID is not empty in the ListParticipantRequests method.
- Returns a 400 Bad Request response with an appropriate error message if Group ID is missing.

* feat: Enhance participant request management with error handling

- Updated ManageGroupRequestParticipants method to include error handling for participant requests.
- Added validation for the Action field in participant management requests to ensure it is not empty and is one of the allowed values.

* refactor: Update message handling to use domainMessage consistently

- Removed redundant import of message package.
- Updated ReactMessage method to use domainMessage types for request and response, ensuring consistency across the service.
pull/280/head v5.6.0
Aldino Kemal 11 months ago
committed by GitHub
parent
commit
d43aa43e3d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 154
      docs/openapi.yaml
  2. 17
      readme.md
  3. 4
      src/config/settings.go
  4. 19
      src/domains/group/group.go
  5. 18
      src/go.mod
  6. 20
      src/go.sum
  7. 81
      src/internal/rest/group.go
  8. 66
      src/services/group.go
  9. 3
      src/services/message.go
  10. 3
      src/services/user.go
  11. 29
      src/validations/group_validation.go
  12. 2
      src/views/assets/app.css
  13. 134
      src/views/components/GroupList.js
  14. 4
      src/views/index.html

154
docs/openapi.yaml

@ -1,7 +1,7 @@
openapi: "3.0.0"
info:
title: WhatsApp API MultiDevice
version: 5.3.0
version: 5.4.0
description: This API is used for sending whatsapp via API
servers:
- url: http://localhost:3000
@ -18,6 +18,9 @@ tags:
description: Group setting
- name: newsletter
description: newsletter setting
security:
- basicAuth: []
paths:
/app/login:
get:
@ -1217,6 +1220,123 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/group/participant-requests:
get:
operationId: getGroupParticipantRequests
tags:
- group
summary: Get list of participant requests to join group
parameters:
- name: group_id
in: query
required: true
schema:
type: string
example: '120363024512399999@g.us'
description: The group ID to get participant requests for
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GroupParticipantRequestListResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/group/participant-requests/approve:
post:
operationId: approveGroupParticipantRequest
tags:
- group
summary: Approve participant request to join group
requestBody:
content:
application/json:
schema:
type: object
properties:
group_id:
type: string
example: '120363024512399999@g.us'
description: The group ID
participant_id:
type: string
example: '6281234567890'
description: The participant's WhatsApp ID to approve
required:
- group_id
- participant_id
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/group/participant-requests/reject:
post:
operationId: rejectGroupParticipantRequest
tags:
- group
summary: Reject participant request to join group
requestBody:
content:
application/json:
schema:
type: object
properties:
group_id:
type: string
example: '120363024512399999@g.us'
description: The group ID
participant_id:
type: string
example: '6281234567890'
description: The participant's WhatsApp ID to reject
required:
- group_id
- participant_id
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/group/leave:
post:
operationId: leaveGroup
@ -1287,6 +1407,10 @@ paths:
$ref: '#/components/schemas/ErrorInternalServer'
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas:
CreateGroupResponse:
type: object
@ -1319,6 +1443,7 @@ components:
- '6839241294719274'
ManageParticipantResponse:
type: object
additionalProperties: false
properties:
code:
type: string
@ -1329,6 +1454,8 @@ components:
results:
type: array
items:
type: object
additionalProperties: false
properties:
participant:
type: string
@ -1852,3 +1979,28 @@ components:
AddRequest:
type: string
example: null
GroupParticipantRequestListResponse:
type: object
properties:
code:
type: string
example: "SUCCESS"
message:
type: string
example: "Success getting list requested participants"
results:
type: object
properties:
data:
type: array
items:
type: object
properties:
jid:
type: string
example: "6289685024091@s.whatsapp.net"
requested_at:
type: string
format: date-time
example: "2024-10-11T21:27:29+07:00"

17
readme.md

@ -2,7 +2,7 @@
[![Patreon](https://img.shields.io/badge/Support%20on-Patreon-orange.svg)](https://www.patreon.com/c/aldinokemal)
**If you're using this tools to generate income, consider supporting its development by becoming a Patreon member!**
Your support helps ensure the library stays maintained and receives regular updates. Join our community of supporters today!
Your support helps ensure the library stays maintained and receives regular updates!
___
![release version](https://img.shields.io/github/v/release/aldinokemal/go-whatsapp-web-multidevice)
@ -50,7 +50,8 @@ Now that we support ARM64 for Linux:
## Configuration
You can configure the application using either command-line flags (shown above) or environment variables. Configuration can be set in three ways (in order of priority):
You can configure the application using either command-line flags (shown above) or environment variables. Configuration
can be set in three ways (in order of priority):
1. Command-line flags (highest priority)
2. Environment variables
@ -182,7 +183,7 @@ You can fork or edit this source code !
- Generate HTTP clients using [openapi-generator](https://openapi-generator.tech/#try).
| Feature | Menu | Method | URL |
|---------|------------------------------|--------|-------------------------------|
|---------|----------------------------------------|--------|---------------------------------------|
| ✅ | Login with Scan QR | GET | /app/login |
| ✅ | Login With Pair Code | GET | /app/login-with-code |
| ✅ | Logout | GET | /app/logout |
@ -211,7 +212,7 @@ You can fork or edit this source code !
| ✅ | Delete Message | POST | /message/:message_id/delete |
| ✅ | Edit Message | POST | /message/:message_id/update |
| ✅ | Read Message (DM) | POST | /message/:message_id/read |
| ❌ | Star Message | POST | /message/:message_id/star |
| ✅ | Star Message | POST | /message/:message_id/star |
| ✅ | Join Group With Link | POST | /group/join-with-link |
| ✅ | Leave Group | POST | /group/leave |
| ✅ | Create Group | POST | /group |
@ -219,6 +220,9 @@ You can fork or edit this source code !
| ✅ | Remove Participant in Group | POST | /group/participants/remove |
| ✅ | Promote Participant in Group | POST | /group/participants/promote |
| ✅ | Demote Participant in Group | POST | /group/participants/demote |
| ✅ | List Requested Participants in Group | POST | /group/participants/requested |
| ✅ | Approve Requested Participant in Group | POST | /group/participants/requested/approve |
| ✅ | Reject Requested Participant in Group | POST | /group/participants/requested/reject |
| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow |
```txt
@ -261,3 +265,8 @@ You can fork or edit this source code !
- Please do this if you have an error (invalid flag in pkg-config --cflags: -Xpreprocessor)
`export CGO_CFLAGS_ALLOW="-Xpreprocessor"`
## Important
- This project is unofficial and not affiliated with WhatsApp.
- Please use official WhatsApp API to avoid any issues.

4
src/config/settings.go

@ -5,7 +5,7 @@ import (
)
var (
AppVersion = "v5.5.0"
AppVersion = "v5.6.0"
AppPort = "3000"
AppDebug = false
AppOs = "AldinoKemal"
@ -19,7 +19,7 @@ var (
PathStorages = "storages"
PathChatStorage = "storages/chat.csv"
DBURI = "file:storages/whatsapp.db?_foreign_keys=off"
DBURI = "file:storages/whatsapp.db?_foreign_keys=on"
WhatsappAutoReplyMessage string
WhatsappWebhook []string

19
src/domains/group/group.go

@ -2,6 +2,8 @@ package group
import (
"context"
"time"
"go.mau.fi/whatsmeow"
)
@ -10,6 +12,8 @@ type IGroupService interface {
LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error)
CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error)
ManageParticipant(ctx context.Context, request ParticipantRequest) (result []ParticipantStatus, err error)
GetGroupRequestParticipants(ctx context.Context, request GetGroupRequestParticipantsRequest) (result []GetGroupRequestParticipantsResponse, err error)
ManageGroupRequestParticipants(ctx context.Context, request GroupRequestParticipantsRequest) (result []ParticipantStatus, err error)
}
type JoinGroupWithLinkRequest struct {
@ -36,3 +40,18 @@ type ParticipantStatus struct {
Status string `json:"status"`
Message string `json:"message"`
}
type GetGroupRequestParticipantsRequest struct {
GroupID string `json:"group_id" query:"group_id"`
}
type GetGroupRequestParticipantsResponse struct {
JID string `json:"jid"`
RequestedAt time.Time `json:"requested_at"`
}
type GroupRequestParticipantsRequest struct {
GroupID string `json:"group_id" form:"group_id"`
Participants []string `json:"participants" form:"participants"`
Action whatsmeow.ParticipantRequestChange `json:"action" form:"action"`
}

18
src/go.mod

@ -3,7 +3,7 @@ module github.com/aldinokemal/go-whatsapp-web-multidevice
go 1.24.0
require (
github.com/PuerkitoBio/goquery v1.10.2
github.com/PuerkitoBio/goquery v1.10.3
github.com/disintegration/imaging v1.6.2
github.com/dustin/go-humanize v1.0.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
@ -12,7 +12,7 @@ require (
github.com/gofiber/websocket/v2 v2.2.1
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.27
github.com/mattn/go-sqlite3 v1.14.28
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cobra v1.9.1
@ -20,8 +20,8 @@ require (
github.com/stretchr/testify v1.10.0
github.com/valyala/fasthttp v1.60.0
go.mau.fi/libsignal v0.1.2
go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088
golang.org/x/image v0.25.0
go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526
golang.org/x/image v0.26.0
google.golang.org/protobuf v1.36.6
)
@ -42,12 +42,13 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
@ -56,8 +57,9 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.mau.fi/util v0.8.6 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

20
src/go.sum

@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@ -71,8 +73,14 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a h1:S+AGcmAESQ0pXCUNnRH7V+bOUIgkSX5qVt2cNKCrm0Q=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@ -92,6 +100,8 @@ github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFT
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
@ -134,6 +144,8 @@ go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvP
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 h1:ns6nk2NjqdaQnCKrp+Qqwpf+3OI7+nnH56D71+7XzOM=
go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 h1:i9w16FdM3zmOWdF5nh1l2MlmE/wK7ulL6rbT02WBBJs=
go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -144,9 +156,15 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -165,6 +183,8 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

81
src/internal/rest/group.go

@ -2,6 +2,7 @@ package rest
import (
"fmt"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
@ -22,6 +23,9 @@ func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group {
app.Post("/group/participants/remove", rest.DeleteParticipants)
app.Post("/group/participants/promote", rest.PromoteParticipants)
app.Post("/group/participants/demote", rest.DemoteParticipants)
app.Get("/group/participant-requests", rest.ListParticipantRequests)
app.Post("/group/participant-requests/approve", rest.ApproveParticipantRequests)
app.Post("/group/participant-requests/reject", rest.RejectParticipantRequests)
return rest
}
@ -77,83 +81,86 @@ func (controller *Group) CreateGroup(c *fiber.Ctx) error {
},
})
}
func (controller *Group) AddParticipants(c *fiber.Ctx) error {
var request domainGroup.ParticipantRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
return controller.manageParticipants(c, whatsmeow.ParticipantChangeAdd, "Success add participants")
}
whatsapp.SanitizePhone(&request.GroupID)
func (controller *Group) DeleteParticipants(c *fiber.Ctx) error {
return controller.manageParticipants(c, whatsmeow.ParticipantChangeRemove, "Success delete participants")
}
request.Action = whatsmeow.ParticipantChangeAdd
func (controller *Group) PromoteParticipants(c *fiber.Ctx) error {
return controller.manageParticipants(c, whatsmeow.ParticipantChangePromote, "Success promote participants")
}
result, err := controller.Service.ManageParticipant(c.UserContext(), request)
func (controller *Group) DemoteParticipants(c *fiber.Ctx) error {
return controller.manageParticipants(c, whatsmeow.ParticipantChangeDemote, "Success demote participants")
}
func (controller *Group) ListParticipantRequests(c *fiber.Ctx) error {
var request domainGroup.GetGroupRequestParticipantsRequest
err := c.QueryParser(&request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success add participants",
Results: result,
if request.GroupID == "" {
return c.Status(fiber.StatusBadRequest).JSON(utils.ResponseData{
Status: 400,
Code: "INVALID_GROUP_ID",
Message: "Group ID cannot be empty",
})
}
func (controller *Group) DeleteParticipants(c *fiber.Ctx) error {
var request domainGroup.ParticipantRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
whatsapp.SanitizePhone(&request.GroupID)
request.Action = whatsmeow.ParticipantChangeRemove
result, err := controller.Service.ManageParticipant(c.UserContext(), request)
result, err := controller.Service.GetGroupRequestParticipants(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success delete participants",
Message: "Success getting list requested participants",
Results: result,
})
}
func (controller *Group) PromoteParticipants(c *fiber.Ctx) error {
func (controller *Group) ApproveParticipantRequests(c *fiber.Ctx) error {
return controller.handleRequestedParticipants(c, whatsmeow.ParticipantChangeApprove, "Success approve requested participants")
}
func (controller *Group) RejectParticipantRequests(c *fiber.Ctx) error {
return controller.handleRequestedParticipants(c, whatsmeow.ParticipantChangeReject, "Success reject requested participants")
}
// Generalized participant management handler
func (controller *Group) manageParticipants(c *fiber.Ctx, action whatsmeow.ParticipantChange, successMsg string) error {
var request domainGroup.ParticipantRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
whatsapp.SanitizePhone(&request.GroupID)
request.Action = whatsmeow.ParticipantChangePromote
request.Action = action
result, err := controller.Service.ManageParticipant(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success promote participants",
Message: successMsg,
Results: result,
})
}
func (controller *Group) DemoteParticipants(c *fiber.Ctx) error {
var request domainGroup.ParticipantRequest
// Generalized requested participants handler
func (controller *Group) handleRequestedParticipants(c *fiber.Ctx, action whatsmeow.ParticipantRequestChange, successMsg string) error {
var request domainGroup.GroupRequestParticipantsRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
whatsapp.SanitizePhone(&request.GroupID)
request.Action = whatsmeow.ParticipantChangeDemote
result, err := controller.Service.ManageParticipant(c.UserContext(), request)
request.Action = action
result, err := controller.Service.ManageGroupRequestParticipants(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success demote participants",
Message: successMsg,
Results: result,
})
}

66
src/services/group.go

@ -2,6 +2,8 @@ package services
import (
"context"
"fmt"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
@ -113,6 +115,70 @@ func (service groupService) ManageParticipant(ctx context.Context, request domai
return result, nil
}
func (service groupService) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []domainGroup.GetGroupRequestParticipantsResponse, err error) {
if err = validations.ValidateGetGroupRequestParticipants(ctx, request); err != nil {
return result, err
}
groupJID, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.GroupID)
if err != nil {
return result, err
}
participants, err := service.WaCli.GetGroupRequestParticipants(groupJID)
if err != nil {
return result, err
}
for _, participant := range participants {
result = append(result, domainGroup.GetGroupRequestParticipantsResponse{
JID: participant.JID.String(),
RequestedAt: participant.RequestedAt,
})
}
return result, nil
}
func (service groupService) ManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) (result []domainGroup.ParticipantStatus, err error) {
if err = validations.ValidateManageGroupRequestParticipants(ctx, request); err != nil {
return result, err
}
groupJID, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.GroupID)
if err != nil {
return result, err
}
participantsJID, err := service.participantToJID(request.Participants)
if err != nil {
return result, err
}
participants, err := service.WaCli.UpdateGroupRequestParticipants(groupJID, participantsJID, request.Action)
if err != nil {
return result, err
}
for _, participant := range participants {
if participant.Error != 0 {
result = append(result, domainGroup.ParticipantStatus{
Participant: participant.JID.String(),
Status: "error",
Message: fmt.Sprintf("Action %s failed (code %d)", request.Action, participant.Error),
})
} else {
result = append(result, domainGroup.ParticipantStatus{
Participant: participant.JID.String(),
Status: "success",
Message: fmt.Sprintf("Action %s success", request.Action),
})
}
}
return result, nil
}
func (service groupService) participantToJID(participants []string) ([]types.JID, error) {
var participantsJID []types.JID
for _, participant := range participants {

3
src/services/message.go

@ -5,7 +5,6 @@ import (
"fmt"
"time"
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
@ -55,7 +54,7 @@ func (service serviceMessage) MarkAsRead(ctx context.Context, request domainMess
return response, nil
}
func (service serviceMessage) ReactMessage(ctx context.Context, request message.ReactionRequest) (response message.GenericResponse, err error) {
func (service serviceMessage) ReactMessage(ctx context.Context, request domainMessage.ReactionRequest) (response domainMessage.GenericResponse, err error) {
if err = validations.ValidateReactMessage(ctx, request); err != nil {
return response, err
}

3
src/services/user.go

@ -233,8 +233,7 @@ func (service userService) ChangeAvatar(ctx context.Context, request domainUser.
return nil
}
// ChangePushName implements user.IUserService.
func (service *userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) {
func (service userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) {
whatsapp.MustLogin(service.WaCli)
err = service.WaCli.SendAppState(appstate.BuildSettingPushName(request.PushName))

29
src/validations/group_validation.go

@ -2,9 +2,11 @@ package validations
import (
"context"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
validation "github.com/go-ozzo/ozzo-validation/v4"
"go.mau.fi/whatsmeow"
)
func ValidateJoinGroupWithLink(ctx context.Context, request domainGroup.JoinGroupWithLinkRequest) error {
@ -58,3 +60,30 @@ func ValidateParticipant(ctx context.Context, request domainGroup.ParticipantReq
return nil
}
func ValidateGetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.GroupID, validation.Required),
)
if err != nil {
return pkgError.ValidationError(err.Error())
}
return nil
}
func ValidateManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.GroupID, validation.Required),
validation.Field(&request.Participants, validation.Required),
validation.Field(&request.Participants, validation.Each(validation.Required)),
validation.Field(&request.Action, validation.Required, validation.In(whatsmeow.ParticipantChangeApprove, whatsmeow.ParticipantChangeReject)),
)
if err != nil {
return pkgError.ValidationError(err.Error())
}
return nil
}

2
src/views/assets/app.css

@ -204,7 +204,7 @@ body {
border-radius: 12px !important;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
/* background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%) !important; */
background: var(--primary-color) !important;
/* background: var(--primary-color) !important; */
color: white !important;
font-weight: 600 !important;
letter-spacing: 0.5px;

134
src/views/components/GroupList.js

@ -1,8 +1,20 @@
export default {
name: 'ListGroup',
props: ['connected'],
data() {
return {
groups: []
groups: [],
selectedGroupId: null,
requestedMembers: [],
loadingRequestedMembers: false,
processingMember: null
}
},
computed: {
currentUserId() {
if (!this.connected || this.connected.length === 0) return null;
const device = this.connected[0].device;
return device.split('@')[0].split(':')[0];
}
},
methods: {
@ -67,6 +79,69 @@ export default {
formatDate: function (value) {
if (!value) return ''
return moment(value).format('LLL');
},
isAdmin(ownerJID) {
const owner = ownerJID.split('@')[0];
return owner === this.currentUserId;
},
async handleSeeRequestedMember(group_id) {
this.selectedGroupId = group_id;
this.loadingRequestedMembers = true;
this.requestedMembers = [];
try {
const response = await window.http.get(`/group/participant-requests?group_id=${group_id}`);
this.requestedMembers = response.data.results || [];
this.loadingRequestedMembers = false;
$('#modalRequestedMembers').modal('show');
} catch (error) {
this.loadingRequestedMembers = false;
let errorMessage = "Failed to fetch requested members";
if (error.response) {
errorMessage = error.response.data.message || errorMessage;
}
showErrorInfo(errorMessage);
}
},
formatJID(jid) {
return jid ? jid.split('@')[0] : '';
},
closeRequestedMembersModal() {
$('#modalRequestedMembers').modal('hide');
// open modal again
this.openModal();
},
async handleProcessRequest(member, action) {
if (!this.selectedGroupId || !member) return;
const actionText = action === 'approve' ? 'approve' : 'reject';
const confirmMsg = `Are you sure you want to ${actionText} this member request?`;
const ok = confirm(confirmMsg);
if (!ok) return;
try {
this.processingMember = member.jid;
const payload = {
group_id: this.selectedGroupId,
participants: [this.formatJID(member.jid)]
};
await window.http.post(`/group/participant-requests/${action}`, payload);
// Remove the processed member from the list
this.requestedMembers = this.requestedMembers.filter(m => m.jid !== member.jid);
showSuccessInfo(`Member request ${actionText}d`);
this.processingMember = null;
} catch (error) {
this.processingMember = null;
let errorMessage = `Failed to ${actionText} member request`;
if (error.response) {
errorMessage = error.response.data.message || errorMessage;
}
showErrorInfo(errorMessage);
}
}
},
template: `
@ -81,7 +156,7 @@ export default {
</div>
<!-- Modal AccountGroup -->
<div class="ui small modal" id="modalGroupList">
<div class="ui large modal" id="modalGroupList">
<i class="close icon"></i>
<div class="header">
My Group List
@ -104,12 +179,67 @@ export default {
<td>{{ g.Participants.length }}</td>
<td>{{ formatDate(g.GroupCreated) }}</td>
<td>
<div style="display: flex; gap: 8px; align-items: center;">
<button v-if="isAdmin(g.OwnerJID)" class="ui green tiny button" @click="handleSeeRequestedMember(g.JID)">Requested Members</button>
<button class="ui red tiny button" @click="handleLeaveGroup(g.JID)">Leave</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Requested Members Modal -->
<div class="ui modal" id="modalRequestedMembers">
<i class="close icon"></i>
<div class="header">
Requested Group Members
</div>
<div class="content">
<div v-if="loadingRequestedMembers" class="ui active centered inline loader"></div>
<div v-else-if="requestedMembers.length === 0" class="ui info message">
<div class="header">No Requested Members</div>
<p>There are no pending member requests for this group.</p>
</div>
<table v-else class="ui celled table">
<thead>
<tr>
<th>User ID</th>
<th>Request Time</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="member in requestedMembers" :key="member.jid">
<td>{{ formatJID(member.jid) }}</td>
<td>{{ formatDate(member.requested_at) }}</td>
<td>
<div class="ui mini buttons">
<button class="ui green button"
@click="handleProcessRequest(member, 'approve')"
:disabled="processingMember === member.jid">
<i v-if="processingMember === member.jid" class="spinner loading icon"></i>
Approve
</button>
<div class="or"></div>
<button class="ui red button"
@click="handleProcessRequest(member, 'reject')"
:disabled="processingMember === member.jid">
<i v-if="processingMember === member.jid" class="spinner loading icon"></i>
Reject
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="actions">
<div class="ui button" @click="closeRequestedMembersModal">Close</div>
</div>
</div>
`
}

4
src/views/index.html

@ -136,7 +136,7 @@
</div>
<div class="ui three column doubling grid cards">
<group-list></group-list>
<group-list :connected="connected_devices"></group-list>
<group-create></group-create>
<group-join-with-link></group-join-with-link>
<group-add-participants></group-add-participants>
@ -218,12 +218,12 @@
import GroupCreate from "./components/GroupCreate.js";
import GroupJoinWithLink from "./components/GroupJoinWithLink.js";
import GroupAddParticipants from "./components/GroupManageParticipants.js";
import NewsletterList from "./components/NewsletterList.js";
import AccountAvatar from "./components/AccountAvatar.js";
import AccountChangeAvatar from "./components/AccountChangeAvatar.js";
import AccountChangePushName from "./components/AccountChangePushName.js";
import AccountUserInfo from "./components/AccountUserInfo.js";
import AccountPrivacy from "./components/AccountPrivacy.js";
import NewsletterList from "./components/NewsletterList.js";
import AccountContact from "./components/AccountContact.js";
const showErrorInfo = (message) => {

Loading…
Cancel
Save