Browse Source

feat: add group participants (#132)

* feat: add group participant api

* fix: sanitize leave group

* chore: update openapi

* feat: ui add participant

* chore: update docs

* chore: update docs

* chore: update docs
pull/152/head v4.13.0
Aldino Kemal 2 years ago
committed by GitHub
parent
commit
e78767d289
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 90
      docs/openapi.yaml
  2. 13
      readme.md
  3. 2
      src/config/settings.go
  4. 12
      src/domains/group/group.go
  5. 22
      src/internal/rest/group.go
  6. 3
      src/pkg/whatsapp/whatsapp.go
  7. 70
      src/services/group.go
  8. 14
      src/validations/group_validation.go
  9. 119
      src/views/components/GroupAddParticipants.js
  10. 4
      src/views/index.html

90
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: 3.10.1
version: 3.11.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
@ -773,7 +773,53 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SendResponse'
$ref: '#/components/schemas/CreateGroupResponse'
'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/participants:
post:
operationId: addParticipantToGroup
tags:
- group
summary: Adding more participants to group
requestBody:
content:
application/json:
schema:
type: object
properties:
group_id:
type: string
example: '120363228882361111'
participants:
type: array
items:
type: string
example:
- '6819241294719274'
- '6829241294719274'
- '6839241294719274'
example:
- '6819241294719274'
- '6829241294719274'
- '6839241294719274'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AddParticipantToGroupResponse'
'400': '400':
description: Bad Request description: Bad Request
content: content:
@ -807,7 +853,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SendResponse'
$ref: '#/components/schemas/GenericResponse'
'400': '400':
description: Bad Request description: Bad Request
content: content:
@ -857,6 +903,44 @@ paths:
components: components:
schemas: schemas:
CreateGroupResponse:
type: object
properties:
code:
type: string
example: SUCCESS
message:
type: string
example: Success get list groups
results:
type: object
properties:
group_id:
type: string
example: 1203632782168851111@g.us
AddParticipantToGroupResponse:
type: object
properties:
code:
type: string
example: SUCCESS
message:
type: string
example: Success get list groups
results:
type: array
items:
properties:
participant:
type: string
example: '6289987391723@s.whatsapp.net'
status:
type: string
example: success
message:
type: string
example: Participant added
UserGroupResponse: UserGroupResponse:
type: object type: object
properties: properties:

13
readme.md

@ -93,11 +93,11 @@ You can fork or edit this source code !
### Current API ### Current API
You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API, furthermore you can generate HTTP Client from this
API using [openapi-generator](https://openapi-generator.tech/#try)
- You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API or paste to [SwaggerEditor](https://editor.swagger.io).
- Furthermore you can generate HTTP Client from this API using [openapi-generator](https://openapi-generator.tech/#try)
| Feature | Menu | Method | URL | | Feature | Menu | Method | URL |
|---------|--------------------------------|--------|-----------------------------|
|---------|------------------------------|--------|-----------------------------|
| ✅ | Login | GET | /app/login | | ✅ | Login | GET | /app/login |
| ✅ | Logout | GET | /app/logout | | ✅ | Logout | GET | /app/logout |
| ✅ | Reconnect | GET | /app/reconnect | | ✅ | Reconnect | GET | /app/reconnect |
@ -120,9 +120,10 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
| ✅ | Join Group With Link | POST | /group/join-with-link | | ✅ | Join Group With Link | POST | /group/join-with-link |
| ✅ | Leave Group | POST | /group/leave | | ✅ | Leave Group | POST | /group/leave |
| ✅ | Create Group | POST | /group | | ✅ | Create Group | POST | /group |
| ❌ | Add More Participants in Group | POST | |
| ❌ | Remove Participant in Group | POST | |
| ❌ | Promote Participant in Group | POST | |
| ✅ | Add Participants in Group | POST | /group/participants |
| ❌ | Remove Participant in Group | DELETE | /group/participants |
| ❌ | Promote Participant in Group | POST | /group/participants/promote |
| ❌ | Demote Participant in Group | POST | /group/participants/demote |
``` ```
✅ = Available ✅ = Available

2
src/config/settings.go

@ -5,7 +5,7 @@ import (
) )
var ( var (
AppVersion = "v4.12.0"
AppVersion = "v4.13.0"
AppPort = "3000" AppPort = "3000"
AppDebug = false AppDebug = false
AppOs = "AldinoKemal" AppOs = "AldinoKemal"

12
src/domains/group/group.go

@ -6,6 +6,7 @@ type IGroupService interface {
JoinGroupWithLink(ctx context.Context, request JoinGroupWithLinkRequest) (groupID string, err error) JoinGroupWithLink(ctx context.Context, request JoinGroupWithLinkRequest) (groupID string, err error)
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)
AddParticipant(ctx context.Context, request ParticipantRequest) (result []ParticipantStatus, err error)
} }
type JoinGroupWithLinkRequest struct { type JoinGroupWithLinkRequest struct {
@ -20,3 +21,14 @@ type CreateGroupRequest struct {
Title string `json:"title" form:"title"` Title string `json:"title" form:"title"`
Participants []string `json:"participants" form:"participants"` Participants []string `json:"participants" form:"participants"`
} }
type ParticipantRequest struct {
GroupID string `json:"group_id" form:"group_id"`
Participants []string `json:"participants" form:"participants"`
}
type ParticipantStatus struct {
Participant string `json:"participant"`
Status string `json:"status"`
Message string `json:"message"`
}

22
src/internal/rest/group.go

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group" 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/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -16,6 +17,7 @@ func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group {
app.Post("/group", rest.CreateGroup) app.Post("/group", rest.CreateGroup)
app.Post("/group/join-with-link", rest.JoinGroupWithLink) app.Post("/group/join-with-link", rest.JoinGroupWithLink)
app.Post("/group/leave", rest.LeaveGroup) app.Post("/group/leave", rest.LeaveGroup)
app.Post("/group/participants", rest.AddParticipants)
return rest return rest
} }
@ -42,6 +44,8 @@ func (controller *Group) LeaveGroup(c *fiber.Ctx) error {
err := c.BodyParser(&request) err := c.BodyParser(&request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
whatsapp.SanitizePhone(&request.GroupID)
err = controller.Service.LeaveGroup(c.UserContext(), request) err = controller.Service.LeaveGroup(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -69,3 +73,21 @@ 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)
result, err := controller.Service.AddParticipant(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success add participants",
Results: result,
})
}

3
src/pkg/whatsapp/whatsapp.go

@ -233,9 +233,6 @@ func handler(rawEvt interface{}) {
if evt.IsViewOnce { if evt.IsViewOnce {
metaParts = append(metaParts, "view once") metaParts = append(metaParts, "view once")
} }
if evt.IsViewOnce {
metaParts = append(metaParts, "ephemeral")
}
log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message) log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message)

70
src/services/group.go

@ -53,17 +53,9 @@ func (service groupService) CreateGroup(ctx context.Context, request domainGroup
} }
whatsapp.MustLogin(service.WaCli) whatsapp.MustLogin(service.WaCli)
var participantsJID []types.JID
for _, participant := range request.Participants {
formattedParticipant := participant + config.WhatsappTypeUser
if !whatsapp.IsOnWhatsapp(service.WaCli, formattedParticipant) {
return "", pkgError.ErrUserNotRegistered
}
if participantJID, err := types.ParseJID(formattedParticipant); err == nil {
participantsJID = append(participantsJID, participantJID)
}
participantsJID, err := service.participantToJID(request.Participants)
if err != nil {
return
} }
groupConfig := whatsmeow.ReqCreateGroup{ groupConfig := whatsmeow.ReqCreateGroup{
@ -80,3 +72,59 @@ func (service groupService) CreateGroup(ctx context.Context, request domainGroup
return groupInfo.JID.String(), nil return groupInfo.JID.String(), nil
} }
func (service groupService) AddParticipant(ctx context.Context, request domainGroup.ParticipantRequest) (result []domainGroup.ParticipantStatus, err error) {
if err = validations.ValidateParticipant(ctx, request); err != nil {
return result, err
}
whatsapp.MustLogin(service.WaCli)
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.UpdateGroupParticipants(groupJID, participantsJID, whatsmeow.ParticipantChangeAdd)
if err != nil {
return result, err
}
for _, participant := range participants {
if participant.Error == 403 && participant.AddRequest != nil {
result = append(result, domainGroup.ParticipantStatus{
Participant: participant.JID.String(),
Status: "error",
Message: "Failed to add participant",
})
} else {
result = append(result, domainGroup.ParticipantStatus{
Participant: participant.JID.String(),
Status: "success",
Message: "Participant added",
})
}
}
return result, nil
}
func (service groupService) participantToJID(participants []string) ([]types.JID, error) {
var participantsJID []types.JID
for _, participant := range participants {
formattedParticipant := participant + config.WhatsappTypeUser
if !whatsapp.IsOnWhatsapp(service.WaCli, formattedParticipant) {
return nil, pkgError.ErrUserNotRegistered
}
if participantJID, err := types.ParseJID(formattedParticipant); err == nil {
participantsJID = append(participantsJID, participantJID)
}
}
return participantsJID, nil
}

14
src/validations/group_validation.go

@ -44,3 +44,17 @@ func ValidateCreateGroup(ctx context.Context, request domainGroup.CreateGroupReq
return nil return nil
} }
func ValidateParticipant(ctx context.Context, request domainGroup.ParticipantRequest) 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)),
)
if err != nil {
return pkgError.ValidationError(err.Error())
}
return nil
}

119
src/views/components/GroupAddParticipants.js

@ -0,0 +1,119 @@
export default {
name: 'AddParticipantsToGroup',
data() {
return {
loading: false,
group: '',
participants: ['', ''],
}
},
computed: {
group_id() {
return `${this.group}@${window.TYPEGROUP}`
}
},
methods: {
openModal() {
$('#modalGroupAddParticipant').modal({
onApprove: function () {
return false;
}
}).modal('show');
},
handleAddParticipant() {
this.participants.push('')
},
handleDeleteParticipant(index) {
this.participants.splice(index, 1)
},
async handleSubmit() {
try {
let response = await this.submitApi()
showSuccessInfo(response)
$('#modalGroupAddParticipant').modal('hide');
} catch (err) {
showErrorInfo(err)
}
},
async submitApi() {
this.loading = true;
try {
let response = await window.http.post(`/group/participants`, {
group_id: this.group_id,
// convert participant become list of string
participants: this.participants.filter(participant => participant !== '').map(participant => `${participant}`)
})
this.handleReset();
return response.data.message;
} catch (error) {
if (error.response) {
throw new Error(error.response.data.message);
}
throw new Error(error.message);
} finally {
this.loading = false;
}
},
handleReset() {
this.group = '';
this.participants = ['', ''];
},
},
template: `
<div class="green card" @click="openModal" style="cursor: pointer">
<div class="content">
<a class="ui green right ribbon label">Group</a>
<div class="header">Add Participants</div>
<div class="description">
Add multiple participants
</div>
</div>
</div>
<!-- Modal AccountGroup -->
<div class="ui small modal" id="modalGroupAddParticipant">
<i class="close icon"></i>
<div class="header">
Add Participants
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Group ID</label>
<input v-model="group" type="text"
placeholder="12036322888236XXXX..."
aria-label="Group Name">
<input :value="group_id" disabled aria-label="whatsapp_id">
</div>
<div class="field">
<label>Participants</label>
<div style="display: flex; flex-direction: column; gap: 5px">
<div class="ui action input" :key="index" v-for="(participant, index) in participants">
<input type="number" placeholder="Phone Int Number (6289...)" v-model="participants[index]"
aria-label="list participant">
<button class="ui button" @click="handleDeleteParticipant(index)" type="button">
<i class="minus circle icon"></i>
</button>
</div>
<div class="field" style="display: flex; flex-direction: column; gap: 3px">
<div>
<button class="mini ui primary button" @click="handleAddParticipant" type="button">
<i class="plus icon"></i> Option
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.loading}"
@click="handleSubmit" type="button">
Create
<i class="send icon"></i>
</div>
</div>
</div>
`
}

4
src/views/index.html

@ -81,6 +81,7 @@
<group-list></group-list> <group-list></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>
</div> </div>
<div class="ui horizontal divider"> <div class="ui horizontal divider">
@ -141,6 +142,7 @@
import GroupList from "./components/GroupList.js"; import GroupList from "./components/GroupList.js";
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/GroupAddParticipants.js";
import AccountAvatar from "./components/AccountAvatar.js"; import AccountAvatar from "./components/AccountAvatar.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";
@ -169,7 +171,7 @@
AppLogin, AppLogout, AppReconnect, AppLogin, AppLogout, AppReconnect,
SendMessage, SendImage, SendFile, SendVideo, SendContact, SendLocation, SendAudio, SendPoll, SendMessage, SendImage, SendFile, SendVideo, SendContact, SendLocation, SendAudio, SendPoll,
MessageUpdate, MessageReact, MessageRevoke, MessageUpdate, MessageReact, MessageRevoke,
GroupList, GroupCreate, GroupJoinWithLink,
GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants,
AccountAvatar, AccountUserInfo, AccountPrivacy AccountAvatar, AccountUserInfo, AccountPrivacy
}, },
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],

Loading…
Cancel
Save