Skip to content

Commit 4ee9e4a

Browse files
authored
Merge pull request #118 from NdoleStudio/104/bulk-sms-send
Added the ability to send 50 messages in bulk. Closes #104
2 parents a9bb859 + 19c68fb commit 4ee9e4a

File tree

4 files changed

+170
-2
lines changed

4 files changed

+170
-2
lines changed

api/pkg/handlers/message_handler.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/NdoleStudio/httpsms/pkg/entities"
8+
79
"github.com/NdoleStudio/httpsms/pkg/repositories"
810
"github.com/google/uuid"
911

@@ -46,6 +48,7 @@ func NewMessageHandler(
4648
// RegisterRoutes registers the routes for the MessageHandler
4749
func (h *MessageHandler) RegisterRoutes(router fiber.Router) {
4850
router.Post("/messages/send", h.PostSend)
51+
router.Post("/messages/bulk-send", h.BulkSend)
4952
router.Post("/messages/receive", h.PostReceive)
5053
router.Get("/messages/outstanding", h.GetOutstanding)
5154
router.Get("/messages", h.Index)
@@ -100,6 +103,59 @@ func (h *MessageHandler) PostSend(c *fiber.Ctx) error {
100103
return h.responseOK(c, "message added to queue", message)
101104
}
102105

106+
// BulkSend a bulk entities.Message
107+
// @Summary Send bulk SMS messages
108+
// @Description Add bulk SMS messages to be sent by the android phone
109+
// @Security ApiKeyAuth
110+
// @Tags Messages
111+
// @Accept json
112+
// @Produce json
113+
// @Param payload body requests.MessageBulkSend true "Bulk send message request payload"
114+
// @Success 200 {object} []responses.MessagesResponse
115+
// @Failure 400 {object} responses.BadRequest
116+
// @Failure 401 {object} responses.Unauthorized
117+
// @Failure 422 {object} responses.UnprocessableEntity
118+
// @Failure 500 {object} responses.InternalServerError
119+
// @Router /messages/bulk-send [post]
120+
func (h *MessageHandler) BulkSend(c *fiber.Ctx) error {
121+
ctx, span := h.tracer.StartFromFiberCtx(c)
122+
defer span.End()
123+
124+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
125+
126+
var request requests.MessageBulkSend
127+
if err := c.BodyParser(&request); err != nil {
128+
msg := fmt.Sprintf("cannot marshall [%s] into %T", c.Body(), request)
129+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
130+
return h.responseBadRequest(c, err)
131+
}
132+
133+
if errors := h.validator.ValidateMessageBulkSend(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
134+
msg := fmt.Sprintf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())
135+
ctxLogger.Warn(stacktrace.NewError(msg))
136+
return h.responseUnprocessableEntity(c, errors, "validation errors while sending messages")
137+
}
138+
139+
var responses []*entities.Message
140+
params := request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL())
141+
for _, param := range params {
142+
if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil {
143+
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] can't send a message", h.userIDFomContext(c))))
144+
break
145+
}
146+
147+
message, err := h.service.SendMessage(ctx, param)
148+
if err != nil {
149+
msg := fmt.Sprintf("cannot send message with paylod [%s]", c.Body())
150+
ctxLogger.Error(stacktrace.Propagate(err, msg))
151+
break
152+
}
153+
responses = append(responses, message)
154+
}
155+
156+
return h.responseOK(c, "messages added to queue", responses)
157+
}
158+
103159
// GetOutstanding returns an entities.Message which is still to be sent by the mobile phone
104160
// @Summary Get an outstanding message
105161
// @Description Get an outstanding message to be sent by an android phone
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package requests
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
8+
"github.com/nyaruka/phonenumbers"
9+
10+
"github.com/NdoleStudio/httpsms/pkg/services"
11+
)
12+
13+
// MessageBulkSend is the payload for sending bulk SMS messages
14+
type MessageBulkSend struct {
15+
request
16+
From string `json:"from" example:"+18005550199"`
17+
To []string `json:"to" example:"[+18005550100,+18005550100]"`
18+
Content string `json:"content" example:"This is a sample text message"`
19+
}
20+
21+
// Sanitize sets defaults to MessageReceive
22+
func (input *MessageBulkSend) Sanitize() MessageBulkSend {
23+
var to []string
24+
for _, address := range input.To {
25+
to = append(to, input.sanitizeAddress(address))
26+
}
27+
input.To = to
28+
input.From = input.sanitizeAddress(input.From)
29+
return *input
30+
}
31+
32+
// ToMessageSendParams converts MessageSend to services.MessageSendParams
33+
func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source string) []services.MessageSendParams {
34+
from, _ := phonenumbers.Parse(input.From, phonenumbers.UNKNOWN_REGION)
35+
var result []services.MessageSendParams
36+
for _, to := range input.To {
37+
toAddress, _ := phonenumbers.Parse(to, phonenumbers.UNKNOWN_REGION)
38+
result = append(result, services.MessageSendParams{
39+
Source: source,
40+
Owner: *from,
41+
UserID: userID,
42+
RequestReceivedAt: time.Now().UTC(),
43+
Contact: *toAddress,
44+
Content: input.Content,
45+
})
46+
}
47+
48+
return result
49+
}

api/pkg/validators/message_handler_validator.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,52 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context
105105
return result
106106
}
107107

108+
// ValidateMessageBulkSend validates the requests.MessageBulkSend request
109+
func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Context, userID entities.UserID, request requests.MessageBulkSend) url.Values {
110+
ctx, span := validator.tracer.Start(ctx)
111+
defer span.End()
112+
113+
ctxLogger := validator.tracer.CtxLogger(validator.logger, span)
114+
115+
v := govalidator.New(govalidator.Options{
116+
Data: &request,
117+
Rules: govalidator.MapData{
118+
"to": []string{
119+
"required",
120+
"max:50",
121+
"min:1",
122+
multiplePhoneNumberRule,
123+
},
124+
"from": []string{
125+
"required",
126+
phoneNumberRule,
127+
},
128+
"content": []string{
129+
"required",
130+
"min:1",
131+
"max:1024",
132+
},
133+
},
134+
})
135+
136+
result := v.ValidateStruct()
137+
if len(result) != 0 {
138+
return result
139+
}
140+
141+
_, err := validator.phoneService.Load(ctx, userID, request.From)
142+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
143+
result.Add("from", fmt.Sprintf("no phone found with with 'from' number [%s]. Install the android app on your phone to start sending messages", request.From))
144+
}
145+
146+
if err != nil {
147+
ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("could not load phone for user [%s] and phone [%s]", userID, request.From))))
148+
result.Add("from", fmt.Sprintf("could not validate 'from' number [%s], please try again later", request.From))
149+
}
150+
151+
return result
152+
}
153+
108154
// ValidateMessageOutstanding validates the requests.MessageOutstanding request
109155
func (validator MessageHandlerValidator) ValidateMessageOutstanding(_ context.Context, request requests.MessageOutstanding) url.Values {
110156
v := govalidator.New(govalidator.Options{

api/pkg/validators/validator.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import (
1414
type validator struct{}
1515

1616
const (
17-
phoneNumberRule = "phoneNumber"
18-
webhookEventsRule = "webhookEvents"
17+
phoneNumberRule = "phoneNumber"
18+
multiplePhoneNumberRule = "multiplePhoneNumber"
19+
webhookEventsRule = "webhookEvents"
1920
)
2021

2122
func init() {
@@ -35,6 +36,22 @@ func init() {
3536
return nil
3637
})
3738

39+
govalidator.AddCustomRule(multiplePhoneNumberRule, func(field string, rule string, message string, value interface{}) error {
40+
phoneNumbers, ok := value.([]string)
41+
if !ok {
42+
return fmt.Errorf("the %s field must be an array of a valid E.164 phone number: https://en.wikipedia.org/wiki/E.164", field)
43+
}
44+
45+
for index, number := range phoneNumbers {
46+
_, err := phonenumbers.Parse(number, phonenumbers.UNKNOWN_REGION)
47+
if err != nil {
48+
return fmt.Errorf("the %s value in index [%d] must be a valid E.164 phone number: https://en.wikipedia.org/wiki/E.164", field, index)
49+
}
50+
}
51+
52+
return nil
53+
})
54+
3855
govalidator.AddCustomRule(webhookEventsRule, func(field string, rule string, message string, value interface{}) error {
3956
input, ok := value.([]string)
4057
if !ok {

0 commit comments

Comments
 (0)