Browse Source

feat: create and join group (#123)

* feat: add create group api

* feat: add create group UI

* chore: update docs

* feat: add join group invitation ui

* chore update docs
pull/126/head
Aldino Kemal 2 years ago
committed by GitHub
parent
commit
4efd3c43bc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 52
      readme.md
  2. 4
      src/config/settings.go
  3. 6
      src/domains/group/group.go
  4. 20
      src/internal/rest/group.go
  5. 5
      src/pkg/error/whatsapp_error.go
  6. 8
      src/pkg/whatsapp/whatsapp.go
  7. 37
      src/services/group.go
  8. 14
      src/validations/group_validation.go
  9. 114
      src/views/components/GroupCreate.js
  10. 83
      src/views/components/GroupJoinWithLink.js
  11. 11
      src/views/components/GroupList.js
  12. 20
      src/views/index.html

52
readme.md

@ -99,29 +99,33 @@ You can fork or edit this source code !
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)
| Feature | Menu | Method | URL |
|---------|-------------------------|--------|-----------------------------|
| ✅ | Login | GET | /app/login |
| ✅ | Logout | GET | /app/logout |
| ✅ | Reconnect | GET | /app/reconnect |
| ✅ | User Info | GET | /user/info |
| ✅ | User Avatar | GET | /user/avatar |
| ✅ | User My Group List | GET | /user/my/groups |
| ✅ | User My Privacy Setting | GET | /user/my/privacy |
| ✅ | 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 |
| ✅ | Revoke Message | POST | /message/:message_id/revoke |
| ✅ | React Message | POST | /message/:message_id/react |
| ✅ | Edit Message | POST | /message/:message_id/update |
| ✅ | Join Group With Link | POST | /group/join-with-link |
| ✅ | Leave Group | POST | /group/leave |
| Feature | Menu | Method | URL |
|---------|--------------------------------|--------|-----------------------------|
| ✅ | Login | GET | /app/login |
| ✅ | Logout | GET | /app/logout |
| ✅ | Reconnect | GET | /app/reconnect |
| ✅ | User Info | GET | /user/info |
| ✅ | User Avatar | GET | /user/avatar |
| ✅ | User My Group List | GET | /user/my/groups |
| ✅ | User My Privacy Setting | GET | /user/my/privacy |
| ✅ | 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 |
| ✅ | Revoke Message | POST | /message/:message_id/revoke |
| ✅ | React Message | POST | /message/:message_id/react |
| ✅ | Edit Message | POST | /message/:message_id/update |
| ✅ | Join Group With Link | POST | /group/join-with-link |
| ✅ | Leave Group | POST | /group/leave |
| ✅ | Create Group | POST | /group/leave |
| ❌ | Add More Participants in Group | POST | |
| ❌ | Remove Participant in Group | POST | |
| ❌ | Promote Participant in Group | POST | |
```
✅ = Available
@ -130,7 +134,7 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
### App User Interface
1. Homepage ![Homepage](https://i.ibb.co/18f8vCz/homepage.png)
1. Homepage ![Homepage](https://i.ibb.co/TBNcFT0/homepage.png)
2. Login ![Login](https://i.ibb.co/jkcB15R/login.png)
3. Send Message ![Send Message](https://i.ibb.co/rc3NXMX/send-message.png?v1)
4. Send Image ![Send Image](https://i.ibb.co/BcFL3SD/send-image.png?v1)

4
src/config/settings.go

@ -6,7 +6,7 @@ import (
)
var (
AppVersion = "v4.11.1"
AppVersion = "v4.12.0"
AppPort = "3000"
AppDebug = false
AppOs = fmt.Sprintf("AldinoKemal")
@ -25,4 +25,6 @@ var (
WhatsappWebhook string
WhatsappSettingMaxFileSize int64 = 50000000 // 50MB
WhatsappSettingMaxVideoSize int64 = 100000000 // 100MB
WhatsappTypeUser = "@s.whatsapp.net"
WhatsappTypeGroup = "@g.us"
)

6
src/domains/group/group.go

@ -5,6 +5,7 @@ import "context"
type IGroupService interface {
JoinGroupWithLink(ctx context.Context, request JoinGroupWithLinkRequest) (groupID string, err error)
LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error)
CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error)
}
type JoinGroupWithLinkRequest struct {
@ -14,3 +15,8 @@ type JoinGroupWithLinkRequest struct {
type LeaveGroupRequest struct {
GroupID string `json:"group_id" form:"group_id"`
}
type CreateGroupRequest struct {
Title string `json:"title" form:"title"`
Participants []string `json:"participants" form:"participants"`
}

20
src/internal/rest/group.go

@ -1,6 +1,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/gofiber/fiber/v2"
@ -12,6 +13,7 @@ type Group struct {
func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group {
rest := Group{Service: service}
app.Post("/group", rest.CreateGroup)
app.Post("/group/join-with-link", rest.JoinGroupWithLink)
app.Post("/group/leave", rest.LeaveGroup)
return rest
@ -49,3 +51,21 @@ func (controller *Group) LeaveGroup(c *fiber.Ctx) error {
Message: "Success leave group",
})
}
func (controller *Group) CreateGroup(c *fiber.Ctx) error {
var request domainGroup.CreateGroupRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
groupID, err := controller.Service.CreateGroup(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: fmt.Sprintf("Success created group with id %s", groupID),
Results: map[string]string{
"group_id": groupID,
},
})
}

5
src/pkg/error/whatsapp_error.go

@ -71,6 +71,7 @@ func (e WaUploadMediaError) StatusCode() int {
}
const (
ErrInvalidJID = InvalidJID("your JID is invalid")
ErrWaCLI = WaCliError("your WhatsApp CLI is invalid or empty")
ErrInvalidJID = InvalidJID("your JID is invalid")
ErrUserNotRegistered = InvalidJID("user is not registered")
ErrWaCLI = WaCliError("your WhatsApp CLI is invalid or empty")
)

8
src/pkg/whatsapp/whatsapp.go

@ -55,9 +55,9 @@ type evtMessage struct {
func SanitizePhone(phone *string) {
if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") {
if len(*phone) <= 15 {
*phone = fmt.Sprintf("%s@s.whatsapp.net", *phone)
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeUser)
} else {
*phone = fmt.Sprintf("%s@g.us", *phone)
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeGroup)
}
}
}
@ -116,7 +116,7 @@ func ParseJID(arg string) (types.JID, error) {
}
}
func isOnWhatsapp(waCli *whatsmeow.Client, jid string) bool {
func IsOnWhatsapp(waCli *whatsmeow.Client, jid string) bool {
// only check if the jid a user with @s.whatsapp.net
if strings.Contains(jid, "@s.whatsapp.net") {
data, err := waCli.IsOnWhatsApp([]string{jid})
@ -137,7 +137,7 @@ func isOnWhatsapp(waCli *whatsmeow.Client, jid string) bool {
func ValidateJidWithLogin(waCli *whatsmeow.Client, jid string) (types.JID, error) {
MustLogin(waCli)
if !isOnWhatsapp(waCli, jid) {
if !IsOnWhatsapp(waCli, jid) {
return types.JID{}, pkgError.InvalidJID(fmt.Sprintf("Phone %s is not on whatsapp", jid))
}

37
src/services/group.go

@ -2,10 +2,13 @@ package services
import (
"context"
"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"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
)
type groupService struct {
@ -43,3 +46,37 @@ func (service groupService) LeaveGroup(ctx context.Context, request domainGroup.
return service.WaCli.LeaveGroup(JID)
}
func (service groupService) CreateGroup(ctx context.Context, request domainGroup.CreateGroupRequest) (groupID string, err error) {
if err = validations.ValidateCreateGroup(ctx, request); err != nil {
return groupID, err
}
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)
}
}
groupConfig := whatsmeow.ReqCreateGroup{
Name: request.Title,
Participants: participantsJID,
GroupParent: types.GroupParent{},
GroupLinkedParent: types.GroupLinkedParent{},
}
groupInfo, err := service.WaCli.CreateGroup(groupConfig)
if err != nil {
return
}
return groupInfo.JID.String(), nil
}

14
src/validations/group_validation.go

@ -30,3 +30,17 @@ func ValidateLeaveGroup(ctx context.Context, request domainGroup.LeaveGroupReque
return nil
}
func ValidateCreateGroup(ctx context.Context, request domainGroup.CreateGroupRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Title, 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
}

114
src/views/components/GroupCreate.js

@ -0,0 +1,114 @@
export default {
name: 'CreateGroup',
data() {
return {
loading: false,
title: '',
participants: ['', ''],
}
},
methods: {
openModal() {
$('#modalGroupCreate').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)
$('#modalGroupCreate').modal('hide');
} catch (err) {
showErrorInfo(err)
}
},
async submitApi() {
this.loading = true;
try {
let response = await window.http.post(`/group`, {
title: this.title,
// 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.title = '';
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">Create Groups</div>
<div class="description">
Add more friends to your group
</div>
</div>
</div>
<!-- Modal AccountGroup -->
<div class="ui small modal" id="modalGroupCreate">
<i class="close icon"></i>
<div class="header">
Create Group
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Group Name</label>
<input v-model="title" type="text"
placeholder="Group Name..."
aria-label="Group Name">
</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">
<small>You do not need to include yourself as participant. it will be automatically included.</small>
<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>
`
}

83
src/views/components/GroupJoinWithLink.js

@ -0,0 +1,83 @@
export default {
name: 'JoinGroupWithLink',
data() {
return {
loading: false,
link: '',
}
},
methods: {
openModal() {
$('#modalGroupJoinWithLink').modal({
onApprove: function () {
return false;
}
}).modal('show');
},
async handleSubmit() {
try {
let response = await this.submitApi()
showSuccessInfo(response)
$('#modalGroupJoinWithLink').modal('hide');
} catch (err) {
showErrorInfo(err)
}
},
async submitApi() {
this.loading = true;
try {
let response = await window.http.post(`/group/join-with-link`, {
link: this.link,
})
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.link = '';
},
},
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">Join Groups</div>
<div class="description">
Join group with invitation link
</div>
</div>
</div>
<!-- Modal AccountGroup -->
<div class="ui small modal" id="modalGroupJoinWithLink">
<i class="close icon"></i>
<div class="header">
Join Group With Link
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Invitation Link</label>
<input v-model="link" type="text"
placeholder="Invitation link..."
aria-label="Invitation Link">
</div>
</form>
</div>
<div class="actions">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.loading}"
@click="handleSubmit" type="button">
Join
<i class="send icon"></i>
</div>
</div>
</div>
`
}

11
src/views/components/AccountGroup.js → src/views/components/GroupList.js

@ -1,5 +1,5 @@
export default {
name: 'AccountGroup',
name: 'ListGroup',
data() {
return {
groups: []
@ -10,7 +10,7 @@ export default {
try {
this.dtClear()
await this.submitApi();
$('#modalUserGroup').modal('show');
$('#modalGroupList').modal('show');
this.dtRebuild()
showSuccessInfo("Groups fetched")
} catch (err) {
@ -72,15 +72,16 @@ export default {
template: `
<div class="green card" @click="openModal" style="cursor: pointer">
<div class="content">
<div class="header">My List Groups</div>
<a class="ui green right ribbon label">Group</a>
<div class="header">List Groups</div>
<div class="description">
Display all groups you joined
Display all your groups
</div>
</div>
</div>
<!-- Modal AccountGroup -->
<div class="ui small modal" id="modalUserGroup">
<div class="ui small modal" id="modalGroupList">
<i class="close icon"></i>
<div class="header">
My Group List

20
src/views/index.html

@ -73,14 +73,23 @@
<message-update></message-update>
</div>
<div class="ui horizontal divider">
Group
</div>
<div class="ui three column doubling grid cards">
<group-list></group-list>
<group-create></group-create>
<group-join-with-link></group-join-with-link>
</div>
<div class="ui horizontal divider">
Account
</div>
<div class="ui four column doubling grid cards">
<div class="ui three column doubling grid cards">
<account-avatar></account-avatar>
<account-user-info></account-user-info>
<account-group></account-group>
<account-privacy></account-privacy>
</div>
@ -129,9 +138,11 @@
import MessageUpdate from "./components/MessageUpdate.js";
import MessageReact from "./components/MessageReact.js";
import MessageRevoke from "./components/MessageRevoke.js";
import GroupList from "./components/GroupList.js";
import GroupCreate from "./components/GroupCreate.js";
import GroupJoinWithLink from "./components/GroupJoinWithLink.js";
import AccountAvatar from "./components/AccountAvatar.js";
import AccountUserInfo from "./components/AccountUserInfo.js";
import AccountGroup from "./components/AccountGroup.js";
import AccountPrivacy from "./components/AccountPrivacy.js";
const showErrorInfo = (message) => {
@ -158,7 +169,8 @@
AppLogin, AppLogout, AppReconnect,
SendMessage, SendImage, SendFile, SendVideo, SendContact, SendLocation, SendAudio, SendPoll,
MessageUpdate, MessageReact, MessageRevoke,
AccountAvatar, AccountUserInfo, AccountGroup, AccountPrivacy
GroupList, GroupCreate, GroupJoinWithLink,
AccountAvatar, AccountUserInfo, AccountPrivacy
},
delimiters: ['[[', ']]'],
data() {

Loading…
Cancel
Save