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"
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
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)
**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.

2
src/config/settings.go

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

8
src/domains/group/group.go

@ -2,6 +2,7 @@ package group
import (
"context"
"time"
"go.mau.fi/whatsmeow"
)
@ -11,7 +12,7 @@ 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 []string, err error)
GetGroupRequestParticipants(ctx context.Context, request GetGroupRequestParticipantsRequest) (result []GetGroupRequestParticipantsResponse, 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"`
}
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"`

7
src/services/group.go

@ -115,7 +115,7 @@ func (service groupService) ManageParticipant(ctx context.Context, request domai
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 {
return result, err
}
@ -131,7 +131,10 @@ func (service groupService) GetGroupRequestParticipants(ctx context.Context, req
}
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

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;

160
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,93 @@ 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/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: `
@ -81,7 +180,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 +203,67 @@ export default {
<td>{{ g.Participants.length }}</td>
<td>{{ formatDate(g.GroupCreated) }}</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>
</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="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 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