From 6ee3703b6dc70fefb0e6bf93e8b62bdc84f6520a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Wed, 10 Jan 2024 11:26:02 +0800 Subject: [PATCH] feat(sms): Added SMS(with `mitake` provider) (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sms): Added SMS Signed-off-by: Flc゛ * feat(sms): fix lint Signed-off-by: Flc゛ * feat(sms): fix lint Signed-off-by: Flc゛ * test(sms): Increased unit test coverage. Signed-off-by: Flc゛ * test(sms): Increased unit test coverage. Signed-off-by: Flc゛ --------- Signed-off-by: Flc゛ --- go.mod | 3 +- go.sum | 4 ++ sms/mitake/provider.go | 104 ++++++++++++++++++++++++++++++++++++ sms/mitake/provider_test.go | 58 ++++++++++++++++++++ sms/null_provider.go | 13 +++++ sms/provider.go | 7 +++ sms/sms.go | 63 ++++++++++++++++++++++ sms/sms_test.go | 19 +++++++ 8 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 sms/mitake/provider.go create mode 100644 sms/mitake/provider_test.go create mode 100644 sms/null_provider.go create mode 100644 sms/provider.go create mode 100644 sms/sms.go create mode 100644 sms/sms_test.go diff --git a/go.mod b/go.mod index 53c16237..e1c73ee2 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/redis/go-redis/v9 v9.4.0 github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.8.4 + golang.org/x/text v0.13.0 gorm.io/gorm v1.25.5 ) @@ -42,8 +43,8 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/net v0.17.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/grpc v1.60.1 // indirect google.golang.org/protobuf v1.32.0 // indirect diff --git a/go.sum b/go.sum index cc4ac7f6..f930ac55 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= @@ -103,6 +105,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= diff --git a/sms/mitake/provider.go b/sms/mitake/provider.go new file mode 100644 index 00000000..0500faaa --- /dev/null +++ b/sms/mitake/provider.go @@ -0,0 +1,104 @@ +// Package mitake is a sms provider for mitake. +// See: https://sms.mitake.com.tw/ +package mitake + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "golang.org/x/text/encoding/traditionalchinese" + + "github.com/go-kratos-ecosystem/components/v2/sms" +) + +type provider struct { + api string + username string + password string + + httpClient *http.Client +} + +type Option func(t *provider) + +func WithAPI(api string) Option { + return func(t *provider) { + t.api = api + } +} + +func WithHTTPClient(httpClient *http.Client) Option { + return func(t *provider) { + t.httpClient = httpClient + } +} + +func New(username, password string, opts ...Option) sms.Provider { + p := &provider{ + api: "https://smsapi.mitake.com.tw", + username: username, + password: password, + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(p) + } + + return p +} + +func (p *provider) Send(ctx context.Context, phone *sms.Phone, message *sms.Message) error { + if err := p.verify(phone, message); err != nil { + return err + } + + // Convert to Big5 + text, err := traditionalchinese.Big5.NewEncoder().String(message.Text) + if err != nil { + return err + } + + // Combine params + params := url.Values{} + params.Set("username", p.username) + params.Set("password", p.password) + params.Set("type", "now") + params.Set("encoding", "big5") + params.Set("dstaddr", phone.Number) + params.Set("smbody", text) + + // new request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.api+"?"+params.Encode(), nil) + if err != nil { + return err + } + + // send request + resp, err := p.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // check response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("sms mitake: http status code: %d", resp.StatusCode) + } + + return nil +} + +func (p *provider) verify(phone *sms.Phone, message *sms.Message) error { + if phone.Number == "" { + return sms.ErrInvalidPhone + } + + if message.Text == "" { + return sms.ErrInvalidMessage + } + + return nil +} diff --git a/sms/mitake/provider_test.go b/sms/mitake/provider_test.go new file mode 100644 index 00000000..612e222e --- /dev/null +++ b/sms/mitake/provider_test.go @@ -0,0 +1,58 @@ +package mitake + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-kratos-ecosystem/components/v2/sms" +) + +func TestProvider(t *testing.T) { + var ( + username = "test" + password = "test" + number = "123456789" + text = "Hello, world" + ) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatal("method error") + } + + if r.URL.Query().Get("username") != username { + t.Fatal("username error") + } + + if r.URL.Query().Get("password") != password { + t.Fatal("password error") + } + + if r.URL.Query().Get("dstaddr") != number { + t.Fatal("dstaddr error") + } + + if r.URL.Query().Get("smbody") != text { + t.Fatal("smbody error") + } + + w.Write([]byte("hello")) //nolint:errcheck + })) + defer srv.Close() + + p := New(username, password, + WithAPI(srv.URL), + WithHTTPClient(http.DefaultClient), + ) + + err := p.Send(context.Background(), &sms.Phone{ + Number: number, + }, &sms.Message{ + Text: text, + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/sms/null_provider.go b/sms/null_provider.go new file mode 100644 index 00000000..442d6d8a --- /dev/null +++ b/sms/null_provider.go @@ -0,0 +1,13 @@ +package sms + +import "context" + +type NullProvider struct{} + +func NewNullProvider() Provider { + return &NullProvider{} +} + +func (p *NullProvider) Send(_ context.Context, _ *Phone, _ *Message) error { + return nil +} diff --git a/sms/provider.go b/sms/provider.go new file mode 100644 index 00000000..2a07a6c8 --- /dev/null +++ b/sms/provider.go @@ -0,0 +1,7 @@ +package sms + +import "context" + +type Provider interface { + Send(ctx context.Context, phone *Phone, message *Message) error +} diff --git a/sms/sms.go b/sms/sms.go new file mode 100644 index 00000000..d7cafeaa --- /dev/null +++ b/sms/sms.go @@ -0,0 +1,63 @@ +package sms + +import ( + "context" + "errors" + "time" +) + +var ( + ErrInvalidPhone = errors.New("sms: invalid phone") + ErrInvalidMessage = errors.New("sms: invalid message") +) + +type Phone struct { + IDDCode string + Number string +} + +type Message struct { + Text string + Template string + Variables map[string]string + Schedule time.Time +} + +type Sms struct { + gw Provider +} + +func New(gw Provider) *Sms { + return &Sms{ + gw: gw, + } +} + +func (s *Sms) Send(ctx context.Context, phone *Phone, message *Message) error { + return s.gw.Send(ctx, phone, message) +} + +func (s *Sms) SendText(ctx context.Context, phone *Phone, text string) error { + return s.Send(ctx, phone, &Message{ + Text: text, + }) +} + +func (s *Sms) SendTemplate(ctx context.Context, phone *Phone, template string, variables map[string]string) error { + return s.Send(ctx, phone, &Message{ + Template: template, + Variables: variables, + }) +} + +func (s *Sms) SendTextWithNumber(ctx context.Context, phoneNumber, text string) error { + return s.SendText(ctx, &Phone{ + Number: phoneNumber, + }, text) +} + +func (s *Sms) SendTemplateWithNumber(ctx context.Context, phoneNumber, template string, variables map[string]string) error { //nolint:lll + return s.SendTemplate(ctx, &Phone{ + Number: phoneNumber, + }, template, variables) +} diff --git a/sms/sms_test.go b/sms/sms_test.go new file mode 100644 index 00000000..f91af163 --- /dev/null +++ b/sms/sms_test.go @@ -0,0 +1,19 @@ +package sms + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSms(t *testing.T) { + sms := New(NewNullProvider()) + ctx := context.Background() + + assert.NoError(t, sms.Send(ctx, nil, nil)) + assert.NoError(t, sms.SendText(ctx, nil, "")) + assert.NoError(t, sms.SendTemplate(ctx, nil, "", nil)) + assert.NoError(t, sms.SendTextWithNumber(ctx, "", "")) + assert.NoError(t, sms.SendTemplateWithNumber(ctx, "", "", nil)) +}