diff --git a/cmd/smsgwhookstocsv/go.mod b/cmd/smsgwhookstocsv/go.mod new file mode 100644 index 0000000..1fdaf87 --- /dev/null +++ b/cmd/smsgwhookstocsv/go.mod @@ -0,0 +1,5 @@ +module github.com/therootcompany/golib/cmd/smsgwhookstocsv + +go 1.25.0 + +require github.com/jszwec/csvutil v1.10.0 diff --git a/cmd/smsgwhookstocsv/go.sum b/cmd/smsgwhookstocsv/go.sum new file mode 100644 index 0000000..6a61a22 --- /dev/null +++ b/cmd/smsgwhookstocsv/go.sum @@ -0,0 +1,2 @@ +github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI= +github.com/jszwec/csvutil v1.10.0/go.mod h1:/E4ONrmGkwmWsk9ae9jpXnv9QT8pLHEPcCirMFhxG9I= diff --git a/cmd/smsgwhookstocsv/main.go b/cmd/smsgwhookstocsv/main.go new file mode 100644 index 0000000..5111363 --- /dev/null +++ b/cmd/smsgwhookstocsv/main.go @@ -0,0 +1,133 @@ +// smsgwhooks-to-tsv - Converts messages.jsonl webhook events to TSV/CSV +// +// Authored in 2026 by AJ ONeal , assisted by Grok Ai. +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along with +// this software. If not, see . +package main + +import ( + "encoding/csv" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "os" + + "github.com/jszwec/csvutil" +) + +type SmsReceivedPayload struct { + Message string `json:"message" csv:"message"` + MessageID string `json:"messageId" csv:"messageId"` + PhoneNumber string `json:"phoneNumber" csv:"phoneNumber"` + ReceivedAt string `json:"receivedAt" csv:"receivedAt"` + SimNumber int `json:"simNumber" csv:"simNumber"` +} + +type WebhookEvent struct { + DeviceID string `json:"deviceId" csv:"deviceId"` + Event string `json:"event" csv:"event"` + ID string `json:"id" csv:"id"` + Payload SmsReceivedPayload `json:"payload" csv:",inline"` + WebhookID string `json:"webhookId" csv:"webhookId"` + XSignature string `json:"x-signature" csv:"x-signature"` + XTimestamp int64 `json:"x-timestamp" csv:"x-timestamp"` +} + +type MainConfig struct { + out string + comma string + in string +} + +func main() { + cfg := MainConfig{} + + fs := flag.NewFlagSet("smsgwhooks-to-tsv", flag.ExitOnError) + + fs.StringVar(&cfg.out, "o", "-", "output file ('-' = stdout)") + fs.StringVar(&cfg.comma, "comma", "\t", "field separator (default: tab)") + + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: jsonl-to-tsv [flags] [input.jsonl]\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "Converts JSONL webhook events to TSV (or CSV with custom separator).\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + fs.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "Examples:\n") + fmt.Fprintf(os.Stderr, " jsonl-to-tsv messages.jsonl\n") + fmt.Fprintf(os.Stderr, " cat messages.jsonl | jsonl-to-tsv -\n") + fmt.Fprintf(os.Stderr, " jsonl-to-tsv -o ./out.tsv messages.jsonl\n") + fmt.Fprintf(os.Stderr, " jsonl-to-tsv --comma , -o ./out.csv messages.jsonl\n") + fmt.Fprintf(os.Stderr, " jsonl-to-tsv -o - messages.jsonl # explicit stdout\n") + } + + _ = fs.Parse(os.Args[1:]) + + fi, _ := os.Stdin.Stat() + isProbablyPipe := (fi.Mode() & os.ModeCharDevice) == 0 + if fs.NArg() == 0 && !isProbablyPipe { + fs.Usage() + os.Exit(1) + } + + var in io.Reader = os.Stdin + if cfg.in = fs.Arg(0); cfg.in != "" && cfg.in != "-" { + f, err := os.Open(cfg.in) + if err != nil { + log.Fatalf("open input: %v", err) + } + defer func() { _ = f.Close() }() + in = f + } + + var out io.Writer = os.Stdout + if cfg.out != "-" { + f, err := os.Create(cfg.out) + if err != nil { + log.Fatalf("create output: %v", err) + } + defer func() { _ = f.Close() }() + out = f + } + + csvWriter := csv.NewWriter(out) + csvWriter.Comma = rune((cfg.comma)[0]) + csvWriter.UseCRLF = false + + enc := csvutil.NewEncoder(csvWriter) + + // write header + _ = enc.EncodeHeader(WebhookEvent{}) + + dec := json.NewDecoder(in) + for dec.More() { + var evt WebhookEvent + if err := dec.Decode(&evt); err != nil { + if err == io.EOF { + break + } + log.Printf("decode error, skipping: %v", err) + continue + } + if evt.Event != "sms:received" { + continue + } + + if err := enc.Encode(evt); err != nil { + log.Fatalf("encode error: %v", err) + } + } + + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + log.Printf("flush error: %v", err) + } +}