Skip to content

Commit

Permalink
feat:add SendCloud SMS Sender
Browse files Browse the repository at this point in the history
  • Loading branch information
Bsheepcoder committed Apr 29, 2024
1 parent a225932 commit c671ecc
Showing 1 changed file with 80 additions and 59 deletions.
139 changes: 80 additions & 59 deletions sendcloud.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
package go_sms_sender

import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)

// ResponseData structure holds the response from SendCloud API.
type ResponseData struct {
Result bool `json:"result"`
StatusCode int `json:"statusCode"`
Message string `json:"message"`
Info string `json:"info"`
}

// Config holds the configuration for the SMS sending service.
type Config struct {
CharSet string
Server string
Expand All @@ -31,140 +32,160 @@ type Config struct {
MaxReceivers int
}

func NewConfig(smsUser, smsKey string) (Config, error) {
// NewConfig creates a new configuration with the provided user and key.
func NewConfig(smsUser, smsKey string) (*Config, error) {
if smsUser == "" {
return Config{}, errors.New("smsUser cannot be empty")
return nil, errors.New("smsUser cannot be empty")
}
if smsKey == "" {
return Config{}, errors.New("smsKey cannot be empty")
return nil, errors.New("smsKey cannot be empty")
}

return Config{
return &Config{
CharSet: "utf-8",
Server: "https://api.sendcloud.net",
SendSMSAPI: "https://api.sendcloud.net/smsapi/send",
SendSMSAPI: "/smsapi/send",
MaxReceivers: 100,
SmsUser: smsUser,
SmsKey: smsKey,
}, nil
}

// SendCloudSms represents the data required to send an SMS message.
type SendCloudSms struct {
TemplateId int
MsgType int
Phone []string
Vars map[string]string
}

func NewSendCloudSms(templateId, msgType int, phone []string, vars map[string]string) (SendCloudSms, error) {
if templateId == 0 {
return SendCloudSms{}, errors.New("templateId cannot be zero")
// NewSendCloudSms creates a new SendCloudSms instance with the provided parameters.
func NewSendCloudSms(templateId, msgType int, phone []string, vars map[string]string) (*SendCloudSms, error) {
if templateId <= 0 {
return nil, errors.New("templateId must be greater than zero")
}
if msgType < 0 {
return SendCloudSms{}, errors.New("msgType cannot be negative")
return nil, errors.New("msgType cannot be negative")
}
if len(phone) == 0 {
return SendCloudSms{}, errors.New("phone cannot be empty")
return nil, errors.New("phone cannot be empty")
}
return SendCloudSms{
return &SendCloudSms{
TemplateId: templateId,
MsgType: msgType,
Phone: phone,
Vars: vars,
}, nil
}

// SendMessage sends an SMS using SendCloud API.
func SendMessage(sms SendCloudSms, config Config) error {

if err := ValidateSendCloudSms(sms); err != nil {
return err
if err := validateSendCloudSms(sms); err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
if err := ValidateConfig(config); err != nil {
return err
if err := validateConfig(config); err != nil {
return fmt.Errorf("failed to send message: %w", err)
}

params := url.Values{}
params.Set("smsUser", config.SmsUser)
params.Set("msgType", strconv.Itoa(sms.MsgType))
params.Set("phone", strings.Join(sms.Phone, ","))
params.Set("templateId", strconv.Itoa(sms.TemplateId))
params.Set("timestamp", strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10))

if len(sms.Vars) > 0 {
varsJSON, err := json.Marshal(sms.Vars)
if err != nil {
return fmt.Errorf("failed to marshal vars: %v", err)
}
params.Set("vars", string(varsJSON))
params, err := prepareParams(sms, config)
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}

signature := calculateSignature(params, config.SmsKey)
params.Set("signature", signature)

resp, err := http.PostForm(config.SendSMSAPI, params)
if err != nil {
return fmt.Errorf("failed to send HTTP POST request: %v", err)
return fmt.Errorf("failed to send HTTP POST request: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Printf("error closing response body: %v", err)
err := resp.Body.Close()
if err != nil {
fmt.Println(err)
return
}
}()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP POST request failed with status code %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
return fmt.Errorf("failed to read response body: %w", err)
}

var responseData ResponseData
if err := json.Unmarshal(body, &responseData); err != nil {
return fmt.Errorf("failed to unmarshal response body: %v", err)
return fmt.Errorf("failed to unmarshal response body: %w", err)
}

if !responseData.Result {
return fmt.Errorf("SMS sending failed: %s", responseData.Message)
}

return nil
}

func ValidateConfig(config Config) error {
if config.CharSet == "" {
return errors.New("charSet cannot be empty")
// prepareParams prepares parameters for sending SMS.
func prepareParams(sms SendCloudSms, config Config) (url.Values, error) {
params := url.Values{}
params.Set("smsUser", config.SmsUser)
params.Set("msgType", strconv.Itoa(sms.MsgType))
params.Set("phone", strings.Join(sms.Phone, ","))
params.Set("templateId", strconv.Itoa(sms.TemplateId))
params.Set("timestamp", strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10))

if len(sms.Vars) > 0 {
varsJSON, err := json.Marshal(sms.Vars)
if err != nil {
return nil, fmt.Errorf("failed to marshal vars: %v", err)
}
params.Set("vars", string(varsJSON))
}
if config.Server == "" {

return params, nil
}

// validateConfig validates the SMS sending configuration.
func validateConfig(config Config) error {
switch {
case config.CharSet == "":
return errors.New("charSet cannot be empty")
case config.Server == "":
return errors.New("server cannot be empty")
}
if config.SendSMSAPI == "" {
case config.SendSMSAPI == "":
return errors.New("sendSMSAPI cannot be empty")
}
if config.SmsUser == "" {
case config.SmsUser == "":
return errors.New("smsUser cannot be empty")
}
if config.SmsKey == "" {
case config.SmsKey == "":
return errors.New("smsKey cannot be empty")
}
if config.MaxReceivers <= 0 {
case config.MaxReceivers <= 0:
return errors.New("maxReceivers must be greater than zero")
}

return nil
}

func ValidateSendCloudSms(sms SendCloudSms) error {
if sms.TemplateId == 0 {
// validateSendCloudSms validates the SendCloudSms data.
func validateSendCloudSms(sms SendCloudSms) error {
switch {
case sms.TemplateId == 0:
return errors.New("templateId cannot be zero")
}
if sms.MsgType < 0 {
case sms.MsgType < 0:
return errors.New("msgType cannot be negative")
}
if len(sms.Phone) == 0 {
case len(sms.Phone) == 0:
return errors.New("phone cannot be empty")
}
return nil
}

// calculateSignature calculates the signature for the request.
func calculateSignature(params url.Values, key string) string {

sortedParams := params.Encode()
signStr := sortedParams + key
hasher := md5.New()
signStr := sortedParams + "&key=" + key
hasher := sha256.New()
hasher.Write([]byte(signStr))
signature := hex.EncodeToString(hasher.Sum(nil))
return signature
return hex.EncodeToString(hasher.Sum(nil))
}

0 comments on commit c671ecc

Please sign in to comment.