diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0ba4426..3b34482 100644 --- a/docs/openapi.yaml +++ b/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 @@ -1851,4 +1978,29 @@ components: example: 0 AddRequest: type: string - example: null \ No newline at end of file + 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" \ No newline at end of file diff --git a/readme.md b/readme.md index 488bad9..7c25343 100644 --- a/readme.md +++ b/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) @@ -44,13 +44,14 @@ Now that we support ARM64 for Linux: - `-w="http://yourwebhook.site/handler"` - Webhook Secret Our webhook will be sent to you with an HMAC header and a sha256 default key `secret`. - + You may modify this by using the option below: - `--webhook-secret="secret"` ## 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 @@ -181,45 +182,48 @@ You can fork or edit this source code ! - Use [SwaggerEditor](https://editor.swagger.io) to visualize the API. - 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 | -| ✅ | Reconnect | GET | /app/reconnect | -| ✅ | Devices | GET | /app/devices | -| ✅ | User Info | GET | /user/info | -| ✅ | User Avatar | GET | /user/avatar | -| ✅ | User Change Avatar | POST | /user/avatar | -| ✅ | User Change PushName | POST | /user/pushname | -| ✅ | User My Groups | GET | /user/my/groups | -| ✅ | User My Newsletter | GET | /user/my/newsletters | -| ✅ | User My Privacy Setting | GET | /user/my/privacy | -| ✅ | User My Contacts | GET | /user/my/contacts | -| ✅ | Send Message | POST | /send/message | -| ✅ | Send Image | POST | /send/image | -| ✅ | Send Audio | POST | /send/audio | -| ✅ | Send File | POST | /send/file | -| ✅ | Send Video | POST | /send/video | -| ✅ | Send Contact | POST | /send/contact | -| ✅ | Send Link | POST | /send/link | -| ✅ | Send Location | POST | /send/location | -| ✅ | Send Poll / Vote | POST | /send/poll | -| ✅ | Send Presence | POST | /send/presence | -| ✅ | Revoke Message | POST | /message/:message_id/revoke | -| ✅ | React Message | POST | /message/:message_id/reaction | -| ✅ | 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 | -| ✅ | Join Group With Link | POST | /group/join-with-link | -| ✅ | Leave Group | POST | /group/leave | -| ✅ | Create Group | POST | /group | -| ✅ | Add Participants in Group | POST | /group/participants | -| ✅ | Remove Participant in Group | POST | /group/participants/remove | -| ✅ | Promote Participant in Group | POST | /group/participants/promote | -| ✅ | Demote Participant in Group | POST | /group/participants/demote | -| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | +| Feature | Menu | Method | URL | +|---------|----------------------------------------|--------|---------------------------------------| +| ✅ | Login with Scan QR | GET | /app/login | +| ✅ | Login With Pair Code | GET | /app/login-with-code | +| ✅ | Logout | GET | /app/logout | +| ✅ | Reconnect | GET | /app/reconnect | +| ✅ | Devices | GET | /app/devices | +| ✅ | User Info | GET | /user/info | +| ✅ | User Avatar | GET | /user/avatar | +| ✅ | User Change Avatar | POST | /user/avatar | +| ✅ | User Change PushName | POST | /user/pushname | +| ✅ | User My Groups | GET | /user/my/groups | +| ✅ | User My Newsletter | GET | /user/my/newsletters | +| ✅ | User My Privacy Setting | GET | /user/my/privacy | +| ✅ | User My Contacts | GET | /user/my/contacts | +| ✅ | Send Message | POST | /send/message | +| ✅ | Send Image | POST | /send/image | +| ✅ | Send Audio | POST | /send/audio | +| ✅ | Send File | POST | /send/file | +| ✅ | Send Video | POST | /send/video | +| ✅ | Send Contact | POST | /send/contact | +| ✅ | Send Link | POST | /send/link | +| ✅ | Send Location | POST | /send/location | +| ✅ | Send Poll / Vote | POST | /send/poll | +| ✅ | Send Presence | POST | /send/presence | +| ✅ | Revoke Message | POST | /message/:message_id/revoke | +| ✅ | React Message | POST | /message/:message_id/reaction | +| ✅ | 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 | +| ✅ | Join Group With Link | POST | /group/join-with-link | +| ✅ | Leave Group | POST | /group/leave | +| ✅ | Create Group | POST | /group | +| ✅ | Add Participants in Group | POST | /group/participants | +| ✅ | 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 ✅ = Available @@ -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. diff --git a/src/config/settings.go b/src/config/settings.go index 712a431..7815f1d 100644 --- a/src/config/settings.go +++ b/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 diff --git a/src/domains/group/group.go b/src/domains/group/group.go index 59d6764..36e8db4 100644 --- a/src/domains/group/group.go +++ b/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"` +} diff --git a/src/go.mod b/src/go.mod index a5f24f0..dda8e1a 100644 --- a/src/go.mod +++ b/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 diff --git a/src/go.sum b/src/go.sum index 5fd4c2b..b2b1136 100644 --- a/src/go.sum +++ b/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= diff --git a/src/internal/rest/group.go b/src/internal/rest/group.go index 53f98bd..b365862 100644 --- a/src/internal/rest/group.go +++ b/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) - - whatsapp.SanitizePhone(&request.GroupID) + return controller.manageParticipants(c, whatsmeow.ParticipantChangeAdd, "Success add participants") +} - request.Action = whatsmeow.ParticipantChangeAdd +func (controller *Group) DeleteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangeRemove, "Success delete participants") +} - result, err := controller.Service.ManageParticipant(c.UserContext(), request) - utils.PanicIfNeeded(err) +func (controller *Group) PromoteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangePromote, "Success promote participants") +} - return c.JSON(utils.ResponseData{ - Status: 200, - Code: "SUCCESS", - Message: "Success add participants", - Results: result, - }) +func (controller *Group) DemoteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangeDemote, "Success demote participants") } -func (controller *Group) DeleteParticipants(c *fiber.Ctx) error { - var request domainGroup.ParticipantRequest - err := c.BodyParser(&request) +func (controller *Group) ListParticipantRequests(c *fiber.Ctx) error { + var request domainGroup.GetGroupRequestParticipantsRequest + err := c.QueryParser(&request) utils.PanicIfNeeded(err) - whatsapp.SanitizePhone(&request.GroupID) + if request.GroupID == "" { + return c.Status(fiber.StatusBadRequest).JSON(utils.ResponseData{ + Status: 400, + Code: "INVALID_GROUP_ID", + Message: "Group ID cannot be empty", + }) + } - request.Action = whatsmeow.ParticipantChangeRemove + whatsapp.SanitizePhone(&request.GroupID) - 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, }) } diff --git a/src/services/group.go b/src/services/group.go index afe2aa1..2c31bb6 100644 --- a/src/services/group.go +++ b/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 { diff --git a/src/services/message.go b/src/services/message.go index 1a9eaaa..9d16da1 100644 --- a/src/services/message.go +++ b/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 } diff --git a/src/services/user.go b/src/services/user.go index b49562d..12e7f1f 100644 --- a/src/services/user.go +++ b/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)) diff --git a/src/validations/group_validation.go b/src/validations/group_validation.go index 0758c1f..cd6ab5a 100644 --- a/src/validations/group_validation.go +++ b/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 +} diff --git a/src/views/assets/app.css b/src/views/assets/app.css index 7b7fe6f..9c41e99 100644 --- a/src/views/assets/app.css +++ b/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; diff --git a/src/views/components/GroupList.js b/src/views/components/GroupList.js index 38cb2d5..3fe44c2 100644 --- a/src/views/components/GroupList.js +++ b/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 { -
- + @@ -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) => {