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. 8
      readme.md
  2. 4
      src/config/settings.go
  3. 6
      src/domains/group/group.go
  4. 20
      src/internal/rest/group.go
  5. 1
      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

8
readme.md

@ -100,7 +100,7 @@ You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API, furthermor
API using [openapi-generator](https://openapi-generator.tech/#try) 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 |
@ -122,6 +122,10 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
| ✅ | Edit Message | POST | /message/:message_id/update | | ✅ | Edit Message | POST | /message/:message_id/update |
| ✅ | 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/leave |
| ❌ | Add More Participants in Group | POST | |
| ❌ | Remove Participant in Group | POST | |
| ❌ | Promote Participant in Group | POST | |
``` ```
✅ = Available ✅ = Available
@ -130,7 +134,7 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
### App User Interface ### 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) 2. Login ![Login](https://i.ibb.co/jkcB15R/login.png)
3. Send Message ![Send Message](https://i.ibb.co/rc3NXMX/send-message.png?v1) 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. Send Image ![Send Image](https://i.ibb.co/BcFL3SD/send-image.png?v1)

4
src/config/settings.go

@ -6,7 +6,7 @@ import (
) )
var ( var (
AppVersion = "v4.11.1"
AppVersion = "v4.12.0"
AppPort = "3000" AppPort = "3000"
AppDebug = false AppDebug = false
AppOs = fmt.Sprintf("AldinoKemal") AppOs = fmt.Sprintf("AldinoKemal")
@ -25,4 +25,6 @@ var (
WhatsappWebhook string WhatsappWebhook string
WhatsappSettingMaxFileSize int64 = 50000000 // 50MB WhatsappSettingMaxFileSize int64 = 50000000 // 50MB
WhatsappSettingMaxVideoSize int64 = 100000000 // 100MB 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 { 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)
} }
type JoinGroupWithLinkRequest struct { type JoinGroupWithLinkRequest struct {
@ -14,3 +15,8 @@ type JoinGroupWithLinkRequest struct {
type LeaveGroupRequest struct { type LeaveGroupRequest struct {
GroupID string `json:"group_id" form:"group_id"` 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 package rest
import ( import (
"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/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -12,6 +13,7 @@ type Group struct {
func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group { func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group {
rest := Group{Service: service} rest := Group{Service: service}
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)
return rest return rest
@ -49,3 +51,21 @@ func (controller *Group) LeaveGroup(c *fiber.Ctx) error {
Message: "Success leave group", 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,
},
})
}

1
src/pkg/error/whatsapp_error.go

@ -72,5 +72,6 @@ func (e WaUploadMediaError) StatusCode() int {
const ( const (
ErrInvalidJID = InvalidJID("your JID is invalid") ErrInvalidJID = InvalidJID("your JID is invalid")
ErrUserNotRegistered = InvalidJID("user is not registered")
ErrWaCLI = WaCliError("your WhatsApp CLI is invalid or empty") 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) { func SanitizePhone(phone *string) {
if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") { if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") {
if len(*phone) <= 15 { if len(*phone) <= 15 {
*phone = fmt.Sprintf("%s@s.whatsapp.net", *phone)
*phone = fmt.Sprintf("%s%s", *phone, config.WhatsappTypeUser)
} else { } 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 // only check if the jid a user with @s.whatsapp.net
if strings.Contains(jid, "@s.whatsapp.net") { if strings.Contains(jid, "@s.whatsapp.net") {
data, err := waCli.IsOnWhatsApp([]string{jid}) 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) { func ValidateJidWithLogin(waCli *whatsmeow.Client, jid string) (types.JID, error) {
MustLogin(waCli) MustLogin(waCli)
if !isOnWhatsapp(waCli, jid) {
if !IsOnWhatsapp(waCli, jid) {
return types.JID{}, pkgError.InvalidJID(fmt.Sprintf("Phone %s is not on whatsapp", 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 ( import (
"context" "context"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group" 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/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations" "github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
) )
type groupService struct { type groupService struct {
@ -43,3 +46,37 @@ func (service groupService) LeaveGroup(ctx context.Context, request domainGroup.
return service.WaCli.LeaveGroup(JID) 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 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 { export default {
name: 'AccountGroup',
name: 'ListGroup',
data() { data() {
return { return {
groups: [] groups: []
@ -10,7 +10,7 @@ export default {
try { try {
this.dtClear() this.dtClear()
await this.submitApi(); await this.submitApi();
$('#modalUserGroup').modal('show');
$('#modalGroupList').modal('show');
this.dtRebuild() this.dtRebuild()
showSuccessInfo("Groups fetched") showSuccessInfo("Groups fetched")
} catch (err) { } catch (err) {
@ -72,15 +72,16 @@ export default {
template: ` template: `
<div class="green card" @click="openModal" style="cursor: pointer"> <div class="green card" @click="openModal" style="cursor: pointer">
<div class="content"> <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"> <div class="description">
Display all groups you joined
Display all your groups
</div> </div>
</div> </div>
</div> </div>
<!-- Modal AccountGroup --> <!-- Modal AccountGroup -->
<div class="ui small modal" id="modalUserGroup">
<div class="ui small 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

20
src/views/index.html

@ -73,14 +73,23 @@
<message-update></message-update> <message-update></message-update>
</div> </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"> <div class="ui horizontal divider">
Account Account
</div> </div>
<div class="ui four column doubling grid cards">
<div class="ui three column doubling grid cards">
<account-avatar></account-avatar> <account-avatar></account-avatar>
<account-user-info></account-user-info> <account-user-info></account-user-info>
<account-group></account-group>
<account-privacy></account-privacy> <account-privacy></account-privacy>
</div> </div>
@ -129,9 +138,11 @@
import MessageUpdate from "./components/MessageUpdate.js"; import MessageUpdate from "./components/MessageUpdate.js";
import MessageReact from "./components/MessageReact.js"; import MessageReact from "./components/MessageReact.js";
import MessageRevoke from "./components/MessageRevoke.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 AccountAvatar from "./components/AccountAvatar.js";
import AccountUserInfo from "./components/AccountUserInfo.js"; import AccountUserInfo from "./components/AccountUserInfo.js";
import AccountGroup from "./components/AccountGroup.js";
import AccountPrivacy from "./components/AccountPrivacy.js"; import AccountPrivacy from "./components/AccountPrivacy.js";
const showErrorInfo = (message) => { const showErrorInfo = (message) => {
@ -158,7 +169,8 @@
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,
AccountAvatar, AccountUserInfo, AccountGroup, AccountPrivacy
GroupList, GroupCreate, GroupJoinWithLink,
AccountAvatar, AccountUserInfo, AccountPrivacy
}, },
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
data() { data() {

Loading…
Cancel
Save