committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 840 additions and 74 deletions
-
44.air.toml
-
7.gitignore
-
4Makefile
-
4README.md
-
53dev/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
-
2docker-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,53 @@ |
|||||
|
# ----------------------------------- |
||||
|
# 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 |
||||
|
|
||||
|
# POSTGRES ENVs |
||||
|
POSTGRES_USER=postgres |
||||
|
POSTGRES_PASSWORD=postgres |
||||
|
POSTGRES_DB=postgres |
||||
@ -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_USER} |
||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} |
||||
|
POSTGRES_DB: ${POSTGRES_DB} |
||||
|
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