119 lines
3.4 KiB
Go
119 lines
3.4 KiB
Go
package go_sms_sender
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// SendResult describes provider acceptance, not handset delivery.
|
|
type SendResult struct {
|
|
State, RequestID, MessageID, Code string
|
|
}
|
|
|
|
var ErrSendUnknown = errors.New("sms acceptance unknown")
|
|
var ErrSendRejected = errors.New("sms rejected")
|
|
|
|
type singleSendTransport struct {
|
|
ctx context.Context
|
|
base http.RoundTripper
|
|
host string
|
|
attempted atomic.Bool
|
|
body []byte
|
|
status int
|
|
}
|
|
|
|
func (t *singleSendTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if req.URL.Scheme != "https" || req.URL.Host != t.host || t.attempted.Swap(true) {
|
|
return nil, ErrSendUnknown
|
|
}
|
|
response, err := t.base.RoundTrip(req.Clone(t.ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.body, t.status = body, response.StatusCode
|
|
response.Body = io.NopCloser(bytes.NewReader(body))
|
|
return response, nil
|
|
}
|
|
|
|
// SendMessageContext requires a fresh client per call. It deliberately disables
|
|
// implicit SDK retries and validates acceptance independently of SendMessage.
|
|
func SendMessageContext(ctx context.Context, client SmsClient, transport http.RoundTripper, params map[string]string, phone string) (SendResult, error) {
|
|
result := SendResult{State: "unknown", Code: "acceptance_unknown"}
|
|
if transport == nil || phone == "" {
|
|
return result, ErrSendUnknown
|
|
}
|
|
t := &singleSendTransport{ctx: ctx, base: transport}
|
|
switch c := client.(type) {
|
|
case *AliyunClient:
|
|
t.host = "dysmsapi.aliyuncs.com"
|
|
c.core.GetConfig().AutoRetry = false
|
|
c.core.SetTransport(t)
|
|
case *TencentClient:
|
|
t.host = "sms.tencentcloudapi.com"
|
|
c.core.WithHttpTransport(t)
|
|
default:
|
|
return result, ErrSendRejected
|
|
}
|
|
// Upstream errors may contain credentials/phone numbers and are not exposed.
|
|
_ = client.SendMessage(params, phone)
|
|
if t.status < 200 || t.status >= 300 {
|
|
return result, ErrSendUnknown
|
|
}
|
|
switch client.(type) {
|
|
case *AliyunClient:
|
|
var body struct{ Code, RequestId, BizId string }
|
|
if json.Unmarshal(t.body, &body) != nil {
|
|
return result, ErrSendUnknown
|
|
}
|
|
result.RequestID = body.RequestId
|
|
if body.Code != "" && body.Code != "OK" {
|
|
result.State, result.Code = "rejected", body.Code
|
|
return result, ErrSendRejected
|
|
}
|
|
if body.Code != "OK" || body.BizId == "" || body.RequestId == "" {
|
|
return result, ErrSendUnknown
|
|
}
|
|
result.MessageID = body.BizId
|
|
case *TencentClient:
|
|
var body struct {
|
|
Response struct {
|
|
RequestId string
|
|
Error *struct{ Code string }
|
|
SendStatusSet []struct{ Code, SerialNo string }
|
|
}
|
|
}
|
|
if json.Unmarshal(t.body, &body) != nil {
|
|
return result, ErrSendUnknown
|
|
}
|
|
result.RequestID = body.Response.RequestId
|
|
if body.Response.Error != nil && body.Response.Error.Code != "" {
|
|
result.State, result.Code = "rejected", body.Response.Error.Code
|
|
return result, ErrSendRejected
|
|
}
|
|
if len(body.Response.SendStatusSet) != 1 {
|
|
return result, ErrSendUnknown
|
|
}
|
|
status := body.Response.SendStatusSet[0]
|
|
if status.Code != "" && status.Code != "Ok" {
|
|
result.State, result.Code = "rejected", status.Code
|
|
return result, ErrSendRejected
|
|
}
|
|
if status.Code != "Ok" || status.SerialNo == "" || result.RequestID == "" {
|
|
return result, ErrSendUnknown
|
|
}
|
|
result.MessageID = status.SerialNo
|
|
}
|
|
result.State, result.Code = "accepted", ""
|
|
return result, nil
|
|
}
|