Browse Source

feat: Event listener, docker support and development environment

Event listener can be processed and sent as a webhook
Adding docker support for further development and scaling development
pull/82/head
Ilham Fadhilah 2 years ago
parent
commit
66f61408a7
  1. 44
      .air.toml
  2. 5
      .gitignore
  3. 4
      Makefile
  4. 4
      README.md
  5. 48
      dev/docker/.env
  6. 2
      dev/docker/.env.docker.example
  7. 3
      dev/docker/Dockerfile
  8. 44
      dev/docker/air/.air.toml
  9. 20
      dev/docker/docker-compose.override.yaml
  10. 22
      dev/docker/docker-compose.production.yaml
  11. 75
      dev/docker/docker-compose.yaml
  12. 19
      dev/docker/nginx/nginx.conf
  13. 0
      docker-compose.yml
  14. 49
      docs/docs.go
  15. 44
      docs/swagger.json
  16. 44
      docs/swagger.yaml
  17. 1
      internal/startup.go
  18. 76
      pkg/app/app.go
  19. 57
      pkg/app/database/container.go
  20. 10
      pkg/app/database/model.go
  21. 112
      pkg/app/database/upgrade.go
  22. 6
      pkg/app/drivers.go
  23. 169
      pkg/app/http/request.go
  24. 19
      pkg/utils/json.go
  25. 28
      pkg/whatsapp/whatsapp.go

44
.air.toml

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "volumes", "dbs", "node_modules", "misc"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "10s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

5
.gitignore

@ -5,3 +5,8 @@ dbs/*
.env
*.DS_Store
!*.gitkeep
dev/docker/volumes
dev/docker/.env.docker
!dev/docker/.env
tmp
node_modules

4
Makefile

@ -43,6 +43,10 @@ run:
make vendor
go run cmd/main/*.go
run-dev:
make vendor
air -c .air.toml || echo "Please ensure 'air' is installed and '.air.toml' exists."
gen-docs:
rm -rf docs/*
swag init -g cmd/main/main.go --output docs

4
README.md

@ -99,9 +99,9 @@ make vendor
5) Link or copy environment variables file
```sh
ln -sf .env.development .env
ln -sf .env.example .env
# - OR -
cp .env.development .env
cp .env.example .env
```
6) Until this step you already can run this code by using this command

48
dev/docker/.env

@ -0,0 +1,48 @@
# -----------------------------------
# Server Configuration
# -----------------------------------
SERVER_ADDRESS=0.0.0.0
SERVER_PORT=1321
# -----------------------------------
# HTTP Configuration
# -----------------------------------
HTTP_BASE_URL=/api/v1/whatsapp
# HTTP_CORS_ORIGIN=*
# HTTP_BODY_LIMIT_SIZE=8m
# HTTP_GZIP_LEVEL=1
# HTTP_CACHE_CAPACITY=100
# HTTP_CACHE_TTL_SECONDS=5
# -----------------------------------
# Authentication Configuration
# -----------------------------------
AUTH_BASIC_USERNAME=root
AUTH_BASIC_PASSWORD=root
AUTH_JWT_SECRET=ThisIsJWTSecret
AUTH_JWT_EXPIRED_HOUR=24
# -----------------------------------
# WhatsApp Configuration
# -----------------------------------
WHATSAPP_DATASTORE_TYPE=postgres
WHATSAPP_DATASTORE_URI=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
# WHATSAPP_DATASTORE_TYPE=sqlite
# WHATSAPP_DATASTORE_URI=file:dbs/WhatsApp.db?_pragma=foreign_keys(1)
WHATSAPP_CLIENT_PROXY_URL=""
WHATSAPP_MEDIA_IMAGE_COMPRESSION=true
WHATSAPP_MEDIA_IMAGE_CONVERT_WEBP=true
# WHATSAPP_VERSION_MAJOR=2
# WHATSAPP_VERSION_MINOR=2411
# WHATSAPP_VERSION_PATCH=2
# -----------------------------------
# 3rd Party Configuration
# -----------------------------------
LIBWEBP_VERSION=0.6.1

2
dev/docker/.env.docker.example

@ -0,0 +1,2 @@
APP_WEBHOOK_URL_TARGET=
APP_WEBHOOK_BASIC_AUTH=

3
Dockerfile → dev/docker/Dockerfile

@ -27,7 +27,6 @@ RUN mkdir -p {.bin/webp,dbs} \
COPY --from=go-builder /usr/src/app/.env.example ./.env
COPY --from=go-builder /usr/src/app/main ./main
EXPOSE 3000
VOLUME ["/usr/app/${SERVICE_NAME}/dbs"]
CMD ["main"]

44
dev/docker/air/.air.toml

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "/app/src/tmp/main"
cmd = "go build -o /app/src/tmp/main /app/src/cmd/main/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "volumes", "dbs", "node_modules", "misc"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "10s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

20
dev/docker/docker-compose.override.yaml

@ -0,0 +1,20 @@
x-app: &whatsapp-rest
image: 'cosmtrek/air:v1.61.7'
# entrypoint: ['tail', '-f', '/dev/null']
command: ['-c', '/app/src/misc/docker/air/.air.toml']
depends_on:
- postgres
volumes:
- ../../:/app/src
- ./.env:/app/src/.env
- ./volumes/dbs:/app/src/dbs
services:
whatsapp-rest-1:
<<: *whatsapp-rest
# whatsapp-rest-2:
# <<: *whatsapp-rest
# whatsapp-rest-3:
# <<: *whatsapp-rest

22
dev/docker/docker-compose.production.yaml

@ -0,0 +1,22 @@
x-app: &whatsapp-rest
build:
context: ../..
dockerfile: ./misc/docker/Dockerfile
image: 'ilhamfadhilah/whatsapp-rest:local'
depends_on:
- postgres
env_file:
- ./.env
- ./.env.docker
volumes:
- ./volumes/dbs:/usr/app/go-whatsapp-multidevice-rest/dbs
services:
whatsapp-rest-1:
<<: *whatsapp-rest
# whatsapp-rest-2:
# <<: *whatsapp-rest
# whatsapp-rest-3:
# <<: *whatsapp-rest

75
dev/docker/docker-compose.yaml

@ -0,0 +1,75 @@
x-app: &whatsapp-rest
depends_on:
- postgres
services:
whatsapp-rest-1:
<<: *whatsapp-rest
# whatsapp-rest-2:
# <<: *whatsapp-rest
# whatsapp-rest-3:
# <<: *whatsapp-rest
postgres:
image: postgres:16.2
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- './volumes/postgres:/var/lib/postgresql/data'
ports:
- "0.0.0.0:5432:5432"
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d postgres']
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
networks:
default:
pgadmin:
image: dpage/pgadmin4
restart: always
ports:
- '8888:80'
depends_on:
- postgres
environment:
PGADMIN_DEFAULT_EMAIL: admin@test.net
PGADMIN_DEFAULT_PASSWORD: 123456
volumes:
- ./volumes/pgadmin:/var/lib/pgadmin
networks:
default:
nginx:
image: nginx:latest
container_name: nginx
ports:
- "1321:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- whatsapp-rest-1
# - whatsapp-rest-2
# - whatsapp-rest-3
webhook-tester:
image: tarampampam/webhook-tester
command: serve --port 8080 --storage-driver redis --pubsub-driver redis --redis-dsn redis://redis:6379/0
ports:
- 8080:8080
depends_on:
redis:
condition: service_healthy
redis:
image: redis:7-alpine
volumes:
- ./volumes/redis:/data
ports:
- 6379:6379
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 1s

19
dev/docker/nginx/nginx.conf

@ -0,0 +1,19 @@
events {}
http {
upstream backend_servers {
server whatsapp-rest-1:1321;
# server whatsapp-rest-2:1321;
# server whatsapp-rest-3:1321;
}
server {
listen 80;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

0
docker-compose.yaml → docker-compose.yml

49
docs/docs.go

@ -1,5 +1,4 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
@ -32,7 +31,7 @@ const docTemplate = `{
"summary": "Show The Status of The Server",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -54,7 +53,7 @@ const docTemplate = `{
"summary": "Generate Authentication Token",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -76,7 +75,7 @@ const docTemplate = `{
"summary": "Get Joined Groups Information",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -107,7 +106,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -138,7 +137,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -177,7 +176,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -202,7 +201,7 @@ const docTemplate = `{
"summary": "Pair Phone for WhatsApp Multi-Device Login",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -224,7 +223,7 @@ const docTemplate = `{
"summary": "Logout Device from WhatsApp Multi-Device",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -265,7 +264,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -313,7 +312,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -361,7 +360,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -392,7 +391,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -433,7 +432,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -481,7 +480,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -522,7 +521,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -577,7 +576,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -624,7 +623,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -672,7 +671,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -727,7 +726,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -768,7 +767,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -809,7 +808,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -864,7 +863,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -892,6 +891,8 @@ var SwaggerInfo = &swag.Spec{
Description: "This is WhatsApp Multi-Device Implementation in Go REST API",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {

44
docs/swagger.json

@ -23,7 +23,7 @@
"summary": "Show The Status of The Server",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -45,7 +45,7 @@
"summary": "Generate Authentication Token",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -67,7 +67,7 @@
"summary": "Get Joined Groups Information",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -98,7 +98,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -129,7 +129,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -168,7 +168,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -193,7 +193,7 @@
"summary": "Pair Phone for WhatsApp Multi-Device Login",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -215,7 +215,7 @@
"summary": "Logout Device from WhatsApp Multi-Device",
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -256,7 +256,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -304,7 +304,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -352,7 +352,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -383,7 +383,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -424,7 +424,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -472,7 +472,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -513,7 +513,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -568,7 +568,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -615,7 +615,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -663,7 +663,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -718,7 +718,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -759,7 +759,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -800,7 +800,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}
@ -855,7 +855,7 @@
],
"responses": {
"200": {
"description": ""
"description": "OK"
}
}
}

44
docs/swagger.yaml

@ -14,7 +14,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
summary: Show The Status of The Server
tags:
- Root
@ -25,7 +25,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BasicAuth: []
summary: Generate Authentication Token
@ -38,7 +38,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Get Joined Groups Information
@ -57,7 +57,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Join Group From Invitation Link
@ -76,7 +76,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Leave Group By Group ID
@ -101,7 +101,7 @@ paths:
- text/html
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Generate QR Code for WhatsApp Multi-Device Login
@ -116,7 +116,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Pair Phone for WhatsApp Multi-Device Login
@ -129,7 +129,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Logout Device from WhatsApp Multi-Device
@ -155,7 +155,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Delete Message
@ -186,7 +186,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Update Message
@ -217,7 +217,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: React Message
@ -236,7 +236,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Check If WhatsApp Personal ID is Registered
@ -262,7 +262,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Audio Message
@ -294,7 +294,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Contact Message
@ -321,7 +321,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Document Message
@ -357,7 +357,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Image Message
@ -387,7 +387,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Link Message
@ -419,7 +419,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Location Message
@ -455,7 +455,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Poll
@ -482,7 +482,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Sticker Message
@ -508,7 +508,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Text Message
@ -544,7 +544,7 @@ paths:
- application/json
responses:
"200":
description: ""
description: OK
security:
- BearerAuth: []
summary: Send Video Message

1
internal/startup.go

@ -1,6 +1,7 @@
package internal
import (
_ "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/app"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/log"
pkgWhatsApp "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/whatsapp"
)

76
pkg/app/app.go

@ -0,0 +1,76 @@
package app
import (
"log"
"time"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/app/database"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/app/http"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/env"
)
var (
AppWebhookURL string
AppWebhookBasicAuth string
AppDatabase *database.DatabaseContainer
AppRequest *http.HttpClient
)
func init() {
var err error
dbType, err := env.GetEnvString("WHATSAPP_DATASTORE_TYPE")
if err != nil {
log.Fatal("Error Parse Environment Variable for Application Datastore Type")
}
dbURI, err := env.GetEnvString("WHATSAPP_DATASTORE_URI")
if err != nil {
log.Fatal("Error Parse Environment Variable for Application Datastore URI")
}
// Initialize App Client Datastore
initDB(dbType, dbURI)
appWebhookUrl, err := env.GetEnvString("APP_WEBHOOK_URL_TARGET")
if err != nil {
log.Fatal("Error Parse Environment Variable for App Webhook URL Target")
}
AppWebhookURL = appWebhookUrl
appWebhookBasicAuth, err := env.GetEnvString("APP_WEBHOOK_BASIC_AUTH")
if err != nil {
AppWebhookBasicAuth = ""
}
AppWebhookBasicAuth = appWebhookBasicAuth
// Initialize App HTTP Request
initHttpRequest()
}
func initDB(dbType string, dbURI string) {
// Initialize App Client Datastore
appDb, err := database.New(dbType, dbURI)
if err != nil {
log.Fatal("Error Connect Application Datastore: ", err)
}
AppDatabase = appDb
}
func initHttpRequest() {
// Initialize App HTTP Request
headers := map[string]string{
"Content-Type": "application/json",
}
if AppWebhookBasicAuth != "" {
headers["Authorization"] = "Basic " + AppWebhookBasicAuth
}
client := http.NewHttpClient(http.HttpClientOptions{
Timeout: 30 * time.Second,
Headers: headers,
})
AppRequest = client
}

57
pkg/app/database/container.go

@ -0,0 +1,57 @@
package database
import (
"database/sql"
"fmt"
)
type DatabaseContainer struct {
db *sql.DB
dialect string
}
// New connects to the given SQL database and wraps it in a Container.
//
// Only SQLite and Postgres are currently fully supported.
//
// The logger can be nil and will default to a no-op logger.
//
// When using SQLite, it's strongly recommended to enable foreign keys by adding `?_foreign_keys=true`:
//
// container, err := sqlstore.New("sqlite3", "file:yoursqlitefile.db?_foreign_keys=on", nil)
func New(dialect, address string) (*DatabaseContainer, error) {
db, err := sql.Open(dialect, address)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
container := NewWithDB(db, dialect)
err = container.Upgrade()
if err != nil {
return nil, fmt.Errorf("failed to upgrade database: %w", err)
}
return container, nil
}
func NewWithDB(db *sql.DB, dialect string) *DatabaseContainer {
return &DatabaseContainer{
db: db,
dialect: dialect,
}
}
const (
insertWebhookResponseQuery = `
INSERT INTO app_callback_responses (callback_url,status,response,error_message)
VALUES ($1, $2, $3, $4)
`
)
func (c *DatabaseContainer) StoreResponse(res *AppWebhookResponse) error {
_, err := c.db.Exec(insertWebhookResponseQuery,
res.CallbackUrl,
res.Status,
res.Response,
res.ErrorMessage,
)
return err
}

10
pkg/app/database/model.go

@ -0,0 +1,10 @@
package database
import "encoding/json"
type AppWebhookResponse struct {
CallbackUrl string `json:"callback_url"`
Status string `json:"status"`
Response json.RawMessage `json:"response"`
ErrorMessage string `json:"error_message"`
}

112
pkg/app/database/upgrade.go

@ -0,0 +1,112 @@
package database
import (
"database/sql"
"fmt"
)
type upgradeFunc func(*sql.Tx, *DatabaseContainer) error
// Upgrades is a list of functions that will upgrade a database to the latest version.
//
// This may be of use if you want to manage the database fully manually, but in most cases you
// should just call DatabaseContainer.Upgrade to let the library handle everything.
var Upgrades = [...]upgradeFunc{upgradeV1}
const (
AppMigrationTable = "app_migration_version"
)
func (c *DatabaseContainer) getVersion() (int, error) {
_, err := c.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version INTEGER)", AppMigrationTable))
if err != nil {
return -1, err
}
version := 0
row := c.db.QueryRow(fmt.Sprintf("SELECT version FROM %s LIMIT 1", AppMigrationTable))
if row != nil {
_ = row.Scan(&version)
}
return version, nil
}
func (c *DatabaseContainer) setVersion(tx *sql.Tx, version int) error {
_, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", AppMigrationTable))
if err != nil {
return err
}
_, err = tx.Exec(fmt.Sprintf("INSERT INTO %s (version) VALUES ($1)", AppMigrationTable), version)
return err
}
func (c *DatabaseContainer) Upgrade() error {
if c.dialect == "sqlite" {
var foreignKeysEnabled bool
err := c.db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeysEnabled)
if err != nil {
return fmt.Errorf("failed to check if foreign keys are enabled: %w", err)
} else if !foreignKeysEnabled {
return fmt.Errorf("foreign keys are not enabled")
}
}
version, err := c.getVersion()
if err != nil {
return err
}
for ; version < len(Upgrades); version++ {
var tx *sql.Tx
tx, err = c.db.Begin()
if err != nil {
return err
}
migrateFunc := Upgrades[version]
err = migrateFunc(tx, c)
if err != nil {
_ = tx.Rollback()
return err
}
if err = c.setVersion(tx, version+1); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
}
return nil
}
func upgradeV1(tx *sql.Tx, c *DatabaseContainer) error {
var err error
postgresAppCallback := `CREATE TABLE app_callback_responses (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
callback_url TEXT NOT NULL,
status VARCHAR(50) NOT NULL,
response JSONB,
error_message TEXT,
timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);`
sqliteAppCallback := `CREATE TABLE app_callback_responses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
callback_url TEXT NOT NULL,
status TEXT NOT NULL,
response TEXT,
error_message TEXT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP
);`
if c.dialect == "postgres" {
_, err = tx.Exec(postgresAppCallback)
} else if c.dialect == "sqlite" {
_, err = tx.Exec(sqliteAppCallback)
}
if err != nil {
return err
}
return nil
}

6
pkg/app/drivers.go

@ -0,0 +1,6 @@
package app
import (
_ "github.com/lib/pq"
_ "modernc.org/sqlite"
)

169
pkg/app/http/request.go

@ -0,0 +1,169 @@
package http
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// HttpClient wraps an http.Client and provides methods for GET and POST
type HttpClient struct {
Client *http.Client
Headers map[string]string
BaseURL string
}
type HttpClientOptions struct {
Timeout time.Duration
Headers map[string]string
BaseURL string
}
type HttpResponse struct {
Status string
StatusCode int
Headers http.Header
Body []byte
}
type HttpError struct {
Response *HttpResponse
Message string
}
func (e *HttpError) Error() string {
return fmt.Sprintf("%s (status code %d)", e.Message, e.Response.StatusCode)
}
func NewHttpClient(option ...HttpClientOptions) *HttpClient {
var timeout time.Duration = 30 * time.Second
var headers map[string]string
var baseURL string
if len(option) > 0 {
if option[0].Timeout > 0 {
timeout = option[0].Timeout
}
headers = option[0].Headers
baseURL = option[0].BaseURL
}
return &HttpClient{
Client: &http.Client{Timeout: timeout},
Headers: headers,
BaseURL: baseURL,
}
}
// internal method to apply headers
func (h *HttpClient) applyHeaders(req *http.Request) {
for key, value := range h.Headers {
req.Header.Set(key, value)
}
}
// internal method to validate absolute URL
func validateAbsoluteURL(rawURL string) (bool, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return false, err
}
if !parsed.IsAbs() {
return false, nil
}
return true, nil
}
func (h *HttpClient) httpRequest(method, url string, body io.Reader) (*http.Request, error) {
// Validate URL
isAbsolute, _ := validateAbsoluteURL(url)
if !isAbsolute {
if h.BaseURL != "" {
url = h.BaseURL + url
}
}
req, err := http.NewRequest(method, url, body)
return req, err
}
// Get performs a GET request and returns full response info
func (h *HttpClient) Get(url string) (*HttpResponse, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
h.applyHeaders(req)
resp, err := h.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
response := &HttpResponse{
Status: resp.Status,
StatusCode: resp.StatusCode,
Headers: resp.Header,
Body: bodyBytes,
}
// Return an error if status code is 4xx or 5xx
if resp.StatusCode >= 400 {
return response, &HttpError{
Response: response,
Message: "unexpected HTTP status",
}
}
return response, nil
}
// Post performs a POST request and returns full response info
func (h *HttpClient) Post(url string, data any) (*HttpResponse, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer([]byte(jsonBytes)))
if err != nil {
return nil, err
}
h.applyHeaders(req)
resp, err := h.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
response := &HttpResponse{
Status: resp.Status,
StatusCode: resp.StatusCode,
Headers: resp.Header,
Body: bodyBytes,
}
// Return an error if status code is 4xx or 5xx
if resp.StatusCode >= 400 {
return response, &HttpError{
Response: response,
Message: "unexpected HTTP status",
}
}
return response, nil
}

19
pkg/utils/json.go

@ -0,0 +1,19 @@
package utils
import "encoding/json"
func ObjectToJson(o interface{}) string {
res, err := json.Marshal(o)
if err != nil {
return ""
}
return string(res)
}
func JsonToObject(data string, o interface{}) error {
err := json.Unmarshal([]byte(data), o)
if err != nil {
return err
}
return nil
}

28
pkg/whatsapp/whatsapp.go

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -28,7 +29,10 @@ import (
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/app"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/app/database"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/env"
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/log"
)
@ -106,6 +110,11 @@ func WhatsAppInitClient(device *store.Device, jid string) {
// Set WhatsApp Client Auto Trust Identity
WhatsAppClient[jid].AutoTrustIdentity = true
// Set WhatsApp Client Event Handler
WhatsAppClient[jid].AddEventHandler(func(evt interface{}) {
whatsAppEventHandler(evt)
})
}
}
@ -1342,3 +1351,22 @@ func WhatsAppGroupLeave(jid string, gjid string) error {
// Return Error WhatsApp Client is not Valid
return errors.New("WhatsApp Client is not Valid")
}
func whatsAppEventHandler(evt interface{}) {
switch v := evt.(type) {
case *events.Message:
res, err := app.AppRequest.Post(app.AppWebhookURL, v)
respBody := database.AppWebhookResponse{
CallbackUrl: app.AppWebhookURL,
Status: res.Status,
Response: json.RawMessage(res.Body),
}
if err != nil {
respBody.ErrorMessage = err.Error()
}
err = app.AppDatabase.StoreResponse(&respBody)
if err != nil {
fmt.Println("Error storing webhook response:", err)
}
}
}
Loading…
Cancel
Save