Browse Source

Merge branch 'main' into bug/large_groups

pull/254/head
almogbaku 1 year ago
parent
commit
8c019a90ef
No known key found for this signature in database GPG Key ID: 66C92B1C5B475512
  1. 4
      .github/workflows/release-linux.yml
  2. 2
      .github/workflows/release-mac.yml
  3. 2
      .github/workflows/release-windows.yml
  4. 2
      docker-compose.yml
  5. 8
      docker/golang.Dockerfile
  6. 47
      docs/openapi.yaml
  7. 5
      readme.md
  8. 22
      src/cmd/root.go
  9. 2
      src/config/settings.go
  10. 12
      src/domains/user/account.go
  11. 1
      src/domains/user/user.go
  12. 8
      src/go.mod
  13. 8
      src/go.sum
  14. 5
      src/internal/rest/message.go
  15. 13
      src/internal/rest/user.go
  16. 116
      src/internal/websocket/websocket.go
  17. 26
      src/pkg/utils/general.go
  18. 3
      src/pkg/utils/general_test.go
  19. 319
      src/pkg/whatsapp/init.go
  20. 12
      src/pkg/whatsapp/utils.go
  21. 2
      src/pkg/whatsapp/webhook.go
  22. 17
      src/services/send.go
  23. 18
      src/services/user.go
  24. 2
      src/views/components/AccountAvatar.js
  25. 2
      src/views/components/AccountChangeAvatar.js
  26. 79
      src/views/components/AccountContact.js
  27. 2
      src/views/components/AccountPrivacy.js
  28. 2
      src/views/components/AccountUserInfo.js
  29. 4
      src/views/index.html

4
.github/workflows/release-linux.yml

@ -18,7 +18,7 @@ jobs:
- name: Golang Installation - name: Golang Installation
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23'
go-version: '1.24'
- name: Golang build - name: Golang build
run: | run: |
cd src && go build -o linux-amd64 cd src && go build -o linux-amd64
@ -38,7 +38,7 @@ jobs:
- name: Golang Installation - name: Golang Installation
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23'
go-version: '1.24'
- name: Golang build - name: Golang build
run: | run: |
cd src && go build -o linux-arm64 cd src && go build -o linux-arm64

2
.github/workflows/release-mac.yml

@ -18,7 +18,7 @@ jobs:
- name: Golang Installation - name: Golang Installation
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23'
go-version: '1.24'
- name: Golang build - name: Golang build
run: | run: |
cd src && go build -o darwin-amd64 cd src && go build -o darwin-amd64

2
.github/workflows/release-windows.yml

@ -18,7 +18,7 @@ jobs:
- name: Golang Installation - name: Golang Installation
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23'
go-version: '1.24'
- name: Golang build - name: Golang build
run: | run: |
cd src && go build -o windows-amd64.exe cd src && go build -o windows-amd64.exe

2
docker-compose.yml

@ -8,7 +8,7 @@ services:
build: build:
context: . context: .
dockerfile: ./docker/golang.Dockerfile dockerfile: ./docker/golang.Dockerfile
restart: 'always'
restart: "on-failure"
ports: ports:
- "3000:3000" - "3000:3000"
env_file: env_file:

8
docker/golang.Dockerfile

@ -1,21 +1,21 @@
############################ ############################
# STEP 1 build executable binary # STEP 1 build executable binary
############################ ############################
FROM golang:1.23-alpine3.20 AS builder
FROM golang:1.24-alpine3.20 AS builder
RUN apk update && apk add --no-cache gcc musl-dev gcompat RUN apk update && apk add --no-cache gcc musl-dev gcompat
WORKDIR /whatsapp WORKDIR /whatsapp
COPY ./src . COPY ./src .
# Fetch dependencies. # Fetch dependencies.
RUN go mod download RUN go mod download
# Build the binary.
RUN go build -o /app/whatsapp
# Build the binary with optimizations
RUN go build -a -ldflags="-w -s" -o /app/whatsapp
############################# #############################
## STEP 2 build a smaller image ## STEP 2 build a smaller image
############################# #############################
FROM alpine:3.20 FROM alpine:3.20
RUN apk update && apk add --no-cache ffmpeg
RUN apk add --no-cache ffmpeg
WORKDIR /app WORKDIR /app
# Copy compiled from builder. # Copy compiled from builder.
COPY --from=builder /app/whatsapp /app/whatsapp COPY --from=builder /app/whatsapp /app/whatsapp

47
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.1.0
version: 5.2.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
@ -282,6 +282,26 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorInternalServer' $ref: '#/components/schemas/ErrorInternalServer'
/user/my/contacts:
get:
operationId: userMyContacts
tags:
- user
summary: Get list of user contacts
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MyListContactsResponse'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/send/message: /send/message:
post: post:
operationId: sendMessage operationId: sendMessage
@ -1646,6 +1666,31 @@ components:
role: role:
type: string type: string
example: "subscriber" example: "subscriber"
MyListContactsResponse:
type: object
properties:
code:
type: string
example: "SUCCESS"
message:
type: string
example: "Success get list contacts"
results:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/MyListContacts'
MyListContacts:
type: object
properties:
jid:
type: string
example: "628123123123123@s.whatsapp.net"
name:
type: string
example: "Aldino Kemal"
GroupResponse: GroupResponse:
type: object type: object
properties: properties:

5
readme.md

@ -38,7 +38,8 @@ Now that we support ARM64 for Linux:
- `--webhook="http://yourwebhook.site/handler"`, or you can simplify - `--webhook="http://yourwebhook.site/handler"`, or you can simplify
- `-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`.<br>
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"`
@ -53,6 +54,7 @@ You can configure the application using either command-line flags (shown above)
### Environment Variables ### Environment Variables
To use environment variables: To use environment variables:
1. Copy `.env.example` to `.env` in your project root 1. Copy `.env.example` to `.env` in your project root
2. Modify the values in `.env` according to your needs 2. Modify the values in `.env` according to your needs
3. Or set the same variables as system environment variables 3. Or set the same variables as system environment variables
@ -187,6 +189,7 @@ You can fork or edit this source code !
| ✅ | User My Groups | GET | /user/my/groups | | ✅ | User My Groups | GET | /user/my/groups |
| ✅ | User My Newsletter | GET | /user/my/newsletters | | ✅ | User My Newsletter | GET | /user/my/newsletters |
| ✅ | User My Privacy Setting | GET | /user/my/privacy | | ✅ | User My Privacy Setting | GET | /user/my/privacy |
| ✅ | User My Contacts | GET | /user/my/contacts |
| ✅ | Send Message | POST | /send/message | | ✅ | Send Message | POST | /send/message |
| ✅ | Send Image | POST | /send/image | | ✅ | Send Image | POST | /send/image |
| ✅ | Send Audio | POST | /send/audio | | ✅ | Send Audio | POST | /send/audio |

22
src/cmd/root.go

@ -3,7 +3,8 @@ package cmd
import ( import (
"embed" "embed"
"fmt" "fmt"
"go.mau.fi/whatsmeow/store/sqlstore"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
waLog "go.mau.fi/whatsmeow/util/log"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -240,12 +241,21 @@ func runRest(_ *cobra.Command, _ []string) {
})) }))
} }
db := whatsapp.InitWaDB(config.DBURI)
var db_keys *sqlstore.Container
if config.DBKeysURI != "" {
db_keys = whatsapp.InitWaDB(config.DBKeysURI)
waLogger := waLog.Stdout("Main", config.WhatsappLogLevel, true)
dbLogger := waLog.Stdout("Database", config.WhatsappLogLevel, true)
db, err := whatsapp.InitDatabase(config.DBURI, dbLogger)
if err != nil {
waLogger.Errorf("Database initialization error: %v", err)
panic(pkgError.InternalServerError(fmt.Sprintf("Database initialization error: %v", err)))
}
dbKeys, err := whatsapp.InitDatabase(config.DBURI, dbLogger)
if err != nil {
waLogger.Errorf("Database initialization error: %v", err)
panic(pkgError.InternalServerError(fmt.Sprintf("Database initialization error: %v", err)))
} }
cli := whatsapp.InitWaCLI(db, db_keys)
cli := whatsapp.InitWaCLI(db, dbKeys)
// Service // Service
appService := services.NewAppService(cli, db) appService := services.NewAppService(cli, db)

2
src/config/settings.go

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

12
src/domains/user/account.go

@ -1,8 +1,9 @@
package user package user
import ( import (
"go.mau.fi/whatsmeow/types"
"mime/multipart" "mime/multipart"
"go.mau.fi/whatsmeow/types"
) )
type InfoRequest struct { type InfoRequest struct {
@ -59,3 +60,12 @@ type MyListNewsletterResponse struct {
type ChangeAvatarRequest struct { type ChangeAvatarRequest struct {
Avatar *multipart.FileHeader `json:"avatar" form:"avatar"` Avatar *multipart.FileHeader `json:"avatar" form:"avatar"`
} }
type MyListContactsResponse struct {
Data []MyListContactsResponseData `json:"data"`
}
type MyListContactsResponseData struct {
JID types.JID `json:"jid"`
Name string `json:"name"`
}

1
src/domains/user/user.go

@ -11,4 +11,5 @@ type IUserService interface {
MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error) MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error)
MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error) MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error)
MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error) MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error)
MyListContacts(ctx context.Context) (response MyListContactsResponse, err error)
} }

8
src/go.mod

@ -1,8 +1,6 @@
module github.com/aldinokemal/go-whatsapp-web-multidevice module github.com/aldinokemal/go-whatsapp-web-multidevice
go 1.23.0
toolchain go1.23.6
go 1.24.0
require ( require (
github.com/PuerkitoBio/goquery v1.10.2 github.com/PuerkitoBio/goquery v1.10.2
@ -22,7 +20,7 @@ require (
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/valyala/fasthttp v1.59.0 github.com/valyala/fasthttp v1.59.0
go.mau.fi/libsignal v0.1.2 go.mau.fi/libsignal v0.1.2
go.mau.fi/whatsmeow v0.0.0-20250216151842-97deed8f95f7
go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
) )
@ -60,7 +58,7 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
go.mau.fi/util v0.8.5 // indirect go.mau.fi/util v0.8.5 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/image v0.24.0 // indirect golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect

8
src/go.sum

@ -126,8 +126,8 @@ go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE= go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI= go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI=
go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M=
go.mau.fi/whatsmeow v0.0.0-20250216151842-97deed8f95f7 h1:K5Vgr6PUguay1zaiRwsd1St+1PUSAxk56CI71jJitXA=
go.mau.fi/whatsmeow v0.0.0-20250216151842-97deed8f95f7/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U=
go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056 h1:1JQUOpYXhFSEQgXMEWD/ZH38FrIe5i1yjxSBwa0aN/Q=
go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -136,8 +136,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

5
src/internal/rest/message.go

@ -1,7 +1,6 @@
package rest package rest
import ( import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
"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/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
@ -82,7 +81,7 @@ func (controller *Message) UpdateMessage(c *fiber.Ctx) error {
} }
func (controller *Message) ReactMessage(c *fiber.Ctx) error { func (controller *Message) ReactMessage(c *fiber.Ctx) error {
var request message.ReactionRequest
var request domainMessage.ReactionRequest
err := c.BodyParser(&request) err := c.BodyParser(&request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -101,7 +100,7 @@ func (controller *Message) ReactMessage(c *fiber.Ctx) error {
} }
func (controller *Message) MarkAsRead(c *fiber.Ctx) error { func (controller *Message) MarkAsRead(c *fiber.Ctx) error {
var request message.MarkAsReadRequest
var request domainMessage.MarkAsReadRequest
err := c.BodyParser(&request) err := c.BodyParser(&request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)

13
src/internal/rest/user.go

@ -19,6 +19,7 @@ func InitRestUser(app *fiber.App, service domainUser.IUserService) User {
app.Get("/user/my/privacy", rest.UserMyPrivacySetting) app.Get("/user/my/privacy", rest.UserMyPrivacySetting)
app.Get("/user/my/groups", rest.UserMyListGroups) app.Get("/user/my/groups", rest.UserMyListGroups)
app.Get("/user/my/newsletters", rest.UserMyListNewsletter) app.Get("/user/my/newsletters", rest.UserMyListNewsletter)
app.Get("/user/my/contacts", rest.UserMyListContacts)
return rest return rest
} }
@ -112,3 +113,15 @@ func (controller *User) UserMyListNewsletter(c *fiber.Ctx) error {
Results: response, Results: response,
}) })
} }
func (controller *User) UserMyListContacts(c *fiber.Ctx) error {
response, err := controller.Service.MyListContacts(c.UserContext())
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success get list contacts",
Results: response,
})
}

116
src/internal/websocket/websocket.go

@ -3,102 +3,111 @@ package websocket
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"log"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2" "github.com/gofiber/websocket/v2"
"log"
) )
type client struct{} // Add more data to this type if needed
type client struct{}
type BroadcastMessage struct { type BroadcastMessage struct {
Code string `json:"code"` Code string `json:"code"`
Message string `json:"message"` Message string `json:"message"`
Result any `json:"result"` Result any `json:"result"`
} }
var Clients = make(map[*websocket.Conn]client) // Note: although large maps with pointer-like types (e.g. strings) as keys are slow, using pointers themselves as keys is acceptable and fast
var Register = make(chan *websocket.Conn)
var Broadcast = make(chan BroadcastMessage)
var Unregister = make(chan *websocket.Conn)
var (
Clients = make(map[*websocket.Conn]client)
Register = make(chan *websocket.Conn)
Broadcast = make(chan BroadcastMessage)
Unregister = make(chan *websocket.Conn)
)
func handleRegister(conn *websocket.Conn) {
Clients[conn] = client{}
log.Println("connection registered")
}
func handleUnregister(conn *websocket.Conn) {
delete(Clients, conn)
log.Println("connection unregistered")
}
func broadcastMessage(message BroadcastMessage) {
marshalMessage, err := json.Marshal(message)
if err != nil {
log.Println("marshal error:", err)
return
}
for conn := range Clients {
if err := conn.WriteMessage(websocket.TextMessage, marshalMessage); err != nil {
log.Println("write error:", err)
closeConnection(conn)
}
}
}
func closeConnection(conn *websocket.Conn) {
if err := conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil {
log.Println("write close message error:", err)
}
if err := conn.Close(); err != nil {
log.Println("close connection error:", err)
}
delete(Clients, conn)
}
func RunHub() { func RunHub() {
for { for {
select { select {
case connection := <-Register:
Clients[connection] = client{}
log.Println("connection registered")
case conn := <-Register:
handleRegister(conn)
case conn := <-Unregister:
handleUnregister(conn)
case message := <-Broadcast: case message := <-Broadcast:
log.Println("message received:", message) log.Println("message received:", message)
marshalMessage, err := json.Marshal(message)
if err != nil {
log.Println("write error:", err)
return
}
// Send the message to all clients
for connection := range Clients {
if err := connection.WriteMessage(websocket.TextMessage, marshalMessage); err != nil {
log.Println("write error:", err)
err := connection.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
log.Println("write message close error:", err)
return
}
err = connection.Close()
if err != nil {
log.Println("close error:", err)
return
}
delete(Clients, connection)
}
}
case connection := <-Unregister:
// Remove the client from the hub
delete(Clients, connection)
log.Println("connection unregistered")
broadcastMessage(message)
} }
} }
} }
func RegisterRoutes(app *fiber.App, service domainApp.IAppService) { func RegisterRoutes(app *fiber.App, service domainApp.IAppService) {
app.Use("/ws", func(c *fiber.Ctx) error { app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) { // Returns true if the client requested upgrade to the WebSocket protocol
if websocket.IsWebSocketUpgrade(c) {
return c.Next() return c.Next()
} }
return c.SendStatus(fiber.StatusUpgradeRequired) return c.SendStatus(fiber.StatusUpgradeRequired)
}) })
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
// When the function returns, unregister the client and close the connection
app.Get("/ws", websocket.New(func(conn *websocket.Conn) {
defer func() { defer func() {
Unregister <- c
_ = c.Close()
Unregister <- conn
_ = conn.Close()
}() }()
// Register the client
Register <- c
Register <- conn
for { for {
messageType, message, err := c.ReadMessage()
messageType, message, err := conn.ReadMessage()
if err != nil { if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Println("read error:", err) log.Println("read error:", err)
} }
return // Calls the deferred function, i.e. closes the connection on error
return
} }
if messageType == websocket.TextMessage { if messageType == websocket.TextMessage {
// Broadcast the received message
var messageData BroadcastMessage var messageData BroadcastMessage
err := json.Unmarshal(message, &messageData)
if err != nil {
log.Println("error unmarshal message:", err)
if err := json.Unmarshal(message, &messageData); err != nil {
log.Println("unmarshal error:", err)
return return
} }
if messageData.Code == "FETCH_DEVICES" { if messageData.Code == "FETCH_DEVICES" {
devices, _ := service.FetchDevices(context.Background()) devices, _ := service.FetchDevices(context.Background())
Broadcast <- BroadcastMessage{ Broadcast <- BroadcastMessage{
@ -107,9 +116,8 @@ func RegisterRoutes(app *fiber.App, service domainApp.IAppService) {
Result: devices, Result: devices,
} }
} }
} else { } else {
log.Println("websocket message received of type", messageType)
log.Println("unsupported message type:", messageType)
} }
} }
})) }))

26
src/pkg/utils/general.go

@ -69,20 +69,22 @@ type Metadata struct {
Description string Description string
Image string Image string
ImageThumb []byte ImageThumb []byte
Height *uint32
Width *uint32
} }
func GetMetaDataFromURL(url string) (meta Metadata) {
func GetMetaDataFromURL(url string) (meta Metadata, err error) {
// Send an HTTP GET request to the website // Send an HTTP GET request to the website
response, err := http.Get(url) response, err := http.Get(url)
if err != nil { if err != nil {
log.Fatal(err)
return meta, err
} }
defer response.Body.Close() defer response.Body.Close()
// Parse the HTML document // Parse the HTML document
document, err := goquery.NewDocumentFromReader(response.Body) document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil { if err != nil {
log.Fatal(err)
return meta, err
} }
document.Find("meta[name='description']").Each(func(index int, element *goquery.Selection) { document.Find("meta[name='description']").Each(func(index int, element *goquery.Selection) {
@ -98,6 +100,22 @@ func GetMetaDataFromURL(url string) (meta Metadata) {
meta.Image, _ = element.Attr("content") meta.Image, _ = element.Attr("content")
}) })
document.Find("meta[property='og:image:width']").Each(func(index int, element *goquery.Selection) {
if content, exists := element.Attr("content"); exists {
width, _ := strconv.Atoi(content)
widthUint32 := uint32(width)
meta.Width = &widthUint32
}
})
document.Find("meta[property='og:image:height']").Each(func(index int, element *goquery.Selection) {
if content, exists := element.Attr("content"); exists {
height, _ := strconv.Atoi(content)
heightUint32 := uint32(height)
meta.Height = &heightUint32
}
})
// If an og:image is found, download it and store its content in ImageThumb // If an og:image is found, download it and store its content in ImageThumb
if meta.Image != "" { if meta.Image != "" {
imageResponse, err := http.Get(meta.Image) imageResponse, err := http.Get(meta.Image)
@ -114,7 +132,7 @@ func GetMetaDataFromURL(url string) (meta Metadata) {
} }
} }
return meta
return meta, nil
} }
// ContainsMention is checking if message contains mention, then return only mention without @ // ContainsMention is checking if message contains mention, then return only mention without @

3
src/pkg/utils/general_test.go

@ -93,7 +93,8 @@ func (suite *UtilsTestSuite) TestGetMetaDataFromURL() {
})) }))
defer server.Close() // Ensure the server is closed when the test ends defer server.Close() // Ensure the server is closed when the test ends
meta := utils.GetMetaDataFromURL(server.URL)
meta, err := utils.GetMetaDataFromURL(server.URL)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), "Test Title", meta.Title) assert.Equal(suite.T(), "Test Title", meta.Title)
assert.Equal(suite.T(), "Test Description", meta.Description) assert.Equal(suite.T(), "Test Description", meta.Description)
assert.Equal(suite.T(), "http://example.com/image.jpg", meta.Image) assert.Equal(suite.T(), "http://example.com/image.jpg", meta.Image)

319
src/pkg/whatsapp/init.go

@ -11,7 +11,6 @@ import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" "github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket" "github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
@ -25,12 +24,7 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
var (
cli *whatsmeow.Client
log waLog.Logger
historySyncID int32
startupTime = time.Now().Unix()
)
// Type definitions
type ExtractedMedia struct { type ExtractedMedia struct {
MediaPath string `json:"media_path"` MediaPath string `json:"media_path"`
@ -50,29 +44,26 @@ type evtMessage struct {
QuotedMessage string `json:"quoted_message,omitempty"` QuotedMessage string `json:"quoted_message,omitempty"`
} }
func InitWaDB(db_uri string) *sqlstore.Container {
// Running Whatsapp
log = waLog.Stdout("Main", config.WhatsappLogLevel, true)
dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true)
// Global variables
var (
cli *whatsmeow.Client
log waLog.Logger
historySyncID int32
startupTime = time.Now().Unix()
)
var storeContainer *sqlstore.Container
var err error
if strings.HasPrefix(db_uri, "file:") {
storeContainer, err = sqlstore.New("sqlite3", db_uri, dbLog)
} else if strings.HasPrefix(db_uri, "postgres:") {
storeContainer, err = sqlstore.New("postgres", db_uri, dbLog)
} else {
log.Errorf("Unknown database type: %s", db_uri)
panic(pkgError.InternalServerError(fmt.Sprintf("Unknown database type: %s. Currently only sqlite3(file:) and postgres are supported", db_uri)))
// InitDatabase creates and returns a database store container based on the configured URI
func InitDatabase(dbUri string, dbLog waLog.Logger) (*sqlstore.Container, error) {
if strings.HasPrefix(dbUri, "file:") {
return sqlstore.New("sqlite3", dbUri, dbLog)
} else if strings.HasPrefix(dbUri, "postgres:") {
return sqlstore.New("postgres", dbUri, dbLog)
} }
if err != nil {
log.Errorf("Failed to connect to database: %v", err)
panic(pkgError.InternalServerError(fmt.Sprintf("Failed to connect to database: %v", err)))
}
return storeContainer
return nil, fmt.Errorf("unknown database type: %s. Currently only sqlite3(file:) and postgres are supported", dbUri)
} }
// InitWaCLI initializes the WhatsApp client
func InitWaCLI(storeContainer, keysStoreContainer *sqlstore.Container) *whatsmeow.Client { func InitWaCLI(storeContainer, keysStoreContainer *sqlstore.Container) *whatsmeow.Client {
device, err := storeContainer.GetFirstDevice() device, err := storeContainer.GetFirstDevice()
if err != nil { if err != nil {
@ -85,10 +76,12 @@ func InitWaCLI(storeContainer, keysStoreContainer *sqlstore.Container) *whatsmeo
panic("No device found") panic("No device found")
} }
// Configure device properties
osName := fmt.Sprintf("%s %s", config.AppOs, config.AppVersion) osName := fmt.Sprintf("%s %s", config.AppOs, config.AppVersion)
store.DeviceProps.PlatformType = &config.AppPlatform store.DeviceProps.PlatformType = &config.AppPlatform
store.DeviceProps.Os = &osName store.DeviceProps.Os = &osName
// Configure a separated database for accelerating encryption caching
if keysStoreContainer != nil && device.ID != nil { if keysStoreContainer != nil && device.ID != nil {
innerStore := sqlstore.NewSQLStore(keysStoreContainer, *device.ID) innerStore := sqlstore.NewSQLStore(keysStoreContainer, *device.ID)
device.Identities = innerStore device.Identities = innerStore
@ -99,6 +92,7 @@ func InitWaCLI(storeContainer, keysStoreContainer *sqlstore.Container) *whatsmeo
device.PrivacyTokens = innerStore device.PrivacyTokens = innerStore
} }
// Create and configure the client
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true)) cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true))
cli.EnableAutoReconnect = true cli.EnableAutoReconnect = true
cli.AutoTrustIdentity = true cli.AutoTrustIdentity = true
@ -107,120 +101,205 @@ func InitWaCLI(storeContainer, keysStoreContainer *sqlstore.Container) *whatsmeo
return cli return cli
} }
// handler is the main event handler for WhatsApp events
func handler(rawEvt interface{}) { func handler(rawEvt interface{}) {
switch evt := rawEvt.(type) { switch evt := rawEvt.(type) {
case *events.DeleteForMe: case *events.DeleteForMe:
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String())
handleDeleteForMe(evt)
case *events.AppStateSyncComplete: case *events.AppStateSyncComplete:
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("Marked self as available")
}
}
handleAppStateSyncComplete(evt)
case *events.PairSuccess: case *events.PairSuccess:
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LOGIN_SUCCESS",
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()),
}
handlePairSuccess(evt)
case *events.LoggedOut: case *events.LoggedOut:
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LIST_DEVICES",
Result: nil,
}
handleLoggedOut()
case *events.Connected, *events.PushNameSetting: case *events.Connected, *events.PushNameSetting:
if len(cli.Store.PushName) == 0 {
return
}
handleConnectionEvents()
case *events.StreamReplaced:
handleStreamReplaced()
case *events.Message:
handleMessage(evt)
case *events.Receipt:
handleReceipt(evt)
case *events.Presence:
handlePresence(evt)
case *events.HistorySync:
handleHistorySync(evt)
case *events.AppState:
handleAppState(evt)
}
}
// Event handler functions
func handleDeleteForMe(evt *events.DeleteForMe) {
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String())
}
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
func handleAppStateSyncComplete(evt *events.AppStateSyncComplete) {
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
if err := cli.SendPresence(types.PresenceAvailable); err != nil {
log.Warnf("Failed to send available presence: %v", err) log.Warnf("Failed to send available presence: %v", err)
} else { } else {
log.Infof("Marked self as available") log.Infof("Marked self as available")
} }
case *events.StreamReplaced:
os.Exit(0)
case *events.Message:
metaParts := []string{
fmt.Sprintf("pushname: %s", evt.Info.PushName),
fmt.Sprintf("timestamp: %s", evt.Info.Timestamp),
}
if evt.Info.Type != "" {
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
}
if evt.Info.Category != "" {
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
}
if evt.IsViewOnce {
metaParts = append(metaParts, "view once")
}
}
}
log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message)
message := ExtractMessageText(evt)
utils.RecordMessage(evt.Info.ID, evt.Info.Sender.String(), message)
func handlePairSuccess(evt *events.PairSuccess) {
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LOGIN_SUCCESS",
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()),
}
}
if img := evt.Message.GetImageMessage(); img != nil {
if path, err := ExtractMedia(config.PathStorages, img); err != nil {
log.Errorf("Failed to download image: %v", err)
} else {
log.Infof("Image downloaded to %s", path)
}
}
func handleLoggedOut() {
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LIST_DEVICES",
Result: nil,
}
}
if config.WhatsappAutoReplyMessage != "" &&
!isGroupJid(evt.Info.Chat.String()) &&
!strings.Contains(evt.Info.SourceString(), "broadcast") {
_, _ = cli.SendMessage(context.Background(), evt.Info.Sender, &waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)})
}
func handleConnectionEvents() {
if len(cli.Store.PushName) == 0 {
return
}
if len(config.WhatsappWebhook) > 0 &&
!strings.Contains(evt.Info.SourceString(), "broadcast") &&
!isFromMySelf(evt.Info.SourceString()) {
go func(evt *events.Message) {
if err := forwardToWebhook(evt); err != nil {
logrus.Error("Failed forward to webhook: ", err)
}
}(evt)
}
case *events.Receipt:
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == types.ReceiptTypeDelivered {
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp)
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
if err := cli.SendPresence(types.PresenceAvailable); err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("Marked self as available")
}
}
func handleStreamReplaced() {
os.Exit(0)
}
func handleMessage(evt *events.Message) {
// Log message metadata
metaParts := buildMessageMetaParts(evt)
log.Infof("Received message %s from %s (%s): %+v",
evt.Info.ID,
evt.Info.SourceString(),
strings.Join(metaParts, ", "),
evt.Message,
)
// Record the message
message := ExtractMessageText(evt)
utils.RecordMessage(evt.Info.ID, evt.Info.Sender.String(), message)
// Handle image message if present
handleImageMessage(evt)
// Handle auto-reply if configured
handleAutoReply(evt)
// Forward to webhook if configured
handleWebhookForward(evt)
}
func buildMessageMetaParts(evt *events.Message) []string {
metaParts := []string{
fmt.Sprintf("pushname: %s", evt.Info.PushName),
fmt.Sprintf("timestamp: %s", evt.Info.Timestamp),
}
if evt.Info.Type != "" {
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
}
if evt.Info.Category != "" {
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
}
if evt.IsViewOnce {
metaParts = append(metaParts, "view once")
}
return metaParts
}
func handleImageMessage(evt *events.Message) {
if img := evt.Message.GetImageMessage(); img != nil {
if path, err := ExtractMedia(config.PathStorages, img); err != nil {
log.Errorf("Failed to download image: %v", err)
} else {
log.Infof("Image downloaded to %s", path)
} }
case *events.Presence:
if evt.Unavailable {
if evt.LastSeen.IsZero() {
log.Infof("%s is now offline", evt.From)
} else {
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen)
}
}
func handleAutoReply(evt *events.Message) {
if config.WhatsappAutoReplyMessage != "" &&
!isGroupJid(evt.Info.Chat.String()) &&
!evt.Info.IsIncomingBroadcast() &&
evt.Message.GetExtendedTextMessage().GetText() != "" {
_, _ = cli.SendMessage(
context.Background(),
FormatJID(evt.Info.Sender.String()),
&waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)},
)
}
}
func handleWebhookForward(evt *events.Message) {
if len(config.WhatsappWebhook) > 0 &&
!strings.Contains(evt.Info.SourceString(), "broadcast") &&
!isFromMySelf(evt.Info.SourceString()) {
go func(evt *events.Message) {
if err := forwardToWebhook(evt); err != nil {
logrus.Error("Failed forward to webhook: ", err)
} }
}(evt)
}
}
func handleReceipt(evt *events.Receipt) {
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == types.ReceiptTypeDelivered {
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp)
}
}
func handlePresence(evt *events.Presence) {
if evt.Unavailable {
if evt.LastSeen.IsZero() {
log.Infof("%s is now offline", evt.From)
} else { } else {
log.Infof("%s is now online", evt.From)
}
case *events.HistorySync:
id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json", config.PathStorages, startupTime, cli.Store.ID.String(), id, evt.Data.SyncType.String())
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Errorf("Failed to open file to write history sync: %v", err)
return
}
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
err = enc.Encode(evt.Data)
if err != nil {
log.Errorf("Failed to write history sync: %v", err)
return
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen)
} }
log.Infof("Wrote history sync to %s", fileName)
_ = file.Close()
case *events.AppState:
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
} else {
log.Infof("%s is now online", evt.From)
}
}
func handleHistorySync(evt *events.HistorySync) {
id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json",
config.PathStorages,
startupTime,
cli.Store.ID.String(),
id,
evt.Data.SyncType.String(),
)
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Errorf("Failed to open file to write history sync: %v", err)
return
}
defer file.Close()
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
if err = enc.Encode(evt.Data); err != nil {
log.Errorf("Failed to write history sync: %v", err)
return
} }
log.Infof("Wrote history sync to %s", fileName)
}
func handleAppState(evt *events.AppState) {
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
} }

12
src/pkg/whatsapp/utils.go

@ -177,6 +177,18 @@ func MustLogin(waCli *whatsmeow.Client) {
} }
} }
func FormatJID(jid string) types.JID {
// Remove any :number suffix if present
if idx := strings.LastIndex(jid, ":"); idx != -1 && strings.Contains(jid, "@s.whatsapp.net") {
jid = jid[:idx] + jid[strings.Index(jid, "@s.whatsapp.net"):]
}
formattedJID, err := ParseJID(jid)
if err != nil {
return types.JID{}
}
return formattedJID
}
// isGroupJid is a helper function to check if the message is from group // isGroupJid is a helper function to check if the message is from group
func isGroupJid(jid string) bool { func isGroupJid(jid string) bool {
return strings.Contains(jid, "@g.us") return strings.Contains(jid, "@g.us")

2
src/pkg/whatsapp/webhook.go

@ -41,7 +41,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
if from := evt.Info.SourceString(); from != "" { if from := evt.Info.SourceString(); from != "" {
body["from"] = from body["from"] = from
} }
if message.Text != "" {
if message.ID != "" {
body["message"] = message body["message"] = message
} }
if pushname := evt.Info.PushName; pushname != "" { if pushname := evt.Info.PushName; pushname != "" {

17
src/services/send.go

@ -430,14 +430,19 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link
return response, err return response, err
} }
getMetaDataFromURL := utils.GetMetaDataFromURL(request.Link)
getMetaDataFromURL, err := utils.GetMetaDataFromURL(request.Link)
if err != nil {
return response, err
}
msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{ msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)),
Title: proto.String(getMetaDataFromURL.Title),
MatchedText: proto.String(request.Link),
Description: proto.String(getMetaDataFromURL.Description),
JPEGThumbnail: getMetaDataFromURL.ImageThumb,
Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)),
Title: proto.String(getMetaDataFromURL.Title),
MatchedText: proto.String(request.Link),
Description: proto.String(getMetaDataFromURL.Description),
JPEGThumbnail: getMetaDataFromURL.ImageThumb,
ThumbnailHeight: getMetaDataFromURL.Height,
ThumbnailWidth: getMetaDataFromURL.Width,
}} }}
content := "🔗 " + request.Link content := "🔗 " + request.Link

18
src/services/user.go

@ -160,6 +160,24 @@ func (service userService) MyPrivacySetting(_ context.Context) (response domainU
return response, nil return response, nil
} }
func (service userService) MyListContacts(ctx context.Context) (response domainUser.MyListContactsResponse, err error) {
whatsapp.MustLogin(service.WaCli)
contacts, err := service.WaCli.Store.Contacts.GetAllContacts()
if err != nil {
return
}
for jid, contact := range contacts {
response.Data = append(response.Data, domainUser.MyListContactsResponseData{
JID: jid,
Name: contact.FullName,
})
}
return response, nil
}
func (service userService) ChangeAvatar(ctx context.Context, request domainUser.ChangeAvatarRequest) (err error) { func (service userService) ChangeAvatar(ctx context.Context, request domainUser.ChangeAvatarRequest) (err error) {
whatsapp.MustLogin(service.WaCli) whatsapp.MustLogin(service.WaCli)

2
src/views/components/AccountAvatar.js

@ -65,7 +65,7 @@ export default {
} }
}, },
template: ` template: `
<div class="green card" @click="openModal" style="cursor: pointer;">
<div class="olive card" @click="openModal" style="cursor: pointer;">
<div class="content"> <div class="content">
<a class="ui olive right ribbon label">Account</a> <a class="ui olive right ribbon label">Account</a>
<div class="header">Avatar</div> <div class="header">Avatar</div>

2
src/views/components/AccountChangeAvatar.js

@ -63,7 +63,7 @@ export default {
} }
}, },
template: ` template: `
<div class="blue card" @click="openModal()" style="cursor:pointer;">
<div class="olive card" @click="openModal()" style="cursor:pointer;">
<div class="content"> <div class="content">
<a class="ui olive right ribbon label">Account</a> <a class="ui olive right ribbon label">Account</a>
<div class="header">Change Avatar</div> <div class="header">Change Avatar</div>

79
src/views/components/AccountContact.js

@ -0,0 +1,79 @@
export default {
name: 'AccountContact',
data() {
return {
contacts: []
}
},
methods: {
async openModal() {
try {
this.dtClear()
await this.submitApi();
$('#modalContactList').modal('show');
this.dtRebuild()
showSuccessInfo("Contacts fetched")
} catch (err) {
showErrorInfo(err)
}
},
dtClear() {
$('#account_contacts_table').DataTable().destroy();
},
dtRebuild() {
$('#account_contacts_table').DataTable({
"pageLength": 10,
"reloadData": true,
}).draw();
},
async submitApi() {
try {
let response = await window.http.get(`/user/my/contacts`)
this.contacts = response.data.results.data;
} catch (error) {
if (error.response) {
throw new Error(error.response.data.message);
}
throw new Error(error.message);
}
},
getPhoneNumber(jid) {
return jid.split('@')[0];
}
},
template: `
<div class="olive card" @click="openModal" style="cursor: pointer">
<div class="content">
<a class="ui olive right ribbon label">Contacts</a>
<div class="header">My Contacts</div>
<div class="description">
Display all your contacts
</div>
</div>
</div>
<!-- Modal Contact List -->
<div class="ui large modal" id="modalContactList">
<i class="close icon"></i>
<div class="header">
My Contacts
</div>
<div class="content">
<table class="ui celled table" id="account_contacts_table">
<thead>
<tr>
<th>Phone Number</th>
<th>Name</th>
</tr>
</thead>
<tbody v-if="contacts != null">
<tr v-for="contact in contacts">
<td>{{ getPhoneNumber(contact.jid) }}</td>
<td>{{ contact.name }}</td>
</tr>
</tbody>
</table>
</div>
</div>
`
}

2
src/views/components/AccountPrivacy.js

@ -28,7 +28,7 @@ export default {
}, },
}, },
template: ` template: `
<div class="green card" @click="openModal" style="cursor: pointer">
<div class="olive card" @click="openModal" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui olive right ribbon label">Account</a> <a class="ui olive right ribbon label">Account</a>
<div class="header">My Privacy Setting</div> <div class="header">My Privacy Setting</div>

2
src/views/components/AccountUserInfo.js

@ -72,7 +72,7 @@ export default {
} }
}, },
template: ` template: `
<div class="green card" @click="openModal" style="cursor: pointer;">
<div class="olive card" @click="openModal" style="cursor: pointer;">
<div class="content"> <div class="content">
<a class="ui olive right ribbon label">Account</a> <a class="ui olive right ribbon label">Account</a>
<div class="header">User Info</div> <div class="header">User Info</div>

4
src/views/index.html

@ -157,6 +157,7 @@
<account-change-avatar></account-change-avatar> <account-change-avatar></account-change-avatar>
<account-user-info></account-user-info> <account-user-info></account-user-info>
<account-privacy></account-privacy> <account-privacy></account-privacy>
<account-contact></account-contact>
</div> </div>
</div> </div>
@ -218,6 +219,7 @@
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 NewsletterList from "./components/NewsletterList.js";
import AccountContact from "./components/AccountContact.js";
const showErrorInfo = (message) => { const showErrorInfo = (message) => {
$('body').toast({ $('body').toast({
@ -256,7 +258,7 @@
MessageDelete, MessageUpdate, MessageReact, MessageRevoke, MessageDelete, MessageUpdate, MessageReact, MessageRevoke,
GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants, GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants,
NewsletterList, NewsletterList,
AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar
AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar, AccountContact
}, },
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
data() { data() {

Loading…
Cancel
Save