forked from NdoleStudio/httpsms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbulk_message_handler.go
More file actions
186 lines (165 loc) · 6.55 KB
/
Copy pathbulk_message_handler.go
File metadata and controls
186 lines (165 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package handlers
import (
"fmt"
"path/filepath"
"regexp"
"sync"
"sync/atomic"
"time"
"github.com/NdoleStudio/httpsms/pkg/requests"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
"github.com/palantir/stacktrace"
)
// BulkMessageHandler handles bulk SMS http requests
type BulkMessageHandler struct {
handler
logger telemetry.Logger
tracer telemetry.Tracer
validator *validators.BulkMessageHandlerValidator
messageService *services.MessageService
billingService *services.BillingService
}
// NewBulkMessageHandler creates a new BulkMessageHandler
func NewBulkMessageHandler(
logger telemetry.Logger,
tracer telemetry.Tracer,
validator *validators.BulkMessageHandlerValidator,
billingService *services.BillingService,
messageService *services.MessageService,
) (h *BulkMessageHandler) {
return &BulkMessageHandler{
logger: logger.WithService(fmt.Sprintf("%T", h)),
tracer: tracer,
validator: validator,
messageService: messageService,
billingService: billingService,
}
}
// RegisterRoutes registers the routes for the MessageHandler
func (h *BulkMessageHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) {
h.register(router, fiber.MethodGet, "/v1/bulk-messages", middlewares, h.Index)
h.register(router, fiber.MethodPost, "/v1/bulk-messages", middlewares, h.Store)
}
// Index fetches the bulk message order history.
// @Summary List bulk message orders
// @Description Fetches the last 10 bulk message order summaries for the authenticated user showing counts per status.
// @Security ApiKeyAuth
// @Tags BulkSMS
// @Accept json
// @Produce json
// @Success 200 {object} responses.BulkMessagesResponse
// @Failure 401 {object} responses.Unauthorized
// @Failure 500 {object} responses.InternalServerError
// @Router /bulk-messages [get]
func (h *BulkMessageHandler) Index(c fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
orders, err := h.messageService.GetBulkMessages(ctx, h.userIDFomContext(c))
if err != nil {
msg := fmt.Sprintf("cannot fetch bulk messages for user [%s]", h.userIDFomContext(c))
ctxLogger.Error(stacktrace.Propagate(err, msg))
return h.responseInternalServerError(c)
}
return h.responseOK(c, fmt.Sprintf("fetched %d bulk %s", len(orders), h.pluralize("message", len(orders))), orders)
}
// Store sends bulk SMS messages from a CSV or Excel file.
// @Summary Store bulk SMS file
// @Description Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx).
// @Security ApiKeyAuth
// @Tags BulkSMS
// @Accept multipart/form-data
// @Produce json
// @Param document formData file true "The Excel or CSV file containing the messages to be sent."
// @Success 202 {object} responses.NoContent
// @Failure 400 {object} responses.BadRequest
// @Failure 401 {object} responses.Unauthorized
// @Failure 422 {object} responses.UnprocessableEntity
// @Failure 500 {object} responses.InternalServerError
// @Router /bulk-messages [post]
func (h *BulkMessageHandler) Store(c fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
file, err := c.FormFile("document")
if err != nil {
msg := fmt.Sprintf("cannot fetch file with name [%s] from request", "document")
ctxLogger.Warn(stacktrace.Propagate(err, msg))
return h.responseBadRequest(c, err)
}
messages, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file)
if len(validationErrors) != 0 {
msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c))
ctxLogger.Warn(stacktrace.NewError(msg))
return h.responseUnprocessableEntity(c, validationErrors, "validation errors while sending bulk SMS")
}
if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(messages))); msg != nil {
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages))))
return h.responsePaymentRequired(c, *msg)
}
requestID := h.generateRequestID(file.Filename)
wg := sync.WaitGroup{}
count := atomic.Int64{}
// Compute per-phone index for rate-based dispatch delay
phoneIndexCounter := make(map[string]int)
for _, message := range messages {
wg.Add(1)
var perPhoneIndex int
if message.GetSendTime(userLocation) == nil {
perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber]
phoneIndexCounter[message.FromPhoneNumber]++
}
go func(message *requests.BulkMessage, index int) {
count.Add(1)
_, err = h.messageService.SendMessage(
ctx,
message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index, userLocation),
)
if err != nil {
count.Add(-1)
msg := fmt.Sprintf("cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index)
ctxLogger.Error(stacktrace.Propagate(err, msg))
}
wg.Done()
}(message, perPhoneIndex)
}
wg.Wait()
return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages)))
}
func (h *BulkMessageHandler) generateRequestID(filename string) string {
return fmt.Sprintf("bulk-%s-%s", encodeBase62(time.Now().UnixMilli()), truncateFilename(sanitizeFilename(filename), 32))
}
func sanitizeFilename(filename string) string {
return regexp.MustCompile(`[^a-zA-Z0-9.\-_: ]`).ReplaceAllString(filename, "")
}
func encodeBase62(n int64) string {
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
if n == 0 {
return "0"
}
result := make([]byte, 0, 8)
for n > 0 {
result = append(result, charset[n%62])
n /= 62
}
// reverse
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}
func truncateFilename(filename string, maxLen int) string {
if len(filename) <= maxLen {
return filename
}
ext := filepath.Ext(filename)
name := filename[:len(filename)-len(ext)]
available := maxLen - len(ext)
if available <= 0 {
return filename[:maxLen]
}
half := available / 2
return name[:half] + name[len(name)-(available-half):] + ext
}