diff --git a/cmd/smsgate/smsgate.go b/cmd/smsgate/smsgate.go index b8e2708..ef5f06b 100644 --- a/cmd/smsgate/smsgate.go +++ b/cmd/smsgate/smsgate.go @@ -16,6 +16,11 @@ import ( "github.com/urfave/cli/v2" ) +const ( + categoryConfiguration = "Configuration" + categoryOutput = "Output" +) + var ( version = "0.0.0" ) @@ -41,7 +46,7 @@ func main() { Name: "endpoint", DefaultText: config.DefaultEndpoint, Usage: "Endpoint", - Category: "Configuration", + Category: categoryConfiguration, Aliases: []string{"e"}, Value: config.DefaultEndpoint, EnvVars: []string{ @@ -52,7 +57,7 @@ func main() { Name: "username", Aliases: []string{"u"}, Usage: "Username", - Category: "Configuration", + Category: categoryConfiguration, EnvVars: []string{ "ASG_USERNAME", }, @@ -62,7 +67,7 @@ func main() { Name: "password", Aliases: []string{"p"}, Usage: "Password", - Category: "Configuration", + Category: categoryConfiguration, EnvVars: []string{ "ASG_PASSWORD", }, @@ -71,7 +76,7 @@ func main() { &cli.StringFlag{ Name: "format", - Category: "Output", + Category: categoryOutput, Usage: "Output format. Supported: text, json, raw", Required: false, Value: string(output.Text), diff --git a/go.mod b/go.mod index d8c0f50..b8ec7f7 100644 --- a/go.mod +++ b/go.mod @@ -3,28 +3,29 @@ module github.com/android-sms-gateway/cli go 1.25.0 require ( - github.com/android-sms-gateway/client-go v1.12.0 + github.com/android-sms-gateway/client-go v1.12.10 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - github.com/samber/lo v1.52.0 - github.com/stretchr/testify v1.10.0 - github.com/urfave/cli/v2 v2.27.5 - github.com/xuri/excelize/v2 v2.9.0 + github.com/samber/lo v1.53.0 + github.com/stretchr/testify v1.11.1 + github.com/urfave/cli/v2 v2.27.7 + github.com/xuri/excelize/v2 v2.10.1 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect - github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/text v0.35.0 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7231398..60b0a39 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ github.com/android-sms-gateway/client-go v1.12.0 h1:4YWnzLi4AWEQIXvvUSLiTK+ZAgQD6SV+xfvlTCjZ8pI= github.com/android-sms-gateway/client-go v1.12.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.12.10 h1:wnCfED/3gXpeq957qeB16eh4J782tjkQh5XoZ+RgSa0= +github.com/android-sms-gateway/client-go v1.12.10/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -14,33 +18,60 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/commands/flags/send.go b/internal/commands/flags/send.go index 614dcc0..c32c98c 100644 --- a/internal/commands/flags/send.go +++ b/internal/commands/flags/send.go @@ -9,62 +9,75 @@ import ( "github.com/urfave/cli/v2" ) +const ( + categoryOptions = "Options" + categoryBody = "Body" +) + func Send() []cli.Flag { return []cli.Flag{ // Body fields &cli.StringFlag{ Name: "device-id", Aliases: []string{"device", "deviceId"}, - Category: "Body", + Category: categoryBody, Usage: "Optional device ID for explicit selection", DefaultText: "auto", }, &cli.UintFlag{ Name: "sim-number", Aliases: []string{"simNumber", "sim"}, - Category: "Body", + Category: categoryBody, Usage: "SIM card index (one-based index, e.g. 1)", DefaultText: "see device settings", }, &cli.BoolFlag{ Name: "delivery-report", Aliases: []string{"deliveryReport"}, - Category: "Body", + Category: categoryBody, Usage: "Enable delivery report", Value: true, }, &cli.IntFlag{ Name: "priority", - Category: "Body", + Category: categoryBody, Usage: "Priority, use >= 100 to bypass all limits and delays (-128 to 127)", Value: 0, }, &cli.DurationFlag{ Name: "ttl", - Category: "Options", + Category: categoryOptions, Usage: "Time to live (duration, e.g. 1h30m)", DefaultText: "unlimited", }, &cli.TimestampFlag{ Name: "valid-until", Aliases: []string{"validUntil"}, - Category: "Options", + Category: categoryOptions, Usage: "Valid until (RFC3339 format, e.g. 2006-01-02T15:04:05Z07:00)", Layout: time.RFC3339, Timezone: time.Local, }, + &cli.TimestampFlag{ + Name: "schedule-at", + Aliases: []string{"scheduleAt"}, + Category: categoryOptions, + Usage: "Schedule message delivery at a specific time (RFC3339 format, e.g. 2006-01-02T15:04:05Z07:00)", + Layout: time.RFC3339, + Timezone: time.Local, + }, &cli.BoolFlag{ Name: "skip-phone-validation", Aliases: []string{"skipPhoneValidation"}, - Category: "Options", + Category: categoryOptions, Usage: "Skip phone number validation", Value: false, }, &cli.UintFlag{ Name: "device-active-within", Aliases: []string{"deviceActiveWithin"}, - Category: "Options", + Category: categoryOptions, Usage: "Filter devices active within the specified number of hours", Value: 0, }, @@ -78,6 +91,7 @@ type SendFlags struct { Priority smsgateway.MessagePriority TTL *uint64 ValidUntil *time.Time + ScheduleAt *time.Time SkipPhoneValidation bool DeviceActiveWithin uint } @@ -91,6 +105,7 @@ func NewSendFlags(c *cli.Context) (*SendFlags, error) { ValidUntil: c.Timestamp("valid-until"), SkipPhoneValidation: c.Bool("skip-phone-validation"), DeviceActiveWithin: c.Uint("device-active-within"), + ScheduleAt: c.Timestamp("schedule-at"), SimNumber: nil, Priority: smsgateway.PriorityDefault, @@ -127,6 +142,16 @@ func NewSendFlags(c *cli.Context) (*SendFlags, error) { } fl.ValidUntil = validUntil + scheduleAt := c.Timestamp("schedule-at") + if scheduleAt != nil && !scheduleAt.After(time.Now()) { + return nil, fmt.Errorf( + "%w: Schedule At must be in the future: %s", + ErrValidationFailed, + scheduleAt.Format(time.RFC3339), + ) + } + fl.ScheduleAt = scheduleAt + priority := c.Int( "priority", ) @@ -164,6 +189,7 @@ func (s SendFlags) Merge(src smsgateway.Message) smsgateway.Message { Priority: lo.CoalesceOrEmpty(src.Priority, s.Priority), TTL: lo.CoalesceOrEmpty(src.TTL, s.TTL), ValidUntil: lo.CoalesceOrEmpty(src.ValidUntil, s.ValidUntil), + ScheduleAt: lo.CoalesceOrEmpty(src.ScheduleAt, s.ScheduleAt), } } diff --git a/internal/commands/messages/batch/send.go b/internal/commands/messages/batch/send.go index cc028ee..93f3f14 100644 --- a/internal/commands/messages/batch/send.go +++ b/internal/commands/messages/batch/send.go @@ -318,6 +318,7 @@ func sendWorker( Priority: 0, TTL: nil, ValidUntil: nil, + ScheduleAt: nil, } req = sendFlags.Merge(req) diff --git a/internal/commands/messages/send.go b/internal/commands/messages/send.go index 519ac47..c34b058 100644 --- a/internal/commands/messages/send.go +++ b/internal/commands/messages/send.go @@ -126,6 +126,7 @@ func sendAction(c *cli.Context) error { Priority: 0, TTL: nil, ValidUntil: nil, + ScheduleAt: nil, } req = sendFlags.Merge(req) diff --git a/internal/commands/webhooks/consts.go b/internal/commands/webhooks/consts.go new file mode 100644 index 0000000..7f7b652 --- /dev/null +++ b/internal/commands/webhooks/consts.go @@ -0,0 +1,5 @@ +package webhooks + +const ( + categoryWebhooks = "Webhooks" +) diff --git a/internal/commands/webhooks/delete.go b/internal/commands/webhooks/delete.go index adec174..159ba09 100644 --- a/internal/commands/webhooks/delete.go +++ b/internal/commands/webhooks/delete.go @@ -11,7 +11,7 @@ import ( func deleteCmd() *cli.Command { return &cli.Command{ - Category: "Webhooks", + Category: categoryWebhooks, Name: "delete", Aliases: []string{"d", "rm", "remove"}, Usage: "Delete webhook", diff --git a/internal/commands/webhooks/list.go b/internal/commands/webhooks/list.go index bc1d191..3d1d303 100644 --- a/internal/commands/webhooks/list.go +++ b/internal/commands/webhooks/list.go @@ -11,7 +11,7 @@ import ( func listCmd() *cli.Command { return &cli.Command{ - Category: "Webhooks", + Category: categoryWebhooks, Name: "list", Aliases: []string{"l", "ls"}, Usage: "List webhooks", diff --git a/internal/commands/webhooks/register.go b/internal/commands/webhooks/register.go index 949fa59..7d9a8f6 100644 --- a/internal/commands/webhooks/register.go +++ b/internal/commands/webhooks/register.go @@ -15,7 +15,7 @@ import ( func registerCmd() *cli.Command { return &cli.Command{ - Category: "Webhooks", + Category: categoryWebhooks, Name: "register", Aliases: []string{"r"}, ArgsUsage: "URL", diff --git a/internal/commands/webhooks/webhooks.go b/internal/commands/webhooks/webhooks.go index 5d4ae3b..34867c7 100644 --- a/internal/commands/webhooks/webhooks.go +++ b/internal/commands/webhooks/webhooks.go @@ -7,7 +7,7 @@ import ( func Commands() []*cli.Command { return []*cli.Command{ { - Category: "Webhooks", + Category: categoryWebhooks, Name: "webhooks", Aliases: []string{"w", "wh"}, Usage: "Manage webhooks",