Skip to content

Commit

Permalink
feat(driver): Added mitake and writer (#2)
Browse files Browse the repository at this point in the history
* feat(mitake): Added mitake driver

* feat(driver): Added `mitake` and `writer`

* feat(driver): Added `mitake` and `writer`

* feat(driver): Added `mitake` and `writer`

* feat(driver): Added `mitake` and `writer`
  • Loading branch information
flc1125 authored Mar 11, 2024
1 parent b6adec9 commit e41d2c4
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 2 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,57 @@
# go-sms
# Go SMS

[![Go Version](https://badgen.net/github/release/flc1125/go-sms/stable)](https://github.com/flc1125/go-sms/releases)
[![GoDoc](https://pkg.go.dev/badge/github.com/flc1125/go-sms)](https://pkg.go.dev/github.com/flc1125/go-sms)
[![codecov](https://codecov.io/gh/flc1125/go-sms/graph/badge.svg?token=OglZDxLYxy)](https://codecov.io/gh/flc1125/go-sms)
[![Go Report Card](https://goreportcard.com/badge/github.com/flc1125/go-sms)](https://goreportcard.com/report/github.com/flc1125/go-sms)
[![lint](https://github.com/flc1125/go-sms/actions/workflows/lint.yml/badge.svg)](https://github.com/flc1125/go-sms/actions/workflows/lint.yml)
[![test](https://github.com/flc1125/go-sms/actions/workflows/test.yml/badge.svg)](https://github.com/flc1125/go-sms/actions/workflows/test.yml)
[![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)

## Installation

```bash
go get github.com/flc1125/go-sms
```

## Usage

### sms with driver

```go
package main

import (
"context"
"fmt"
"os"

"github.com/flc1125/go-sms"
"github.com/flc1125/go-sms/driver/writer"
)

func main() {
s := sms.New(
writer.NewDriver(os.Stdout),
)
resp, err := s.Send(context.Background(), &sms.Request{
Phone: "1234567890",
Content: "test",
})
if err != nil {
panic(err)
}
fmt.Println(resp.Status, resp.Message)

// Output: writer: send sms {1234567890 test <nil>}
}
```

### Drivers

- [writer](https://github.com/flc1125/go-sms/tree/main/driver/writer)
- [mitake](https://github.com/flc1125/go-sms/tree/main/driver/mitake)

## License

The MIT License (MIT). Please see [License File](LICENSE) for more information.
26 changes: 26 additions & 0 deletions _example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"context"
"fmt"
"os"

"github.com/flc1125/go-sms"
"github.com/flc1125/go-sms/driver/writer"
)

func main() {
s := sms.New(
writer.NewDriver(os.Stdout),
)
resp, err := s.Send(context.Background(), &sms.Request{
Phone: "1234567890",
Content: "test",
})
if err != nil {
panic(err)
}
fmt.Println(resp.Status, resp.Message)

// Output: writer: send sms {1234567890 test <nil>}
}
193 changes: 193 additions & 0 deletions driver/mitake/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package mitake

import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"strings"

"golang.org/x/text/encoding/traditionalchinese"

"github.com/flc1125/go-sms"
)

type Request struct {
Dstaddr string `json:"dstaddr"`
Smbody string `json:"smbody"`
Type string `json:"type"`
Encoding string `json:"encoding"`
}

func (r *Request) IsRequestExtra() {}

type Response struct {
MsgID string `json:"msgid"`
StatusCode string `json:"statuscode"`
AccountPoint string `json:"AccountPoint"`
Error string `json:"Error"`
}

func (r *Response) IsResponseExtra() {}

type Config struct {
Addr string
Username string
Password string
}

type Driver struct {
config *Config

httpClient *http.Client
}

type Option func(*Driver)

func WithHTTPClient(httpClient *http.Client) Option {
return func(d *Driver) {
d.httpClient = httpClient
}
}

func New(config *Config, opts ...Option) *Driver {
d := &Driver{
config: config,
}

for _, o := range opts {
o(d)
}

if d.httpClient == nil {
d.httpClient = http.DefaultClient
}

return d
}

func (d *Driver) Send(ctx context.Context, req *sms.Request) (*sms.Response, error) {
response, err := d.send(ctx, d.parseRequest(req))
if err != nil {
return &sms.Response{
Status: sms.Failed,
Message: err.Error(),
Extra: response,
}, err
}

return &sms.Response{
Status: sms.Success,
Message: "success",
Extra: response,
}, nil
}

func (d *Driver) parseRequest(req *sms.Request) *Request {
request, ok := req.Extra.(*Request)
if !ok {
request = &Request{
Dstaddr: req.Phone,
Smbody: req.Content,
}
}

if request.Dstaddr == "" && req.Phone != "" {
request.Dstaddr = req.Phone
}

if request.Smbody == "" && req.Content != "" {
request.Smbody = req.Content
}

if request.Type == "" {
request.Type = "now"
}

if request.Encoding == "" {
request.Encoding = "big5"
}

return request
}

func (d *Driver) newRequest(ctx context.Context, req *Request) (*http.Request, error) {
var (
smbody = req.Smbody
err error
)
if req.Encoding == "big5" {
if smbody, err = traditionalchinese.Big5.NewEncoder().String(req.Smbody); err != nil {
return nil, err
}
}

values := url.Values{}
values.Set("username", d.config.Username)
values.Set("password", d.config.Password)
values.Set("dstaddr", req.Dstaddr)
values.Set("smbody", smbody)
values.Set("type", req.Type)
values.Set("encoding", req.Encoding)

return http.NewRequestWithContext(ctx, http.MethodGet, d.config.Addr+"?"+values.Encode(), nil)
}

func (d *Driver) doRequest(request *http.Request) (*Response, error) {
resp, err := d.httpClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("mitake: invalid status code: %d", resp.StatusCode)
}

return d.decodeResponse(resp)
}

func (d *Driver) decodeResponse(resp *http.Response) (*Response, error) {
var buffer bytes.Buffer
if _, err := buffer.ReadFrom(resp.Body); err != nil {
return nil, err
}

var response Response
for _, str := range strings.Split(buffer.String(), "\n") {
if str == "" {
continue
}
kv := strings.Split(str, "=")
if len(kv) != 2 { // nolint:gomnd
continue
}

switch kv[0] {
case "msgid":
response.MsgID = kv[1]
case "statuscode":
response.StatusCode = kv[1]
case "AccountPoint":
response.AccountPoint = kv[1]
case "Error":
response.Error = kv[1]
}
}

if response.StatusCode != "1" {
return &response, fmt.Errorf("mitake: %s", response.Error)
}

return &response, nil
}

func (d *Driver) send(ctx context.Context, req *Request) (*Response, error) {
request, err := d.newRequest(ctx, req)
if err != nil {
return nil, err
}

return d.doRequest(request)
}
117 changes: 117 additions & 0 deletions driver/mitake/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package mitake

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"

"github.com/flc1125/go-sms"
)

var ctx = context.Background()

func TestNew(t *testing.T) {
driver := New(&Config{
Addr: "http://localhost",
Username: "flc",
Password: "123456",
}, WithHTTPClient(http.DefaultClient))

assert.IsType(t, &Driver{}, driver)
assert.IsType(t, &Config{}, driver.config)
assert.IsType(t, &http.Client{}, driver.httpClient)
assert.Equal(t, "http://localhost", driver.config.Addr)
assert.Equal(t, "flc", driver.config.Username)
assert.Equal(t, "123456", driver.config.Password)
}

func TestParseRequest(t *testing.T) {
driver := &Driver{}

r1 := driver.parseRequest(&sms.Request{
Phone: "1234567890",
Content: "test",
})
assert.Equal(t, "1234567890", r1.Dstaddr)
assert.Equal(t, "test", r1.Smbody)
assert.Equal(t, "now", r1.Type)
assert.Equal(t, "big5", r1.Encoding)

r2 := driver.parseRequest(&sms.Request{
Phone: "1234567890",
Content: "test",
Extra: &Request{},
})
assert.Equal(t, "1234567890", r2.Dstaddr)
assert.Equal(t, "test", r2.Smbody)
assert.Equal(t, "now", r2.Type)
assert.Equal(t, "big5", r2.Encoding)
}

func TestSend_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "flc", r.URL.Query().Get("username"))
assert.Equal(t, "123456", r.URL.Query().Get("password"))
assert.Equal(t, "1234567890", r.URL.Query().Get("dstaddr"))
assert.Equal(t, "test", r.URL.Query().Get("smbody"))

_, _ = fmt.Fprintln(w, "[1]\nmsgid=123123123\nstatuscode=1\nAccountPoint=456789")
}))
defer srv.Close()

driver := New(&Config{
Addr: srv.URL,
Username: "flc",
Password: "123456",
})

resp, err := driver.Send(ctx, &sms.Request{
Phone: "1234567890",
Content: "test",
})
assert.NoError(t, err)
assert.Equal(t, sms.Success, resp.Status)
assert.Equal(t, "success", resp.Message)
assert.IsType(t, &Response{}, resp.Extra)
assert.Equal(t, "123123123", resp.Extra.(*Response).MsgID)
assert.Equal(t, "1", resp.Extra.(*Response).StatusCode)
assert.Equal(t, "456789", resp.Extra.(*Response).AccountPoint)
assert.Empty(t, resp.Extra.(*Response).Error)
}

func TestSend_Fail(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "flc", r.URL.Query().Get("username"))
assert.Equal(t, "123456", r.URL.Query().Get("password"))
assert.Equal(t, "1234567890", r.URL.Query().Get("dstaddr"))
assert.Equal(t, "test", r.URL.Query().Get("smbody"))

_, _ = fmt.Fprintln(w, "[1]\nstatuscode=k\nError=custom error")
}))
defer srv.Close()

driver := New(&Config{
Addr: srv.URL,
Username: "flc",
Password: "123456",
})

resp, err := driver.Send(ctx, &sms.Request{
Phone: "1234567890",
Content: "test",
})
assert.Error(t, err)
assert.Equal(t, sms.Failed, resp.Status)
assert.Equal(t, "mitake: custom error", resp.Message)
assert.IsType(t, &Response{}, resp.Extra)
assert.Equal(t, "k", resp.Extra.(*Response).StatusCode)
assert.Equal(t, "custom error", resp.Extra.(*Response).Error)
assert.Empty(t, resp.Extra.(*Response).MsgID)
assert.Empty(t, resp.Extra.(*Response).AccountPoint)
}
Loading

0 comments on commit e41d2c4

Please sign in to comment.