Browse Source
feat: Event listener, docker support and development environment
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 developmentpull/82/head
25 changed files with 835 additions and 74 deletions
-
44.air.toml
-
5.gitignore
-
4Makefile
-
4README.md
-
48dev/docker/.env
-
2dev/docker/.env.docker.example
-
3dev/docker/Dockerfile
-
44dev/docker/air/.air.toml
-
20dev/docker/docker-compose.override.yaml
-
22dev/docker/docker-compose.production.yaml
-
75dev/docker/docker-compose.yaml
-
19dev/docker/nginx/nginx.conf
-
0docker-compose.yml
-
49docs/docs.go
-
44docs/swagger.json
-
44docs/swagger.yaml
-
1internal/startup.go
-
76pkg/app/app.go
-
57pkg/app/database/container.go
-
10pkg/app/database/model.go
-
112pkg/app/database/upgrade.go
-
6pkg/app/drivers.go
-
169pkg/app/http/request.go
-
19pkg/utils/json.go
-
28pkg/whatsapp/whatsapp.go
@ -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 |
|||
@ -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 |
|||
@ -0,0 +1,2 @@ |
|||
APP_WEBHOOK_URL_TARGET= |
|||
APP_WEBHOOK_BASIC_AUTH= |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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,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 |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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"` |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
package app |
|||
|
|||
import ( |
|||
_ "github.com/lib/pq" |
|||
_ "modernc.org/sqlite" |
|||
) |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue