wip: sendsms use csvutil

This commit is contained in:
AJ ONeal 2026-01-26 03:06:02 -07:00
parent 65b6438f6d
commit 71e41ecdad
No known key found for this signature in database
2 changed files with 71 additions and 92 deletions

View File

@ -15,7 +15,7 @@ import (
"github.com/therootcompany/golib/net/smsgw" "github.com/therootcompany/golib/net/smsgw"
"github.com/therootcompany/golib/net/smsgw/androidsmsgateway" "github.com/therootcompany/golib/net/smsgw/androidsmsgateway"
"github.com/therootcompany/golib/net/smsgw/smscsv" "github.com/therootcompany/golib/net/smsgw/smscsv"
"github.com/therootcompany/golib/net/smsgw/smstmpl" "github.com/therootcompany/golib/text/textvars"
) )
type MainConfig struct { type MainConfig struct {
@ -124,7 +124,7 @@ func main() {
csvr := csv.NewReader(file) csvr := csv.NewReader(file)
csvr.FieldsPerRecord = -1 csvr.FieldsPerRecord = -1
messages, warns, err := smscsv.ReadOrIgnoreAll(csvr) messages, warns, err := smscsv.ReadOrIgnoreAll(csvr, "Name")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err) fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err)
os.Exit(1) os.Exit(1)
@ -144,12 +144,21 @@ func main() {
} }
fmt.Fprintf(os.Stderr, "Info: list of %d messages\n", len(messages)) fmt.Fprintf(os.Stderr, "Info: list of %d messages\n", len(messages))
messages, err = smstmpl.RenderAll(messages) for i, message := range messages {
rowIndex := i + 1
text, err := textvars.ReplaceVars(message.Template, message.Map())
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err) fmt.Fprintf(os.Stderr,
"\n%sError%s: failing due to row %d (%s): %v",
textErr, textReset, rowIndex, message.Get("Name"), err,
)
os.Exit(1) os.Exit(1)
} }
message.Text = text
messages[i] = message
}
if now.After(cfg.endTime) || now.Equal(cfg.endTime) { if now.After(cfg.endTime) || now.Equal(cfg.endTime) {
fmt.Fprintf(os.Stderr, "%sWarning%s: Too late now. %sWaiting until tomorrow%s:\n", textWarn, textReset, textWarn, textReset) fmt.Fprintf(os.Stderr, "%sWarning%s: Too late now. %sWaiting until tomorrow%s:\n", textWarn, textReset, textWarn, textReset)
@ -239,9 +248,9 @@ func main() {
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Info: This is what a %ssample message%s from list look like:\n", textInfo, textReset) fmt.Fprintf(os.Stderr, "Info: This is what a %ssample message%s from list look like:\n", textInfo, textReset)
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " To: %s (%s)\n", messages[0].Number, messages[0].Name()) fmt.Fprintf(os.Stderr, " To: %s (%s)\n", messages[0].Number, messages[0].Get("Name"))
fmt.Fprintf(os.Stderr, " %s%s%s\n", textTmpl, messages[0].Template(), textReset) fmt.Fprintf(os.Stderr, " %s%s%s\n", textTmpl, messages[0].Template, textReset)
fmt.Fprintf(os.Stderr, " %s%s%s\n", textInfo, messages[0].Text(), textReset) fmt.Fprintf(os.Stderr, " %s%s%s\n", textInfo, messages[0].Text, textReset)
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
if !cfg.confirmed && !cfg.dryRun { if !cfg.confirmed && !cfg.dryRun {
@ -290,16 +299,16 @@ func main() {
fmt.Fprintf(os.Stderr, "# Send to %s (%s) %s-%s\n", message.Number[:2], message.Number[2:5], message.Number[5:8], message.Number[8:]) fmt.Fprintf(os.Stderr, "# Send to %s (%s) %s-%s\n", message.Number[:2], message.Number[2:5], message.Number[5:8], message.Number[8:])
if cfg.printCurl { if cfg.printCurl {
curl := sender.CurlString(message.Number, message.Text()) curl := sender.CurlString(message.Number, message.Text)
fmt.Println(curl) fmt.Println(curl)
} else { } else {
fmt.Fprintf(os.Stderr, "%s\n", message.Text()) fmt.Fprintf(os.Stderr, "%s\n", message.Text)
} }
if cfg.dryRun { if cfg.dryRun {
continue continue
} }
if err := sender.Send(message.Number, message.Text()); err != nil { if err := sender.Send(message.Number, message.Text); err != nil {
fmt.Fprintf(os.Stderr, "%sError%s: %v\n", textErr, textReset, err) fmt.Fprintf(os.Stderr, "%sError%s: %v\n", textErr, textReset, err)
continue continue
} }

View File

@ -3,6 +3,7 @@ package smscsv
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"slices" "slices"
"strings" "strings"
@ -26,121 +27,90 @@ func (w CSVWarn) Error() string {
} }
type Message struct { type Message struct {
Record `csv:"*"`
Number string `csv:"Phone"`
Template string `csv:"Message"`
Text string `csv:"-"`
}
type Record struct {
header []string header []string
indices map[string]int
fields []string fields []string
name string
Number string
template string
Vars map[string]string
text string
} }
func (m Message) Name() string { func (r Record) Keys() []string {
return m.name return r.header
} }
func (m Message) Template() string { func (r Record) Get(key string) string {
return m.template // typically there are only a few fields, so indexing is faster than mapping
} i := slices.Index(r.header, key)
if i < 0 {
func (m Message) Text() string {
return m.text
}
func (m *Message) SetText(text string) {
m.text = text
}
func (m Message) Size() int {
return len(m.fields)
}
func (m Message) Get(key string) string {
index, ok := m.indices[key]
if !ok {
return "" return ""
} }
if len(m.fields) >= 1+index { return r.fields[i]
return m.fields[index] }
}
return "" func (r Record) Map() map[string]string {
m := make(map[string]string, len(r.header))
for i, k := range r.header {
m[k] = r.fields[i]
}
return m
} }
// TODO XXX AJ pass in column name mapping // TODO XXX AJ pass in column name mapping
func ReadOrIgnoreAll(csvr Reader) (messages []Message, warns []CSVWarn, err error) { func ReadOrIgnoreAll(csvr Reader, labelKey string) (messages []Message, warns []CSVWarn, err error) {
header, err := csvr.Read() dec, err := csvutil.NewDecoder(csvr)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("header could not be parsed: %w", err) return nil, nil, err
// fmt.Fprintf(os.Stderr, "\n%sError%s: %v\n", textErr, textReset, err)
// os.Exit(1)
} }
FIELD_NAME := GetFieldIndex(header, "Name") header := dec.Header()
FIELD_PHONE := GetFieldIndex(header, "Phone") if GetFieldIndex(header, "Phone") == -1 || GetFieldIndex(header, "Message") == -1 {
FIELD_MESSAGE := GetFieldIndex(header, "Message")
if FIELD_NAME == -1 || FIELD_PHONE == -1 || FIELD_MESSAGE == -1 {
return nil, nil, fmt.Errorf("header is missing one or more of 'Name', 'Phone', and/or 'Message'") return nil, nil, fmt.Errorf("header is missing one or more of 'Name', 'Phone', and/or 'Message'")
} }
FIELD_MIN := 1 + slices.Max([]int{FIELD_NAME, FIELD_PHONE, FIELD_MESSAGE})
var unusedHeader []string
rowIndex := 1 // 1-index, start at header rowIndex := 1 // 1-index, start at header
for { for {
rowIndex++ rowIndex++
rec, err := csvr.Read()
if err == io.EOF { m := Record(header)
if err := dec.Decode(&m); err == io.EOF {
break break
} } else if err != nil {
if err != nil { log.Fatal(err)
return nil, nil, fmt.Errorf("failed to parse row %d (and all following rows): %w", rowIndex, err)
} }
// TODO XXX AJ create an abstraction around the header []string and the record []string // TODO we can't use this optimization when the fields have different lengths
// the idea is to return the same thing for valid and invalid rows if unusedHeader == nil {
vars := make(map[string]string) ids := dec.Unused()
n := min(len(header), len(rec)) unusedHeader = make([]string, len(ids))
for i := range n {
switch i {
case FIELD_NAME, FIELD_PHONE, FIELD_MESSAGE:
continue
default:
key := header[i]
val := rec[i]
vars[key] = val
} }
m.Fields = Record{
header: unusedHeader,
fields: make([]string, len(unusedHeader)),
}
for _, i := range dec.Unused() {
m.Fields.fields[i] = dec.Record()[i]
} }
if len(rec) < FIELD_MIN { m.Number = smsgw.StripFormatting(m.Number)
warns = append(warns, CSVWarn{ m.Number, err = smsgw.PrefixUS10Digit(m.Number)
Index: rowIndex,
Code: "TooFewFields",
Message: fmt.Sprintf("ignoring row %d: too few fields (want %d, have %d)", rowIndex, FIELD_MIN, len(rec)),
Record: rec,
})
continue
}
message := Message{
// Index: rowIndex,
name: strings.TrimSpace(rec[FIELD_NAME]),
Number: strings.TrimSpace(rec[FIELD_PHONE]),
template: strings.TrimSpace(rec[FIELD_MESSAGE]),
Vars: vars,
}
message.Number = smsgw.StripFormatting(message.Number)
message.Number, err = smsgw.PrefixUS10Digit(message.Number)
if err != nil { if err != nil {
warns = append(warns, CSVWarn{ warns = append(warns, CSVWarn{
Index: rowIndex, Index: rowIndex,
Code: "PhoneInvalid", Code: "PhoneInvalid",
Message: fmt.Sprintf("ignoring row %d (%s): %s", rowIndex, message.Name(), err.Error()), Message: fmt.Sprintf("ignoring row %d (%s): %s", rowIndex, m.Fields.Get(labelKey), err.Error()),
// Record: rec, // Record: rec,
}) })
continue continue
} }
messages = append(messages, m)
messages = append(messages, message)
} }
return messages, warns, nil return messages, warns, nil