mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
feat: add cmd/smsapid for use with android-sms-gateway
This commit is contained in:
parent
4176923dd8
commit
71eceb809f
16
cmd/smsapid/go.mod
Normal file
16
cmd/smsapid/go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module github.com/therootcompany/golib/cmd/smsapid
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/simonfrey/jsonl v0.0.0-20240904112901-935399b9a740
|
||||||
|
github.com/therootcompany/golib/colorjson v1.0.1
|
||||||
|
github.com/therootcompany/golib/http/androidsmsgateway v0.0.0-20260223054429-c8f26aca7c6d
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
)
|
||||||
17
cmd/smsapid/go.sum
Normal file
17
cmd/smsapid/go.sum
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/simonfrey/jsonl v0.0.0-20240904112901-935399b9a740 h1:CXJI+lliMiiEwzfgE8yt/38K0heYDgQ0L3f/3fxRnQU=
|
||||||
|
github.com/simonfrey/jsonl v0.0.0-20240904112901-935399b9a740/go.mod h1:G4w16caPmc6at7u4fmkj/8OAoOnM9mkmJr2fvL0vhaw=
|
||||||
|
github.com/therootcompany/golib/colorjson v1.0.1 h1:AfBeVr9GX9xMvlJNFmFYzkWFy62yWwwXjX2LLA/Afto=
|
||||||
|
github.com/therootcompany/golib/colorjson v1.0.1/go.mod h1:bE0wCyOsRFQnz22+TnQu4D0+FPl+ZugaaE79bjgDqRw=
|
||||||
|
github.com/therootcompany/golib/http/androidsmsgateway v0.0.0-20260223054429-c8f26aca7c6d h1:jKf9QQUiGAHsrjkfpoo4FTQnFJu4UkDkPreZLll7tdE=
|
||||||
|
github.com/therootcompany/golib/http/androidsmsgateway v0.0.0-20260223054429-c8f26aca7c6d/go.mod h1:2O9+uXPc1VAJvveK9eqm9X4e4pTJmFWV6vtJa3sI/CA=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
327
cmd/smsapid/main.go
Normal file
327
cmd/smsapid/main.go
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/simonfrey/jsonl"
|
||||||
|
"github.com/therootcompany/golib/colorjson"
|
||||||
|
"github.com/therootcompany/golib/http/androidsmsgateway"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsonf = colorjson.NewFormatter()
|
||||||
|
|
||||||
|
var webhookEvents []androidsmsgateway.WebhookEvent
|
||||||
|
var webhookWriter jsonl.Writer
|
||||||
|
var webhookMux = sync.Mutex{}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
jsonf.Indent = 3
|
||||||
|
|
||||||
|
// TODO manual override via flags
|
||||||
|
// color.NoColor = false
|
||||||
|
|
||||||
|
filePath := "./messages.jsonl"
|
||||||
|
{
|
||||||
|
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open file '%s': %v", filePath, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
|
|
||||||
|
// buf := bufio.NewReader(file)
|
||||||
|
buf := file
|
||||||
|
webhookEvents, err = readWebhooks(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read jsonl file '%s': %v", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to open file: %v", err))
|
||||||
|
}
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
|
|
||||||
|
webhookWriter = jsonl.NewWriter(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /api/webhooks", handlerWebhooks)
|
||||||
|
mux.Handle("GET /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||||
|
mux.Handle("POST /", LogRequest(http.HandlerFunc(handler)))
|
||||||
|
mux.Handle("PATCH /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||||
|
mux.Handle("PUT /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||||
|
mux.Handle("DELETE /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||||
|
|
||||||
|
addr := "localhost:8088"
|
||||||
|
fmt.Printf("Listening on %s...\n\n", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, mux))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleOK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ctxKey struct{}
|
||||||
|
|
||||||
|
var CtxKeyBody = ctxKey{}
|
||||||
|
|
||||||
|
func LogRequest(next http.Handler) http.Handler {
|
||||||
|
return LogHeaders(LogBody(next))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Log method, path, and query
|
||||||
|
var query string
|
||||||
|
if len(r.URL.RawQuery) > 0 {
|
||||||
|
query = "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
log.Printf("%s %s%s", r.Method, r.URL.Path, query)
|
||||||
|
|
||||||
|
// Find max header name length for alignment
|
||||||
|
maxLen := len("HOST")
|
||||||
|
for name := range r.Header {
|
||||||
|
if len(name) > maxLen {
|
||||||
|
maxLen = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxLen += 1
|
||||||
|
|
||||||
|
fmt.Printf(" %-"+fmt.Sprintf("%d", maxLen+1)+"s %s\n", "HOST", r.Host)
|
||||||
|
for name, values := range r.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
fmt.Printf(" %-"+fmt.Sprintf("%d", maxLen+1)+"s %s\n", name+":", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogBody(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
switch strings.ToUpper(r.Method) {
|
||||||
|
case "HEAD", "GET", "DELETE", "OPTIONS":
|
||||||
|
if len(body) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unexpected body:\n%q\n", string(body))
|
||||||
|
}
|
||||||
|
case "POST", "PATCH", "PUT":
|
||||||
|
// known
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unexpected method %s\n", r.Method)
|
||||||
|
}
|
||||||
|
defer fmt.Println()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read body:\n%q\n", string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and pretty-print JSON, or raw body
|
||||||
|
textBytes := body
|
||||||
|
var text string
|
||||||
|
var data any
|
||||||
|
if err := json.Unmarshal(body, &data); err == nil {
|
||||||
|
textBytes, _ = jsonf.Marshal(data)
|
||||||
|
}
|
||||||
|
text = string(textBytes)
|
||||||
|
text = prefixLines(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
fmt.Printf(" %s\n", text)
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), CtxKeyBody, body)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerWebhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
|
||||||
|
next := r.URL.Query().Get("next")
|
||||||
|
previous := r.URL.Query().Get("previous")
|
||||||
|
limitStr := r.URL.Query().Get("limit")
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
var startIdx, endIdx int
|
||||||
|
if next != "" {
|
||||||
|
for i, event := range webhookEvents {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *androidsmsgateway.WebhookSent:
|
||||||
|
if e.ID == next {
|
||||||
|
startIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case *androidsmsgateway.WebhookDelivered:
|
||||||
|
if e.ID == next {
|
||||||
|
startIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case *androidsmsgateway.WebhookReceived:
|
||||||
|
if e.ID == next {
|
||||||
|
startIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if previous != "" {
|
||||||
|
for i, event := range webhookEvents {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *androidsmsgateway.WebhookSent:
|
||||||
|
if e.ID == previous && i >= limit {
|
||||||
|
startIdx = i - limit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case *androidsmsgateway.WebhookDelivered:
|
||||||
|
if e.ID == previous && i >= limit {
|
||||||
|
startIdx = i - limit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case *androidsmsgateway.WebhookReceived:
|
||||||
|
if e.ID == previous && i >= limit {
|
||||||
|
startIdx = i - limit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(webhookEvents) > limit {
|
||||||
|
startIdx = len(webhookEvents) - limit
|
||||||
|
} else {
|
||||||
|
startIdx = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx = min(startIdx+limit, len(webhookEvents))
|
||||||
|
|
||||||
|
if _, err := w.Write([]byte("[")); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to write response"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, event := range webhookEvents[startIdx:endIdx] {
|
||||||
|
if i > 0 {
|
||||||
|
if _, err := w.Write([]byte(",")); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to write response"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := enc.Encode(event); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to encode webhook"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte("]")); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to write response"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// this will return OK unless a retry is needed (e.g. internal error)
|
||||||
|
|
||||||
|
body, ok := r.Context().Value(CtxKeyBody).([]byte)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var webhook androidsmsgateway.Webhook
|
||||||
|
if err := json.Unmarshal(body, &webhook); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to parse webhook"}`, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts, _ := strconv.Atoi(r.Header.Get("X-Timestamp"))
|
||||||
|
webhook.XTimestamp = int64(ts)
|
||||||
|
webhook.XSignature = r.Header.Get("X-Signature")
|
||||||
|
|
||||||
|
h, err := androidsmsgateway.Decode(&webhook)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to parse webhook as a specific event"}`, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h.GetEvent() {
|
||||||
|
case "mms:received", "sms:received", "sms:data-received", "sms:sent", "sms:delivered", "sms:failed":
|
||||||
|
webhookMux.Lock()
|
||||||
|
defer webhookMux.Unlock()
|
||||||
|
if err := webhookWriter.Write(h); err != nil {
|
||||||
|
http.Error(w, `{"error":"failed to save webhook"}`, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
webhookEvents = append(webhookEvents, h)
|
||||||
|
case "system:ping":
|
||||||
|
// nothing to do yet
|
||||||
|
default:
|
||||||
|
http.Error(w, `{"error":"unknown webhook event"}`, http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = w.Write([]byte(`{"message": "ok"}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixLines(text, prefix string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lines[i] = prefix + line
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readWebhooks(f io.Reader) ([]androidsmsgateway.WebhookEvent, error) {
|
||||||
|
var webhooks []androidsmsgateway.WebhookEvent
|
||||||
|
r := jsonl.NewReader(f)
|
||||||
|
err := r.ReadLines(func(line []byte) error {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var webhook androidsmsgateway.Webhook
|
||||||
|
if err := json.Unmarshal(line, &webhook); err != nil {
|
||||||
|
return fmt.Errorf("could not unmarshal into Webhook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch webhook.Event {
|
||||||
|
case "sms:sent":
|
||||||
|
var sent androidsmsgateway.WebhookSent
|
||||||
|
if err := json.Unmarshal(line, &sent); err != nil {
|
||||||
|
return fmt.Errorf("could not unmarshal into WebhookSent: %w", err)
|
||||||
|
}
|
||||||
|
webhooks = append(webhooks, &sent)
|
||||||
|
case "sms:delivered":
|
||||||
|
var delivered androidsmsgateway.WebhookDelivered
|
||||||
|
if err := json.Unmarshal(line, &delivered); err != nil {
|
||||||
|
return fmt.Errorf("could not unmarshal into WebhookDelivered: %w", err)
|
||||||
|
}
|
||||||
|
webhooks = append(webhooks, &delivered)
|
||||||
|
case "sms:received":
|
||||||
|
var received androidsmsgateway.WebhookReceived
|
||||||
|
if err := json.Unmarshal(line, &received); err != nil {
|
||||||
|
return fmt.Errorf("could not unmarshal into WebhookReceived: %w", err)
|
||||||
|
}
|
||||||
|
webhooks = append(webhooks, &received)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown event type: %s", webhook.Event)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return webhooks, fmt.Errorf("failed to read JSONL lines: %w", err)
|
||||||
|
}
|
||||||
|
return webhooks, nil
|
||||||
|
}
|
||||||
13
cmd/smsapid/webhook-list.sh
Executable file
13
cmd/smsapid/webhook-list.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
. ./.env
|
||||||
|
|
||||||
|
# https://docs.sms-gate.app/features/webhooks/
|
||||||
|
|
||||||
|
cmd_curl="curl --fail-with-body -sS"
|
||||||
|
|
||||||
|
printf '\nExisting webhooks\n'
|
||||||
|
$cmd_curl "${SMSGW_BASEURL}/webhooks" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}" | jq
|
||||||
55
cmd/smsapid/webhook-register.sh
Executable file
55
cmd/smsapid/webhook-register.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
. ./.env
|
||||||
|
|
||||||
|
# https://docs.sms-gate.app/features/webhooks/
|
||||||
|
|
||||||
|
cmd_curl="curl --fail-with-body -sS"
|
||||||
|
g_events="sms:sent sms:delivered sms:failed sms:received mms:received sms:data-received system:ping"
|
||||||
|
|
||||||
|
fn_delete_all() { (
|
||||||
|
b_json="$(
|
||||||
|
$cmd_curl "${SMSGW_BASEURL}/webhooks" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}"
|
||||||
|
)"
|
||||||
|
echo "${b_json}" | jq
|
||||||
|
|
||||||
|
echo "${b_json}" | jq -r '.[] | "\(.id) \(.event) \(.url)"' | while read -r b_id b_event b_url; do
|
||||||
|
echo >&2 "Deleting webhook ${b_id} ${b_url} ${b_event}"
|
||||||
|
$cmd_curl -X DELETE "${SMSGW_BASEURL}/webhooks/${b_id}" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}"
|
||||||
|
done
|
||||||
|
); }
|
||||||
|
|
||||||
|
fn_subscribe_all() { (
|
||||||
|
for b_event in $g_events; do
|
||||||
|
echo >&2 "Subscribing to ${b_event}"
|
||||||
|
$cmd_curl "${SMSGW_BASEURL}/webhooks" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"url": "https://smsgateway.lab1.therootcompany.com/api/log",
|
||||||
|
"event": "'"${b_event}"'"
|
||||||
|
}' | jq
|
||||||
|
printf '\n'
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
); }
|
||||||
|
|
||||||
|
printf '\nPurging all existing webooks\n'
|
||||||
|
fn_delete_all
|
||||||
|
|
||||||
|
printf '\nExisting webhooks\n'
|
||||||
|
$cmd_curl "${SMSGW_BASEURL}/webhooks" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}" | jq
|
||||||
|
|
||||||
|
printf '\nSubscribe to all webooks\n'
|
||||||
|
fn_subscribe_all
|
||||||
|
|
||||||
|
printf 'Current webooks\n'
|
||||||
|
$cmd_curl "${SMSGW_BASEURL}/webhooks" \
|
||||||
|
-u "${SMSGW_USER}:${SMSGW_PASSWORD}" | jq
|
||||||
|
|
||||||
|
printf 'OK\n'
|
||||||
176
http/androidsmsgateway/webhooks.go
Normal file
176
http/androidsmsgateway/webhooks.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package androidsmsgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebhookEvent is an interface for all webhook event types.
|
||||||
|
type WebhookEvent interface {
|
||||||
|
GetEvent() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook represents a webhook notification for an SMS sent event.
|
||||||
|
type Webhook struct {
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Payload json.RawMessage `json:"payload"`
|
||||||
|
WebhookID string `json:"webhookId"`
|
||||||
|
XSignature string `json:"x-signature"`
|
||||||
|
XTimestamp int64 `json:"x-timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookSent represents a webhook notification for an SMS sent event.
|
||||||
|
type WebhookSent struct {
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Payload WebhookSentPayload `json:"payload"`
|
||||||
|
WebhookID string `json:"webhookId"`
|
||||||
|
XSignature string `json:"x-signature"`
|
||||||
|
XTimestamp int64 `json:"x-timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEvent marks WebhookSent as part of the WebhookEvent interface.
|
||||||
|
func (w *WebhookSent) GetEvent() string {
|
||||||
|
return w.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookSentPayload contains details about the sent SMS.
|
||||||
|
type WebhookSentPayload struct {
|
||||||
|
MessageID string `json:"messageId"`
|
||||||
|
PartsCount int `json:"partsCount"`
|
||||||
|
PhoneNumber string `json:"phoneNumber"`
|
||||||
|
SentAt time.Time `json:"sentAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookDelivered represents a webhook notification for an SMS delivered event.
|
||||||
|
type WebhookDelivered struct {
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Payload WebhookDeliveredPayload `json:"payload"`
|
||||||
|
WebhookID string `json:"webhookId"`
|
||||||
|
XSignature string `json:"x-signature"`
|
||||||
|
XTimestamp int64 `json:"x-timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEvent marks WebhookDelivered as part of the WebhookEvent interface.
|
||||||
|
func (w *WebhookDelivered) GetEvent() string {
|
||||||
|
return w.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookDeliveredPayload contains details about the delivered SMS.
|
||||||
|
type WebhookDeliveredPayload struct {
|
||||||
|
DeliveredAt time.Time `json:"deliveredAt"`
|
||||||
|
MessageID string `json:"messageId"`
|
||||||
|
PhoneNumber string `json:"phoneNumber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookReceived represents a webhook notification for an SMS received event.
|
||||||
|
type WebhookReceived struct {
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Payload WebhookReceivedPayload `json:"payload"`
|
||||||
|
WebhookID string `json:"webhookId"`
|
||||||
|
XSignature string `json:"x-signature"`
|
||||||
|
XTimestamp int64 `json:"x-timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEvent marks WebhookDelivered as part of the WebhookEvent interface.
|
||||||
|
func (w *WebhookReceived) GetEvent() string {
|
||||||
|
return w.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookReceivedPayload contains details about the received SMS.
|
||||||
|
type WebhookReceivedPayload struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
MessageID string `json:"messageId"`
|
||||||
|
PhoneNumber string `json:"phoneNumber"`
|
||||||
|
ReceivedAt time.Time `json:"receivedAt"`
|
||||||
|
SimNumber int `json:"simNumber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookPing represents a system:ping webhook event.
|
||||||
|
type WebhookPing struct {
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Payload WebhookPingPayload `json:"payload"`
|
||||||
|
WebhookID string `json:"webhookId"`
|
||||||
|
XSignature string `json:"x-signature"`
|
||||||
|
XTimestamp int64 `json:"x-timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEvent marks WebhookPing as part of the WebhookEvent interface.
|
||||||
|
func (w *WebhookPing) GetEvent() string {
|
||||||
|
return w.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookPingPayload contains the health data reported by a system:ping event.
|
||||||
|
type WebhookPingPayload struct {
|
||||||
|
Health DeviceHealth `json:"health"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceHealth is the top-level health object inside a system:ping payload.
|
||||||
|
type DeviceHealth struct {
|
||||||
|
Checks map[string]HealthCheck `json:"checks"`
|
||||||
|
ReleaseID int `json:"releaseId"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck represents a single named health check result.
|
||||||
|
type HealthCheck struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
ObservedUnit string `json:"observedUnit"`
|
||||||
|
ObservedValue float64 `json:"observedValue"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes the raw Payload based on the Event field and returns the appropriate WebhookEvent.
|
||||||
|
func Decode(webhook *Webhook) (WebhookEvent, error) {
|
||||||
|
switch webhook.Event {
|
||||||
|
case "sms:sent":
|
||||||
|
var sent WebhookSent
|
||||||
|
sent.DeviceID = webhook.DeviceID
|
||||||
|
sent.Event = webhook.Event
|
||||||
|
sent.ID = webhook.ID
|
||||||
|
sent.WebhookID = webhook.WebhookID
|
||||||
|
sent.XSignature = webhook.XSignature
|
||||||
|
sent.XTimestamp = webhook.XTimestamp
|
||||||
|
if err := json.Unmarshal(webhook.Payload, &sent.Payload); err != nil {
|
||||||
|
return nil, errors.New("failed to decode sms:sent payload: " + err.Error())
|
||||||
|
}
|
||||||
|
return &sent, nil
|
||||||
|
case "sms:delivered":
|
||||||
|
var delivered WebhookDelivered
|
||||||
|
delivered.DeviceID = webhook.DeviceID
|
||||||
|
delivered.Event = webhook.Event
|
||||||
|
delivered.ID = webhook.ID
|
||||||
|
delivered.WebhookID = webhook.WebhookID
|
||||||
|
delivered.XSignature = webhook.XSignature
|
||||||
|
delivered.XTimestamp = webhook.XTimestamp
|
||||||
|
if err := json.Unmarshal(webhook.Payload, &delivered.Payload); err != nil {
|
||||||
|
return nil, errors.New("failed to decode sms:delivered payload: " + err.Error())
|
||||||
|
}
|
||||||
|
return &delivered, nil
|
||||||
|
case "sms:received":
|
||||||
|
var received WebhookReceived
|
||||||
|
received.DeviceID = webhook.DeviceID
|
||||||
|
received.Event = webhook.Event
|
||||||
|
received.ID = webhook.ID
|
||||||
|
received.WebhookID = webhook.WebhookID
|
||||||
|
received.XSignature = webhook.XSignature
|
||||||
|
received.XTimestamp = webhook.XTimestamp
|
||||||
|
if err := json.Unmarshal(webhook.Payload, &received.Payload); err != nil {
|
||||||
|
return nil, errors.New("failed to decode sms:received payload: " + err.Error())
|
||||||
|
}
|
||||||
|
return &received, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown event type: " + webhook.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user