Skip to content

Commit e59b97e

Browse files
committed
Add API for received message
1 parent 07ff0c4 commit e59b97e

File tree

8 files changed

+298
-23
lines changed

8 files changed

+298
-23
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// EventTypeMessagePhoneReceived is emitted when a new message is received by a mobile phone
10+
const EventTypeMessagePhoneReceived = "message.phone.received"
11+
12+
// MessagePhoneReceivedPayload is the payload of the EventTypeMessagePhoneReceived event
13+
type MessagePhoneReceivedPayload struct {
14+
ID uuid.UUID `json:"id"`
15+
From string `json:"from"`
16+
To string `json:"to"`
17+
Timestamp time.Time `json:"timestamp"`
18+
Content string `json:"content"`
19+
}

api/pkg/handlers/message_handler.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,44 @@ func (h *MessageHandler) PostEvent(c *fiber.Ctx) error {
228228

229229
return h.responseOK(c, "message event stored successfully", message)
230230
}
231+
232+
// PostReceive receives a new entities.Message
233+
// @Summary Receive a new SMS message from a mobile phone
234+
// @Description Add a new message received from a mobile phone
235+
// @Tags Messages
236+
// @Accept json
237+
// @Produce json
238+
// @Param payload body requests.MessageReceive true "Received message request payload"
239+
// @Success 200 {object} responses.MessageResponse
240+
// @Failure 400 {object} responses.BadRequest
241+
// @Failure 422 {object} responses.UnprocessableEntity
242+
// @Failure 500 {object} responses.InternalServerError
243+
// @Router /messages/receive [post]
244+
func (h *MessageHandler) PostReceive(c *fiber.Ctx) error {
245+
ctx, span := h.tracer.StartFromFiberCtx(c)
246+
defer span.End()
247+
248+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
249+
250+
var request requests.MessageReceive
251+
if err := c.BodyParser(&request); err != nil {
252+
msg := fmt.Sprintf("cannot marshall [%s] into %T", c.Body(), request)
253+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
254+
return h.responseBadRequest(c, err)
255+
}
256+
257+
if errors := h.validator.ValidateMessageReceive(ctx, request); len(errors) != 0 {
258+
msg := fmt.Sprintf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())
259+
ctxLogger.Warn(stacktrace.NewError(msg))
260+
return h.responseUnprocessableEntity(c, errors, "validation errors while receiving message")
261+
}
262+
263+
message, err := h.service.ReceiveMessage(ctx, request.ToMessageReceiveParams(c.OriginalURL()))
264+
if err != nil {
265+
msg := fmt.Sprintf("cannot receive message with paylod [%s]", c.Body())
266+
ctxLogger.Error(stacktrace.Propagate(err, msg))
267+
return h.responseInternalServerError(c)
268+
}
269+
270+
return h.responseOK(c, "message received successfully", message)
271+
}

api/pkg/listeners/message_listener.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ func NewMessageListener(
3737
}
3838

3939
return l, map[string]events.EventListener{
40-
events.EventTypeMessageAPISent: l.OnMessageAPISent,
41-
events.EventTypeMessagePhoneSending: l.OnMessagePhoneSending,
42-
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
40+
events.EventTypeMessageAPISent: l.OnMessageAPISent,
41+
events.EventTypeMessagePhoneSending: l.OnMessagePhoneSending,
42+
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
43+
events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived,
4344
}
4445
}
4546

@@ -68,15 +69,14 @@ func (listener *MessageListener) OnMessageAPISent(ctx context.Context, event clo
6869
}
6970

7071
storeParams := services.MessageStoreParams{
71-
From: payload.From,
72-
To: payload.To,
73-
Content: payload.Content,
74-
ID: payload.ID,
75-
Source: event.Source(),
76-
RequestReceivedAt: payload.RequestReceivedAt,
72+
From: payload.From,
73+
To: payload.To,
74+
Content: payload.Content,
75+
ID: payload.ID,
76+
Timestamp: payload.RequestReceivedAt,
7777
}
7878

79-
if _, err = listener.service.StoreMessage(ctx, storeParams); err != nil {
79+
if _, err = listener.service.StoreSentMessage(ctx, storeParams); err != nil {
8080
msg := fmt.Sprintf("cannot store message with ID [%s] for event with ID [%s]", storeParams.ID, event.ID())
8181
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
8282
}
@@ -158,6 +158,46 @@ func (listener *MessageListener) OnMessagePhoneSent(ctx context.Context, event c
158158
return listener.storeEventListenerLog(ctx, listener.signature(event), event)
159159
}
160160

161+
// OnMessagePhoneReceived handles the events.EventTypeMessageAPISent event
162+
func (listener *MessageListener) OnMessagePhoneReceived(ctx context.Context, event cloudevents.Event) error {
163+
ctx, span := listener.tracer.Start(ctx)
164+
defer span.End()
165+
166+
handled, err := listener.repository.Has(ctx, event.ID(), listener.signature(event))
167+
if err != nil {
168+
msg := fmt.Sprintf("cannot verify if event [%s] has been handled by [%T]", event.ID(), listener.signature(event))
169+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
170+
}
171+
172+
ctxLogger := listener.tracer.CtxLogger(listener.logger, span)
173+
174+
if handled {
175+
ctxLogger.Info(fmt.Sprintf("event [%s] has already been handled by [%s]", event.ID(), listener.signature(event)))
176+
return nil
177+
}
178+
179+
var payload events.MessageAPISentPayload
180+
if err = event.DataAs(&payload); err != nil {
181+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
182+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
183+
}
184+
185+
storeParams := services.MessageStoreParams{
186+
From: payload.From,
187+
To: payload.To,
188+
Content: payload.Content,
189+
ID: payload.ID,
190+
Timestamp: payload.RequestReceivedAt,
191+
}
192+
193+
if _, err = listener.service.StoreReceivedMessage(ctx, storeParams); err != nil {
194+
msg := fmt.Sprintf("cannot store message with ID [%s] for event with ID [%s]", storeParams.ID, event.ID())
195+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
196+
}
197+
198+
return listener.storeEventListenerLog(ctx, listener.signature(event), event)
199+
}
200+
161201
func (listener *MessageListener) signature(event cloudevents.Event) string {
162202
return listener.handlerSignature(listener, event)
163203
}

api/pkg/listeners/message_thread_listener.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ func NewMessageThreadListener(
3737
}
3838

3939
return l, map[string]events.EventListener{
40-
events.EventTypeMessageAPISent: l.OnMessageAPISent,
41-
events.EventTypeMessagePhoneSending: l.OnMessagePhoneSending,
42-
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
40+
events.EventTypeMessageAPISent: l.OnMessageAPISent,
41+
events.EventTypeMessagePhoneSending: l.OnMessagePhoneSending,
42+
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
43+
events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived,
4344
}
4445
}
4546

@@ -124,6 +125,33 @@ func (listener *MessageThreadListener) OnMessagePhoneSent(ctx context.Context, e
124125
return nil
125126
}
126127

128+
// OnMessagePhoneReceived handles the events.EventTypeMessagePhoneReceived event
129+
func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Context, event cloudevents.Event) error {
130+
ctx, span := listener.tracer.Start(ctx)
131+
defer span.End()
132+
133+
var payload events.MessagePhoneReceivedPayload
134+
if err := event.DataAs(&payload); err != nil {
135+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
136+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
137+
}
138+
139+
updateParams := services.MessageThreadUpdateParams{
140+
Owner: payload.To,
141+
Contact: payload.From,
142+
Timestamp: event.Time(),
143+
Content: payload.Content,
144+
MessageID: payload.ID,
145+
}
146+
147+
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
148+
msg := fmt.Sprintf("cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())
149+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
150+
}
151+
152+
return nil
153+
}
154+
127155
func (listener *MessageThreadListener) updateThread(ctx context.Context, params services.MessageThreadUpdateParams) error {
128156
return listener.service.UpdateThread(ctx, params)
129157
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package requests
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
7+
)
8+
9+
// MessageReceive is the payload for sending and SMS message
10+
type MessageReceive struct {
11+
From string `json:"from" example:"+18005550199"`
12+
To string `json:"to" example:"+18005550100"`
13+
Content string `json:"content" example:"This is a sample text message received on a phone"`
14+
// Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible
15+
Timestamp time.Time `json:"timestamp" example:"2022-06-05T14:26:09.527976+03:00"`
16+
}
17+
18+
// ToMessageReceiveParams converts MessageReceive to services.MessageReceiveParams
19+
func (input MessageReceive) ToMessageReceiveParams(source string) services.MessageReceiveParams {
20+
return services.MessageReceiveParams{
21+
Source: source,
22+
From: input.From,
23+
Timestamp: input.Timestamp,
24+
To: input.To,
25+
Content: input.Content,
26+
}
27+
}

api/pkg/services/message_service.go

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,58 @@ func (service *MessageService) StoreEvent(ctx context.Context, message *entities
130130
return service.repository.Load(ctx, params.MessageID)
131131
}
132132

133+
// MessageReceiveParams parameters registering a message event
134+
type MessageReceiveParams struct {
135+
From string
136+
To string
137+
Content string
138+
Timestamp time.Time
139+
Source string
140+
}
141+
142+
// ReceiveMessage handles message received by a mobile phone
143+
func (service *MessageService) ReceiveMessage(ctx context.Context, params MessageReceiveParams) (*entities.Message, error) {
144+
ctx, span := service.tracer.Start(ctx)
145+
defer span.End()
146+
147+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
148+
149+
eventPayload := events.MessagePhoneReceivedPayload{
150+
ID: uuid.New(),
151+
From: params.From,
152+
To: params.To,
153+
Timestamp: params.Timestamp,
154+
Content: params.Content,
155+
}
156+
157+
ctxLogger.Info(fmt.Sprintf("creating cloud event for received with ID [%s]", eventPayload.ID))
158+
159+
event, err := service.createMessagePhoneReceivedEvent(params.Source, eventPayload)
160+
if err != nil {
161+
msg := fmt.Sprintf("cannot create %T from payload with message id [%s]", event, eventPayload.ID)
162+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
163+
}
164+
165+
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s]", event.Type(), event.ID(), eventPayload.ID))
166+
167+
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
168+
msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())
169+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
170+
}
171+
172+
ctxLogger.Info(fmt.Sprintf("event [%s] dispatched succesfully", event.ID()))
173+
174+
message, err := service.repository.Load(ctx, eventPayload.ID)
175+
if err != nil {
176+
msg := fmt.Sprintf("cannot load message with ID [%s] in the repository", eventPayload.ID)
177+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
178+
}
179+
180+
ctxLogger.Info(fmt.Sprintf("fetched message with id [%s] from the repository", message.ID))
181+
182+
return message, nil
183+
}
184+
133185
func (service *MessageService) handleMessageSentEvent(ctx context.Context, params MessageStorePhoneEventParams, message *entities.Message) error {
134186
ctx, span := service.tracer.Start(ctx)
135187
defer span.End()
@@ -263,16 +315,15 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe
263315

264316
// MessageStoreParams are parameters for creating a new message
265317
type MessageStoreParams struct {
266-
From string
267-
To string
268-
Content string
269-
ID uuid.UUID
270-
Source string
271-
RequestReceivedAt time.Time
318+
From string
319+
To string
320+
Content string
321+
ID uuid.UUID
322+
Timestamp time.Time
272323
}
273324

274-
// StoreMessage a new message
275-
func (service *MessageService) StoreMessage(ctx context.Context, params MessageStoreParams) (*entities.Message, error) {
325+
// StoreSentMessage a new message
326+
func (service *MessageService) StoreSentMessage(ctx context.Context, params MessageStoreParams) (*entities.Message, error) {
276327
ctx, span := service.tracer.Start(ctx)
277328
defer span.End()
278329

@@ -285,10 +336,10 @@ func (service *MessageService) StoreMessage(ctx context.Context, params MessageS
285336
Content: params.Content,
286337
Type: entities.MessageTypeMobileTerminated,
287338
Status: entities.MessageStatusPending,
288-
RequestReceivedAt: params.RequestReceivedAt,
339+
RequestReceivedAt: params.Timestamp,
289340
CreatedAt: time.Now().UTC(),
290341
UpdatedAt: time.Now().UTC(),
291-
OrderTimestamp: params.RequestReceivedAt,
342+
OrderTimestamp: params.Timestamp,
292343
SendDuration: nil,
293344
LastAttemptedAt: nil,
294345
SentAt: nil,
@@ -304,6 +355,36 @@ func (service *MessageService) StoreMessage(ctx context.Context, params MessageS
304355
return message, nil
305356
}
306357

358+
// StoreReceivedMessage a new message
359+
func (service *MessageService) StoreReceivedMessage(ctx context.Context, params MessageStoreParams) (*entities.Message, error) {
360+
ctx, span := service.tracer.Start(ctx)
361+
defer span.End()
362+
363+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
364+
365+
message := &entities.Message{
366+
ID: params.ID,
367+
From: params.From,
368+
To: params.To,
369+
Content: params.Content,
370+
Type: entities.MessageTypeMobileOriginated,
371+
Status: entities.MessageStatusReceived,
372+
RequestReceivedAt: params.Timestamp,
373+
CreatedAt: time.Now().UTC(),
374+
UpdatedAt: time.Now().UTC(),
375+
OrderTimestamp: params.Timestamp,
376+
ReceivedAt: &params.Timestamp,
377+
}
378+
379+
if err := service.repository.Store(ctx, message); err != nil {
380+
msg := fmt.Sprintf("cannot save message with id [%s]", params.ID)
381+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
382+
}
383+
384+
ctxLogger.Info(fmt.Sprintf("message saved with id [%s] in the repository", message.ID))
385+
return message, nil
386+
}
387+
307388
// HandleMessageParams are parameters for handling a message event
308389
type HandleMessageParams struct {
309390
ID uuid.UUID
@@ -368,6 +449,10 @@ func (service *MessageService) createMessageAPISentEvent(source string, payload
368449
return service.createEvent(events.EventTypeMessageAPISent, source, payload)
369450
}
370451

452+
func (service *MessageService) createMessagePhoneReceivedEvent(source string, payload events.MessagePhoneReceivedPayload) (cloudevents.Event, error) {
453+
return service.createEvent(events.EventTypeMessagePhoneReceived, source, payload)
454+
}
455+
371456
func (service *MessageService) createMessagePhoneSendingEvent(source string, payload events.MessagePhoneSendingPayload) (cloudevents.Event, error) {
372457
return service.createEvent(events.EventTypeMessagePhoneSending, source, payload)
373458
}

0 commit comments

Comments
 (0)