copilot: add CSV and JSON endpoints to smsapid with csvauth role-based auth

This commit is contained in:
AJ ONeal 2026-02-27 02:58:34 -07:00
parent 8056a07d10
commit bf3c4a48e4
No known key found for this signature in database
4 changed files with 443 additions and 44 deletions

View File

@ -3,14 +3,26 @@ module github.com/therootcompany/golib/cmd/smsapid
go 1.25.0
require (
github.com/go-chi/chi/v5 v5.2.5
github.com/jszwec/csvutil v1.10.0
github.com/simonfrey/jsonl v0.0.0-20240904112901-935399b9a740
github.com/therootcompany/golib/auth v1.0.0
github.com/therootcompany/golib/auth/csvauth v0.0.0
github.com/therootcompany/golib/colorjson v1.0.1
github.com/therootcompany/golib/http/androidsmsgateway v0.0.0-20260223054429-c8f26aca7c6d
github.com/therootcompany/golib/http/middleware/v2 v2.0.0
)
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
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
replace (
github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth
github.com/therootcompany/golib/http/androidsmsgateway => ../../http/androidsmsgateway
github.com/therootcompany/golib/http/middleware/v2 => ../../http/middleware
)

View File

@ -1,5 +1,9 @@
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/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI=
github.com/jszwec/csvutil v1.10.0/go.mod h1:/E4ONrmGkwmWsk9ae9jpXnv9QT8pLHEPcCirMFhxG9I=
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=
@ -7,11 +11,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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/auth v1.0.0 h1:17hfwcJO/Efc22/8RcCTKUD49mhCc5tyoHiKonA3Slg=
github.com/therootcompany/golib/auth v1.0.0/go.mod h1:DSw8llmDkMtvMZWrzrTRtcaLPpPMsT6Sg+qwGf5O2U8=
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/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
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=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

View File

@ -2,50 +2,76 @@ package main
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"
chiMiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/jszwec/csvutil"
"github.com/simonfrey/jsonl"
"github.com/therootcompany/golib/auth"
"github.com/therootcompany/golib/auth/csvauth"
"github.com/therootcompany/golib/colorjson"
"github.com/therootcompany/golib/http/androidsmsgateway"
mwpkg "github.com/therootcompany/golib/http/middleware/v2"
)
var jsonf = colorjson.NewFormatter()
// webhookMux protects webhookWriter, pingWriter, webhookEvents, and pingEvents.
var webhookMux = sync.Mutex{}
var webhookEvents []androidsmsgateway.WebhookEvent
var webhookWriter jsonl.Writer
var webhookMux = sync.Mutex{}
var pingEvents []*androidsmsgateway.WebhookPing
var pingWriter jsonl.Writer
var smsAuth *csvauth.Auth
// pingRow is a flat CSV row for a ping event (x-* headers omitted, health fields inlined).
type pingRow struct {
DeviceID string `csv:"deviceId"`
Event string `csv:"event"`
ID string `csv:"id"`
WebhookID string `csv:"webhookId"`
PingedAt time.Time `csv:"pingedAt"`
Status string `csv:"status"`
Version string `csv:"version"`
BatteryLevel float64 `csv:"batteryLevel"`
BatteryCharging float64 `csv:"batteryCharging"`
ConnectionStatus float64 `csv:"connectionStatus"`
ConnectionTransport float64 `csv:"connectionTransport"`
ConnectionCellular float64 `csv:"connectionCellular"`
MessagesFailed float64 `csv:"messagesFailed"`
}
func main() {
jsonf.Indent = 3
// TODO manual override via flags
// color.NoColor = false
filePath := "./messages.jsonl"
messagesPath := "./messages.jsonl"
{
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
file, err := os.OpenFile(messagesPath, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
log.Fatalf("failed to open file '%s': %v", filePath, err)
log.Fatalf("failed to open file '%s': %v", messagesPath, err)
}
defer func() { _ = file.Close() }()
// buf := bufio.NewReader(file)
buf := file
webhookEvents, err = readWebhooks(buf)
webhookEvents, err = readWebhooks(file)
if err != nil {
log.Fatalf("failed to read jsonl file '%s': %v", filePath, err)
log.Fatalf("failed to read jsonl file '%s': %v", messagesPath, err)
}
}
{
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
file, err := os.OpenFile(messagesPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(fmt.Errorf("failed to open file: %v", err))
}
@ -54,6 +80,45 @@ func main() {
webhookWriter = jsonl.NewWriter(file)
}
pingsPath := "./pings.jsonl"
{
file, err := os.OpenFile(pingsPath, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
log.Fatalf("failed to open file '%s': %v", pingsPath, err)
}
defer func() { _ = file.Close() }()
pingEvents, err = readPings(file)
if err != nil {
log.Fatalf("failed to read jsonl file '%s': %v", pingsPath, err)
}
}
{
file, err := os.OpenFile(pingsPath, 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() }()
pingWriter = jsonl.NewWriter(file)
}
// Load credentials for /api/smsgw routes.
credPath := "./credentials.tsv"
if v := os.Getenv("SMSAPID_CREDENTIALS_FILE"); v != "" {
credPath = v
}
aesKey := getAESKey("CSVAUTH_AES_128_KEY")
if f, err := os.Open(credPath); err == nil {
defer func() { _ = f.Close() }()
smsAuth = csvauth.New(aesKey)
if err := smsAuth.LoadCSV(f, '\t'); err != nil {
log.Fatalf("failed to load credentials from %q: %v", credPath, err)
}
} else {
log.Printf("Warning: credentials file %q not found; /api/smsgw routes will return 401: %v", credPath, err)
}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/webhooks", handlerWebhooks)
mux.Handle("GET /", LogRequest(http.HandlerFunc(HandleOK)))
@ -62,9 +127,285 @@ func main() {
mux.Handle("PUT /", LogRequest(http.HandlerFunc(HandleOK)))
mux.Handle("DELETE /", LogRequest(http.HandlerFunc(HandleOK)))
// Protected routes under /api/smsgw, authenticated via csvauth with the "sms" role.
smsgw := mwpkg.WithMux(mux, smsAuthMiddleware)
smsgw.HandleFunc("GET /api/smsgw/received.csv", handlerReceivedCSV)
smsgw.HandleFunc("GET /api/smsgw/received.json", handlerReceivedJSON)
smsgw.HandleFunc("GET /api/smsgw/sent.csv", handlerSentCSV)
smsgw.HandleFunc("GET /api/smsgw/sent.json", handlerSentJSON)
smsgw.HandleFunc("GET /api/smsgw/ping.csv", handlerPingCSV)
smsgw.HandleFunc("GET /api/smsgw/ping.json", handlerPingJSON)
addr := "localhost:8088"
fmt.Printf("Listening on %s...\n\n", addr)
log.Fatal(http.ListenAndServe(addr, mux))
log.Fatal(http.ListenAndServe(addr, chiMiddleware.Logger(chiMiddleware.Compress(5)(mux))))
}
// smsAuthMiddleware requires csvauth credentials with the "sms" role.
func smsAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if smsAuth == nil {
w.Header().Set("WWW-Authenticate", `Basic`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
cred, err := authenticateSMS(r)
if err != nil || !slices.Contains(cred.Permissions(), "sms") {
w.Header().Set("WWW-Authenticate", `Basic`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// authenticateSMS extracts and verifies credentials from Basic Auth, Authorization header, or API-Key header.
func authenticateSMS(r *http.Request) (auth.BasicPrinciple, error) {
// 1. Try Basic Auth
if username, password, ok := r.BasicAuth(); ok {
cred, err := smsAuth.Authenticate(username, password)
if !errors.Is(err, csvauth.ErrNotFound) {
if err == nil {
return cred, err
}
return nil, err
}
// Basic Auth was used to carry a token (username is a token name like "", "api", "apikey").
token := password
if password == "" {
token = username
}
return smsAuth.Authenticate("", token)
}
// 2. Try Authorization: <scheme> <token>
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 {
token := strings.TrimSpace(parts[1])
return smsAuth.Authenticate("", token)
}
return nil, csvauth.ErrUnauthorized
}
// 3. Try API-Key / X-API-Key headers
for _, h := range []string{"API-Key", "X-API-Key"} {
if key := r.Header.Get(h); key != "" {
return smsAuth.Authenticate("", key)
}
}
return nil, csvauth.ErrNotFound
}
// getAESKey reads an AES-128 key (32 hex chars) from an environment variable.
// Returns a zero key if the variable is absent or invalid.
func getAESKey(envname string) []byte {
if v := os.Getenv(envname); v != "" {
key, err := hex.DecodeString(strings.TrimSpace(v))
if err == nil && len(key) == 16 {
return key
}
log.Printf("Warning: %s is invalid (must be 32 hex chars); using zero key", envname)
}
return make([]byte, 16)
}
// parseSinceLimit extracts the "since" (ISO datetime) and "limit" query parameters.
func parseSinceLimit(r *http.Request) (time.Time, int) {
var since time.Time
if s := r.URL.Query().Get("since"); s != "" {
for _, format := range []string{time.RFC3339, "2006-01-02T15:04:05-0700", "2006-01-02"} {
if t, err := time.Parse(format, s); err == nil {
since = t
break
}
}
}
limit := 10000
if l := r.URL.Query().Get("limit"); l != "" {
if n, err := strconv.Atoi(strings.ReplaceAll(l, "_", "")); err == nil && n > 0 {
limit = n
}
}
return since, limit
}
func handlerReceivedCSV(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]*androidsmsgateway.WebhookReceived, 0, min(len(webhookEvents), limit))
for _, event := range webhookEvents {
recv, ok := event.(*androidsmsgateway.WebhookReceived)
if !ok {
continue
}
if !since.IsZero() && !recv.Payload.ReceivedAt.After(since) {
continue
}
rows = append(rows, recv)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
b, err := csvutil.Marshal(rows)
if err != nil {
http.Error(w, `{"error":"failed to encode CSV"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
_, _ = w.Write(b)
}
func handlerReceivedJSON(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]*androidsmsgateway.WebhookReceived, 0, min(len(webhookEvents), limit))
for _, event := range webhookEvents {
recv, ok := event.(*androidsmsgateway.WebhookReceived)
if !ok {
continue
}
if !since.IsZero() && !recv.Payload.ReceivedAt.After(since) {
continue
}
rows = append(rows, recv)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
_ = enc.Encode(rows)
}
func handlerSentCSV(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]*androidsmsgateway.WebhookSent, 0, min(len(webhookEvents), limit))
for _, event := range webhookEvents {
sent, ok := event.(*androidsmsgateway.WebhookSent)
if !ok {
continue
}
if !since.IsZero() && !sent.Payload.SentAt.After(since) {
continue
}
rows = append(rows, sent)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
b, err := csvutil.Marshal(rows)
if err != nil {
http.Error(w, `{"error":"failed to encode CSV"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
_, _ = w.Write(b)
}
func handlerSentJSON(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]*androidsmsgateway.WebhookSent, 0, min(len(webhookEvents), limit))
for _, event := range webhookEvents {
sent, ok := event.(*androidsmsgateway.WebhookSent)
if !ok {
continue
}
if !since.IsZero() && !sent.Payload.SentAt.After(since) {
continue
}
rows = append(rows, sent)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
_ = enc.Encode(rows)
}
func handlerPingCSV(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]pingRow, 0, min(len(pingEvents), limit))
for _, ping := range pingEvents {
pingedAt := time.UnixMilli(ping.XTimestamp).UTC()
if !since.IsZero() && !pingedAt.After(since) {
continue
}
h := ping.Payload.Health
row := pingRow{
DeviceID: ping.DeviceID,
Event: ping.Event,
ID: ping.ID,
WebhookID: ping.WebhookID,
PingedAt: pingedAt,
Status: h.Status,
Version: h.Version,
BatteryLevel: h.Checks["battery:level"].ObservedValue,
BatteryCharging: h.Checks["battery:charging"].ObservedValue,
ConnectionStatus: h.Checks["connection:status"].ObservedValue,
ConnectionTransport: h.Checks["connection:transport"].ObservedValue,
ConnectionCellular: h.Checks["connection:cellular"].ObservedValue,
MessagesFailed: h.Checks["messages:failed"].ObservedValue,
}
rows = append(rows, row)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
b, err := csvutil.Marshal(rows)
if err != nil {
http.Error(w, `{"error":"failed to encode CSV"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
_, _ = w.Write(b)
}
func handlerPingJSON(w http.ResponseWriter, r *http.Request) {
since, limit := parseSinceLimit(r)
webhookMux.Lock()
rows := make([]*androidsmsgateway.WebhookPing, 0, min(len(pingEvents), limit))
for _, ping := range pingEvents {
pingedAt := time.UnixMilli(ping.XTimestamp).UTC()
if !since.IsZero() && !pingedAt.After(since) {
continue
}
rows = append(rows, ping)
if len(rows) >= limit {
break
}
}
webhookMux.Unlock()
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
_ = enc.Encode(rows)
}
func HandleOK(w http.ResponseWriter, r *http.Request) {
@ -256,6 +597,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
switch h.GetEvent() {
case "system:ping":
ping := h.(*androidsmsgateway.WebhookPing)
webhookMux.Lock()
defer webhookMux.Unlock()
if err := pingWriter.Write(ping); err != nil {
http.Error(w, `{"error":"failed to save ping"}`, http.StatusOK)
return
}
pingEvents = append(pingEvents, ping)
case "mms:received", "sms:received", "sms:data-received", "sms:sent", "sms:delivered", "sms:failed":
webhookMux.Lock()
defer webhookMux.Unlock()
@ -264,8 +614,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
return
}
webhookEvents = append(webhookEvents, h)
case "system:ping":
// nothing to do yet
default:
http.Error(w, `{"error":"unknown webhook event"}`, http.StatusOK)
return
@ -325,3 +673,24 @@ func readWebhooks(f io.Reader) ([]androidsmsgateway.WebhookEvent, error) {
}
return webhooks, nil
}
func readPings(f io.Reader) ([]*androidsmsgateway.WebhookPing, error) {
var pings []*androidsmsgateway.WebhookPing
r := jsonl.NewReader(f)
err := r.ReadLines(func(line []byte) error {
if len(line) == 0 {
return nil
}
var ping androidsmsgateway.WebhookPing
if err := json.Unmarshal(line, &ping); err != nil {
return fmt.Errorf("could not unmarshal into WebhookPing: %w", err)
}
pings = append(pings, &ping)
return nil
})
if err != nil {
return pings, fmt.Errorf("failed to read JSONL lines: %w", err)
}
return pings, nil
}

View File

@ -24,13 +24,13 @@ type Webhook struct {
// 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"`
DeviceID string `json:"deviceId" csv:"deviceId"`
Event string `json:"event" csv:"event"`
ID string `json:"id" csv:"id"`
Payload WebhookSentPayload `json:"payload" csv:",inline"`
WebhookID string `json:"webhookId" csv:"webhookId"`
XSignature string `json:"x-signature" csv:"-"`
XTimestamp int64 `json:"x-timestamp" csv:"-"`
}
// GetEvent marks WebhookSent as part of the WebhookEvent interface.
@ -40,10 +40,10 @@ func (w *WebhookSent) GetEvent() string {
// 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"`
MessageID string `json:"messageId" csv:"messageId"`
PartsCount int `json:"partsCount" csv:"partsCount"`
PhoneNumber string `json:"phoneNumber" csv:"phoneNumber"`
SentAt time.Time `json:"sentAt" csv:"sentAt"`
}
// WebhookDelivered represents a webhook notification for an SMS delivered event.
@ -71,13 +71,13 @@ type WebhookDeliveredPayload struct {
// 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"`
DeviceID string `json:"deviceId" csv:"deviceId"`
Event string `json:"event" csv:"event"`
ID string `json:"id" csv:"id"`
Payload WebhookReceivedPayload `json:"payload" csv:",inline"`
WebhookID string `json:"webhookId" csv:"webhookId"`
XSignature string `json:"x-signature" csv:"-"`
XTimestamp int64 `json:"x-timestamp" csv:"-"`
}
// GetEvent marks WebhookDelivered as part of the WebhookEvent interface.
@ -87,11 +87,11 @@ func (w *WebhookReceived) GetEvent() string {
// 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"`
Message string `json:"message" csv:"message"`
MessageID string `json:"messageId" csv:"messageId"`
PhoneNumber string `json:"phoneNumber" csv:"phoneNumber"`
ReceivedAt time.Time `json:"receivedAt" csv:"receivedAt"`
SimNumber int `json:"simNumber" csv:"simNumber"`
}
// WebhookPing represents a system:ping webhook event.
@ -134,6 +134,18 @@ type HealthCheck struct {
// 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 "system:ping":
var ping WebhookPing
ping.DeviceID = webhook.DeviceID
ping.Event = webhook.Event
ping.ID = webhook.ID
ping.WebhookID = webhook.WebhookID
ping.XSignature = webhook.XSignature
ping.XTimestamp = webhook.XTimestamp
if err := json.Unmarshal(webhook.Payload, &ping.Payload); err != nil {
return nil, errors.New("failed to decode system:ping payload: " + err.Error())
}
return &ping, nil
case "sms:sent":
var sent WebhookSent
sent.DeviceID = webhook.DeviceID