diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml new file mode 100644 index 0000000..009597c --- /dev/null +++ b/.idea/GitLink.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/examples/send_sms_example.go b/examples/send_sms_example.go index 6621073..e520b9c 100644 --- a/examples/send_sms_example.go +++ b/examples/send_sms_example.go @@ -1,24 +1,34 @@ package sms_sender import ( - "fmt" + "context" "github.com/MikeMwita/africastalking-go/pkg/sms" "log" + "net/http" + "os" + "time" ) func main() { - // Example usage - sender := sms.SmsSender{ - ApiKey: "your_api_key", - ApiUser: "your_api_user", + apiKey := os.Getenv("API_KEY") + apiUser := os.Getenv("API_USER") + + client := sms.NewClient(&http.Client{}, apiKey, apiUser) + sender := &sms.SmsSender{ + Client: client, Recipients: []string{"+1234567890"}, - Message: "Hello, world!", + Message: "Test message", + Sender: "YourSenderID", + SmsKey: "unique_sms_key", } - response, err := sender.SendSMS() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := sender.RetrySendSMS(ctx, 3) if err != nil { - log.Fatal(err) + log.Fatalf("Failed to send SMS: %v", err) } - fmt.Printf("Response: %+v\n", response) + log.Printf("SMS Response: %+v", resp) } diff --git a/pkg/sms/client.go b/pkg/sms/client.go new file mode 100644 index 0000000..d210b89 --- /dev/null +++ b/pkg/sms/client.go @@ -0,0 +1,28 @@ +package sms + +import "net/http" + +const ( + DefaultAPIURL = "https://api.africastalking.com/version1/messaging" + sandboxAPIURL = "https://api.sandbox.africastalking.com/version1/messaging" +) + +type Doer interface { + Do(req *http.Request) (*http.Response, error) +} + +type Client struct { + apiURL string + apiKey string + apiUser string + client Doer +} + +func NewClient(client Doer, apiKey, apiUser string) *Client { + return &Client{ + apiURL: DefaultAPIURL, + apiKey: apiKey, + apiUser: apiUser, + client: client, + } +} diff --git a/pkg/sms/sms_sender.go b/pkg/sms/sms_sender.go index 1b6626e..579282e 100644 --- a/pkg/sms/sms_sender.go +++ b/pkg/sms/sms_sender.go @@ -1,8 +1,10 @@ package sms import ( + "context" "encoding/json" "fmt" + "math/rand" "net/http" "net/url" "strings" @@ -11,149 +13,94 @@ import ( "github.com/google/uuid" ) -type SmsSender struct { - ApiKey string `json:"api_key"` - ApiUser string `json:"api_user"` - Recipients []string `json:"recipients"` - Message string `json:"message"` - Sender string `json:"sender"` - SmsKey string `json:"sms_key"` -} - -type Recipient struct { - Key string `json:"key"` - Cost string `json:"cost"` - SmsKey string `json:"sms_key"` - MessageId string `json:"message_id"` - MessagePart int `json:"message_part"` - Number string `json:"number"` - Status string `json:"status"` - StatusCode string `json:"status_code"` -} - -type SmsMessageData struct { - Message string `json:"message"` - Cost string `json:"cost"` - Recipients []Recipient `json:"recipients"` -} - -type ErrorResponse struct { - HasError bool `json:"has_error"` - Message string `json:"message"` -} - -type SmsSenderResponse struct { - ErrorResponse ErrorResponse `json:"error_response"` - SmsMessageData SmsMessageData `json:"sms_message_data"` -} - -// SendSMS sends an SMS using the Africa's Talking API -func (s *SmsSender) SendSMS() (SmsSenderResponse, error) { - endpoint := "https://api.africastalking.com/version1/messaging" - parsedURL, err := url.Parse(endpoint) - if err != nil { - return SmsSenderResponse{}, err - } - - body := map[string][]string{ - "username": {s.ApiUser}, - "to": s.Recipients, - "message": {s.Message}, - "from": {s.Sender}, - } - +func (s *SmsSender) SendSMS(ctx context.Context) (SmsSenderResponse, error) { form := url.Values{} - for key, values := range body { - for _, value := range values { - form.Add(key, value) - } - } + form.Add("username", s.Client.apiUser) + form.Add("to", strings.Join(s.Recipients, ",")) + form.Add("message", s.Message) + form.Add("from", s.Sender) - req, err := http.NewRequest(http.MethodPost, parsedURL.String(), strings.NewReader(form.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.Client.apiURL, strings.NewReader(form.Encode())) if err != nil { return SmsSenderResponse{}, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("apiKey", s.ApiKey) + req.Header.Set("apiKey", s.Client.apiKey) - client := &http.Client{} - res, err := client.Do(req) + res, err := s.Client.client.Do(req) if err != nil { return SmsSenderResponse{}, err } defer res.Body.Close() - if res.StatusCode == http.StatusCreated { - recipients := make([]Recipient, 0) - - var data map[string]interface{} - err = json.NewDecoder(res.Body).Decode(&data) - if err != nil { - return SmsSenderResponse{}, err - } + if res.StatusCode != http.StatusCreated { + return SmsSenderResponse{ + ErrorResponse: ErrorResponse{ + HasError: true, + Message: "Message not sent", + }, + }, fmt.Errorf("status code: %d", res.StatusCode) + } - smsMessageData := data["SMSMessageData"].(map[string]interface{}) - message := smsMessageData["Message"].(string) + var data map[string]interface{} + err = json.NewDecoder(res.Body).Decode(&data) + if err != nil { + return SmsSenderResponse{}, err + } - cost := "" - for _, word := range strings.Split(message, " ") { - cost = word - } - recipientsData := smsMessageData["Recipients"].([]interface{}) - - for _, recipient := range recipientsData { - recipientData := recipient.(map[string]interface{}) - - rct := Recipient{ - Key: uuid.New().String(), - Cost: recipientData["cost"].(string), - SmsKey: s.SmsKey, - MessageId: recipientData["messageId"].(string), - MessagePart: int(recipientData["messageParts"].(float64)), - Number: recipientData["number"].(string), - Status: recipientData["status"].(string), - StatusCode: fmt.Sprintf("%v", recipientData["statusCode"]), - } - - recipients = append(recipients, rct) - } + smsMessageData := data["SMSMessageData"].(map[string]interface{}) + message := smsMessageData["Message"].(string) + cost := "" + for _, word := range strings.Split(message, " ") { + cost = word + } - smsSenderResponse := SmsSenderResponse{ - ErrorResponse: ErrorResponse{ - HasError: false, - }, - SmsMessageData: SmsMessageData{ - Message: message, - Cost: cost, - Recipients: recipients, - }, + recipientsData := smsMessageData["Recipients"].([]interface{}) + recipients := make([]Recipient, 0) + + for _, recipient := range recipientsData { + recipientData := recipient.(map[string]interface{}) + + rct := Recipient{ + Key: uuid.New().String(), + Cost: recipientData["cost"].(string), + SmsKey: s.SmsKey, + MessageId: recipientData["messageId"].(string), + MessagePart: int(recipientData["messageParts"].(float64)), + Number: recipientData["number"].(string), + Status: recipientData["status"].(string), + StatusCode: fmt.Sprintf("%v", recipientData["statusCode"]), } - return smsSenderResponse, nil + recipients = append(recipients, rct) } - smsSenderResponse := SmsSenderResponse{ + return SmsSenderResponse{ ErrorResponse: ErrorResponse{ - HasError: true, - Message: "Message not sent", + HasError: false, }, - } - - return smsSenderResponse, fmt.Errorf("status code: %d", res.StatusCode) + SmsMessageData: SmsMessageData{ + Message: message, + Cost: cost, + Recipients: recipients, + }, + }, nil } -// Retry sends an SMS with exponential backoff -func (s *SmsSender) RetrySendSMS(maxRetries int) (SmsSenderResponse, error) { + +func (s *SmsSender) RetrySendSMS(ctx context.Context, maxRetries int) (SmsSenderResponse, error) { for retry := 0; retry < maxRetries; retry++ { - response, err := s.SendSMS() - if err == nil { - return response, nil - } + response, err := s.SendSMS(ctx) + if err == nil { + return response, nil + } - delay := time.Duration(1<