Browse Source

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.
pull/276/head
Aldino Kemal 11 months ago
parent
commit
e05430e811
  1. 156
      docs/openapi.yaml
  2. 93
      readme.md
  3. 2
      src/config/settings.go
  4. 8
      src/domains/group/group.go
  5. 7
      src/services/group.go
  6. 2
      src/views/assets/app.css
  7. 160
      src/views/components/GroupList.js
  8. 4
      src/views/index.html

156
docs/openapi.yaml

@ -1,7 +1,7 @@
openapi: "3.0.0" openapi: "3.0.0"
info: info:
title: WhatsApp API MultiDevice title: WhatsApp API MultiDevice
version: 5.3.0
version: 5.4.0
description: This API is used for sending whatsapp via API description: This API is used for sending whatsapp via API
servers: servers:
- url: http://localhost:3000 - url: http://localhost:3000
@ -18,6 +18,9 @@ tags:
description: Group setting description: Group setting
- name: newsletter - name: newsletter
description: newsletter setting description: newsletter setting
security:
- basicAuth: []
paths: paths:
/app/login: /app/login:
get: get:
@ -1217,6 +1220,123 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorInternalServer' $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: /group/leave:
post: post:
operationId: leaveGroup operationId: leaveGroup
@ -1287,6 +1407,10 @@ paths:
$ref: '#/components/schemas/ErrorInternalServer' $ref: '#/components/schemas/ErrorInternalServer'
components: components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas: schemas:
CreateGroupResponse: CreateGroupResponse:
type: object type: object
@ -1319,6 +1443,7 @@ components:
- '6839241294719274' - '6839241294719274'
ManageParticipantResponse: ManageParticipantResponse:
type: object type: object
additionalProperties: false
properties: properties:
code: code:
type: string type: string
@ -1329,6 +1454,8 @@ components:
results: results:
type: array type: array
items: items:
type: object
additionalProperties: false
properties: properties:
participant: participant:
type: string type: string
@ -1851,4 +1978,29 @@ components:
example: 0 example: 0
AddRequest: AddRequest:
type: string type: string
example: null
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"

93
readme.md

@ -2,7 +2,7 @@
[![Patreon](https://img.shields.io/badge/Support%20on-Patreon-orange.svg)](https://www.patreon.com/c/aldinokemal) [![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!** **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) ![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"` - `-w="http://yourwebhook.site/handler"`
- Webhook Secret - Webhook Secret
Our webhook will be sent to you with an HMAC header and a sha256 default key `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: You may modify this by using the option below:
- `--webhook-secret="secret"` - `--webhook-secret="secret"`
## Configuration ## 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) 1. Command-line flags (highest priority)
2. Environment variables 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. - Use [SwaggerEditor](https://editor.swagger.io) to visualize the API.
- Generate HTTP clients using [openapi-generator](https://openapi-generator.tech/#try). - 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 ```txt
✅ = Available ✅ = 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) - Please do this if you have an error (invalid flag in pkg-config --cflags: -Xpreprocessor)
`export CGO_CFLAGS_ALLOW="-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.

2
src/config/settings.go

@ -5,7 +5,7 @@ import (
) )
var ( var (
AppVersion = "v5.5.0"
AppVersion = "v5.6.0"
AppPort = "3000" AppPort = "3000"
AppDebug = false AppDebug = false
AppOs = "AldinoKemal" AppOs = "AldinoKemal"

8
src/domains/group/group.go

@ -2,6 +2,7 @@ package group
import ( import (
"context" "context"
"time"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
) )
@ -11,7 +12,7 @@ type IGroupService interface {
LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error) LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error)
CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error) CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error)
ManageParticipant(ctx context.Context, request ParticipantRequest) (result []ParticipantStatus, err error) ManageParticipant(ctx context.Context, request ParticipantRequest) (result []ParticipantStatus, err error)
GetGroupRequestParticipants(ctx context.Context, request GetGroupRequestParticipantsRequest) (result []string, err error)
GetGroupRequestParticipants(ctx context.Context, request GetGroupRequestParticipantsRequest) (result []GetGroupRequestParticipantsResponse, err error)
ManageGroupRequestParticipants(ctx context.Context, request GroupRequestParticipantsRequest) (result []ParticipantStatus, err error) ManageGroupRequestParticipants(ctx context.Context, request GroupRequestParticipantsRequest) (result []ParticipantStatus, err error)
} }
@ -44,6 +45,11 @@ type GetGroupRequestParticipantsRequest struct {
GroupID string `json:"group_id" query:"group_id"` GroupID string `json:"group_id" query:"group_id"`
} }
type GetGroupRequestParticipantsResponse struct {
JID string `json:"jid"`
RequestedAt time.Time `json:"requested_at"`
}
type GroupRequestParticipantsRequest struct { type GroupRequestParticipantsRequest struct {
GroupID string `json:"group_id" form:"group_id"` GroupID string `json:"group_id" form:"group_id"`
Participants []string `json:"participants" form:"participants"` Participants []string `json:"participants" form:"participants"`

7
src/services/group.go

@ -115,7 +115,7 @@ func (service groupService) ManageParticipant(ctx context.Context, request domai
return result, nil return result, nil
} }
func (service groupService) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []string, err error) {
func (service groupService) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []domainGroup.GetGroupRequestParticipantsResponse, err error) {
if err = validations.ValidateGetGroupRequestParticipants(ctx, request); err != nil { if err = validations.ValidateGetGroupRequestParticipants(ctx, request); err != nil {
return result, err return result, err
} }
@ -131,7 +131,10 @@ func (service groupService) GetGroupRequestParticipants(ctx context.Context, req
} }
for _, participant := range participants { for _, participant := range participants {
result = append(result, participant.JID.String())
result = append(result, domainGroup.GetGroupRequestParticipantsResponse{
JID: participant.JID.String(),
RequestedAt: participant.RequestedAt,
})
} }
return result, nil return result, nil

2
src/views/assets/app.css

@ -204,7 +204,7 @@ body {
border-radius: 12px !important; border-radius: 12px !important;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !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: 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; color: white !important;
font-weight: 600 !important; font-weight: 600 !important;
letter-spacing: 0.5px; letter-spacing: 0.5px;

160
src/views/components/GroupList.js

@ -1,8 +1,20 @@
export default { export default {
name: 'ListGroup', name: 'ListGroup',
props: ['connected'],
data() { data() {
return { 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: { methods: {
@ -67,6 +79,93 @@ export default {
formatDate: function (value) { formatDate: function (value) {
if (!value) return '' if (!value) return ''
return moment(value).format('LLL'); 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/participants/requested?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 handleApproveRequest(member) {
if (!this.selectedGroupId || !member) return;
try {
this.processingMember = member.jid;
const payload = {
group_id: this.selectedGroupId,
participants: [this.formatJID(member.jid)]
};
await window.http.post('/group/participants/requested/approve', payload);
// Remove the approved member from the list
this.requestedMembers = this.requestedMembers.filter(m => m.jid !== member.jid);
showSuccessInfo("Member request approved");
this.processingMember = null;
} catch (error) {
this.processingMember = null;
let errorMessage = "Failed to approve member request";
if (error.response) {
errorMessage = error.response.data.message || errorMessage;
}
showErrorInfo(errorMessage);
}
},
async handleRejectRequest(member) {
if (!this.selectedGroupId || !member) return;
try {
this.processingMember = member.jid;
const payload = {
group_id: this.selectedGroupId,
participants: [this.formatJID(member.jid)]
};
await window.http.post('/group/participants/requested/reject', payload);
// Remove the rejected member from the list
this.requestedMembers = this.requestedMembers.filter(m => m.jid !== member.jid);
showSuccessInfo("Member request rejected");
this.processingMember = null;
} catch (error) {
this.processingMember = null;
let errorMessage = "Failed to reject member request";
if (error.response) {
errorMessage = error.response.data.message || errorMessage;
}
showErrorInfo(errorMessage);
}
} }
}, },
template: ` template: `
@ -81,7 +180,7 @@ export default {
</div> </div>
<!-- Modal AccountGroup --> <!-- Modal AccountGroup -->
<div class="ui small modal" id="modalGroupList">
<div class="ui large modal" id="modalGroupList">
<i class="close icon"></i> <i class="close icon"></i>
<div class="header"> <div class="header">
My Group List My Group List
@ -104,12 +203,67 @@ export default {
<td>{{ g.Participants.length }}</td> <td>{{ g.Participants.length }}</td>
<td>{{ formatDate(g.GroupCreated) }}</td> <td>{{ formatDate(g.GroupCreated) }}</td>
<td> <td>
<button class="ui red tiny button" @click="handleLeaveGroup(g.JID)">Leave</button>
<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> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</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="handleApproveRequest(member)"
: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="handleRejectRequest(member)"
: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>
<div class="ui three column doubling grid cards"> <div class="ui three column doubling grid cards">
<group-list></group-list>
<group-list :connected="connected_devices"></group-list>
<group-create></group-create> <group-create></group-create>
<group-join-with-link></group-join-with-link> <group-join-with-link></group-join-with-link>
<group-add-participants></group-add-participants> <group-add-participants></group-add-participants>
@ -218,12 +218,12 @@
import GroupCreate from "./components/GroupCreate.js"; import GroupCreate from "./components/GroupCreate.js";
import GroupJoinWithLink from "./components/GroupJoinWithLink.js"; import GroupJoinWithLink from "./components/GroupJoinWithLink.js";
import GroupAddParticipants from "./components/GroupManageParticipants.js"; import GroupAddParticipants from "./components/GroupManageParticipants.js";
import NewsletterList from "./components/NewsletterList.js";
import AccountAvatar from "./components/AccountAvatar.js"; import AccountAvatar from "./components/AccountAvatar.js";
import AccountChangeAvatar from "./components/AccountChangeAvatar.js"; import AccountChangeAvatar from "./components/AccountChangeAvatar.js";
import AccountChangePushName from "./components/AccountChangePushName.js"; import AccountChangePushName from "./components/AccountChangePushName.js";
import AccountUserInfo from "./components/AccountUserInfo.js"; import AccountUserInfo from "./components/AccountUserInfo.js";
import AccountPrivacy from "./components/AccountPrivacy.js"; import AccountPrivacy from "./components/AccountPrivacy.js";
import NewsletterList from "./components/NewsletterList.js";
import AccountContact from "./components/AccountContact.js"; import AccountContact from "./components/AccountContact.js";
const showErrorInfo = (message) => { const showErrorInfo = (message) => {

Loading…
Cancel
Save