Skip to content

Commit 901b02b

Browse files
committed
Add the ability to store phone call events
1 parent cc294bf commit 901b02b

File tree

10 files changed

+231
-12
lines changed

10 files changed

+231
-12
lines changed

android/app/src/main/java/com/httpsms/HttpSmsApiService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
111111
""".trimIndent()
112112

113113
val request: Request = Request.Builder()
114-
.url(resolveURL("/v1/calls/missed"))
114+
.url(resolveURL("/v1/messages/calls/missed"))
115115
.post(body.toRequestBody(jsonMediaType))
116116
.header(apiKeyHeader, apiKey)
117117
.header(clientVersionHeader, BuildConfig.VERSION_NAME)
@@ -121,7 +121,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
121121
if (!response.isSuccessful) {
122122
Timber.e("error response [${response.body?.string()}] with code [${response.code}] while sending missed call event [${body}]")
123123
response.close()
124-
return false
124+
return response.code == 422
125125
}
126126

127127
response.close()

api/pkg/entities/message.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const (
1515

1616
// MessageTypeMobileOriginated means the message comes directly from a mobile phone
1717
MessageTypeMobileOriginated = "mobile-originated"
18+
19+
// MessageTypeCallMissed means the message is generated when a phone call is missed by the android phone
20+
MessageTypeCallMissed = "call/missed"
1821
)
1922

2023
// MessageStatus is the status of the message
@@ -33,7 +36,7 @@ const (
3336
// MessageStatusSent means the message has already sent by the mobile phone
3437
MessageStatusSent = "sent"
3538

36-
// MessageStatusReceived means the message was received by tne mobile phone (MO)
39+
// MessageStatusReceived means the message was received by the mobile phone (MO) or a phone call is missed by the mobile phone
3740
MessageStatusReceived = "received"
3841

3942
// MessageStatusFailed means the mobile phone could not send the message
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
8+
"github.com/google/uuid"
9+
)
10+
11+
// MessageCallMissed is emitted when a new message is sent
12+
const MessageCallMissed = "message.call.missed"
13+
14+
// MessageCallMissedPayload is the payload of the MessageCallMissed event
15+
type MessageCallMissedPayload struct {
16+
MessageID uuid.UUID `json:"message_id"`
17+
UserID entities.UserID `json:"user_id"`
18+
Owner string `json:"owner"`
19+
Contact string `json:"contact"`
20+
Timestamp time.Time `json:"timestamp"`
21+
SIM entities.SIM `json:"sim"`
22+
}

api/pkg/handlers/message_handler.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (h *MessageHandler) RegisterRoutes(router fiber.Router) {
5252
router.Post("/messages/send", h.PostSend)
5353
router.Post("/messages/bulk-send", h.BulkSend)
5454
router.Post("/messages/receive", h.PostReceive)
55+
router.Post("/messages/calls/missed", h.PostCallMissed)
5556
router.Get("/messages/outstanding", h.GetOutstanding)
5657
router.Get("/messages", h.Index)
5758
router.Post("/messages/:messageID/events", h.PostEvent)
@@ -417,3 +418,47 @@ func (h *MessageHandler) Delete(c *fiber.Ctx) error {
417418

418419
return h.responseNoContent(c, "message deleted successfully")
419420
}
421+
422+
// PostCallMissed registers a missed phone call
423+
// @Summary Register a missed call event on the mobile phone
424+
// @Description This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone.
425+
// @Security ApiKeyAuth
426+
// @Tags Messages
427+
// @Accept json
428+
// @Produce json
429+
// @Param payload body requests.MessageCallMissed true "Payload of the missed call event."
430+
// @Success 200 {object} responses.MessageResponse
431+
// @Failure 400 {object} responses.BadRequest
432+
// @Failure 401 {object} responses.Unauthorized
433+
// @Failure 404 {object} responses.NotFound
434+
// @Failure 422 {object} responses.UnprocessableEntity
435+
// @Failure 500 {object} responses.InternalServerError
436+
// @Router /messages/calls/missed [post]
437+
func (h *MessageHandler) PostCallMissed(c *fiber.Ctx) error {
438+
ctx, span := h.tracer.StartFromFiberCtx(c)
439+
defer span.End()
440+
441+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
442+
443+
var request requests.MessageCallMissed
444+
if err := c.BodyParser(&request); err != nil {
445+
msg := fmt.Sprintf("cannot marshall [%s] into %T", c.Body(), request)
446+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
447+
return h.responseBadRequest(c, err)
448+
}
449+
450+
if errors := h.validator.ValidateCallMissed(ctx, request.Sanitize()); len(errors) != 0 {
451+
msg := fmt.Sprintf("validation errors [%s], for missed call event [%s]", spew.Sdump(errors), c.Body())
452+
ctxLogger.Warn(stacktrace.NewError(msg))
453+
return h.responseUnprocessableEntity(c, errors, "validation errors while storing missed call event")
454+
}
455+
456+
message, err := h.service.RegisterMissedCall(ctx, request.ToCallMissedParams(h.userIDFomContext(c), c.OriginalURL()))
457+
if err != nil {
458+
msg := fmt.Sprintf("cannot store missed call event for user [%s] with paylod [%s]", h.userIDFomContext(c), c.Body())
459+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
460+
return h.responseInternalServerError(c)
461+
}
462+
463+
return h.responseOK(c, "missed call event stored successfully", message)
464+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
// MessageCallMissed is the payload for sending and missed call event
14+
type MessageCallMissed struct {
15+
request
16+
From string `json:"from" example:"+18005550199"`
17+
To string `json:"to" example:"+18005550100"`
18+
SIM string `json:"sim" example:"SIM1"`
19+
Timestamp time.Time `json:"timestamp" example:"2022-06-05T14:26:09.527976+03:00"`
20+
}
21+
22+
// Sanitize sets defaults to MessageReceive
23+
func (input *MessageCallMissed) Sanitize() MessageCallMissed {
24+
input.To = input.sanitizeAddress(input.To)
25+
input.From = input.sanitizeAddress(input.From)
26+
input.SIM = input.sanitizeSIM(input.SIM)
27+
28+
return *input
29+
}
30+
31+
// ToCallMissedParams converts MessageCallMissed to services.MessageSendParams
32+
func (input *MessageCallMissed) ToCallMissedParams(userID entities.UserID, source string) *services.MissedCallParams {
33+
to, _ := phonenumbers.Parse(input.From, phonenumbers.UNKNOWN_REGION)
34+
return &services.MissedCallParams{
35+
Source: source,
36+
Owner: to,
37+
Timestamp: input.Timestamp,
38+
SIM: entities.SIM(input.SIM),
39+
UserID: userID,
40+
Contact: input.From,
41+
}
42+
}

api/pkg/requests/phone_update_request.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ type PhoneUpsert struct {
3232
func (input *PhoneUpsert) Sanitize() PhoneUpsert {
3333
input.FcmToken = strings.TrimSpace(input.FcmToken)
3434
input.PhoneNumber = input.sanitizeAddress(input.PhoneNumber)
35-
if input.SIM == "" {
36-
input.SIM = entities.SIM1.String()
37-
}
35+
input.SIM = input.sanitizeSIM(input.SIM)
3836
return *input
3937
}
4038

api/pkg/requests/request.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@ import (
66
"strings"
77
"unicode"
88

9+
"github.com/NdoleStudio/httpsms/pkg/entities"
10+
911
"github.com/nyaruka/phonenumbers"
1012
)
1113

1214
type request struct{}
1315

14-
// getLimit gets the take as a string
1516
func (input *request) sanitizeAddress(value string) string {
16-
value = strings.TrimRight(value, " ")
17-
if len(value) > 0 && value[0] == ' ' {
18-
value = strings.Replace(value, " ", "+", 1)
19-
}
20-
17+
value = strings.TrimSpace(value)
2118
if !strings.HasPrefix(value, "+") && input.isDigits(value) && len(value) > 9 {
2219
value = "+" + value
2320
}
@@ -43,6 +40,13 @@ func (input *request) sanitizeBool(value string) string {
4340
return value
4441
}
4542

43+
func (input *request) sanitizeSIM(value string) string {
44+
if value == entities.SIM1.String() || value == entities.SIM2.String() {
45+
return value
46+
}
47+
return entities.SIM1.String()
48+
}
49+
4650
func (input *request) sanitizeURL(value string) string {
4751
value = strings.TrimSpace(value)
4852
website, err := url.Parse(value)

api/pkg/services/message_service.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,55 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe
416416
return message, err
417417
}
418418

419+
// MissedCallParams parameters for sending a new message
420+
type MissedCallParams struct {
421+
Owner *phonenumbers.PhoneNumber
422+
Contact string
423+
Source string
424+
SIM entities.SIM
425+
Timestamp time.Time
426+
UserID entities.UserID
427+
}
428+
429+
// RegisterMissedCall a new message
430+
func (service *MessageService) RegisterMissedCall(ctx context.Context, params *MissedCallParams) (*entities.Message, error) {
431+
ctx, span := service.tracer.Start(ctx)
432+
defer span.End()
433+
434+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
435+
436+
eventPayload := &events.MessageCallMissedPayload{
437+
MessageID: uuid.New(),
438+
UserID: params.UserID,
439+
Timestamp: params.Timestamp,
440+
Owner: phonenumbers.Format(params.Owner, phonenumbers.E164),
441+
Contact: params.Contact,
442+
SIM: params.SIM,
443+
}
444+
445+
event, err := service.createEvent(events.MessageCallMissed, params.Source, eventPayload)
446+
if err != nil {
447+
msg := fmt.Sprintf("cannot create [%T] from payload with message id [%s]", event, eventPayload.MessageID)
448+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
449+
}
450+
451+
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s] and user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID))
452+
453+
message, err := service.storeMissedCallMessage(ctx, eventPayload)
454+
if err != nil {
455+
msg := fmt.Sprintf("cannot store missed call message message with id [%s]", eventPayload.MessageID)
456+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
457+
}
458+
459+
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
460+
msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())
461+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
462+
}
463+
464+
ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID))
465+
return message, err
466+
}
467+
419468
func (service *MessageService) getSendDelay(ctxLogger telemetry.Logger, eventPayload events.MessageAPISentPayload, sendAt *time.Time) time.Duration {
420469
if sendAt == nil {
421470
return time.Duration(0)
@@ -848,6 +897,36 @@ func (service *MessageService) storeSentMessage(ctx context.Context, payload eve
848897
return message, nil
849898
}
850899

900+
// storeMissedCallMessage a new message
901+
func (service *MessageService) storeMissedCallMessage(ctx context.Context, payload *events.MessageCallMissedPayload) (*entities.Message, error) {
902+
ctx, span := service.tracer.Start(ctx)
903+
defer span.End()
904+
905+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
906+
907+
message := &entities.Message{
908+
ID: payload.MessageID,
909+
Owner: payload.Owner,
910+
Contact: payload.Contact,
911+
UserID: payload.UserID,
912+
SIM: payload.SIM,
913+
Type: entities.MessageTypeCallMissed,
914+
Status: entities.MessageStatusReceived,
915+
RequestReceivedAt: payload.Timestamp,
916+
CreatedAt: time.Now().UTC(),
917+
UpdatedAt: time.Now().UTC(),
918+
OrderTimestamp: payload.Timestamp,
919+
}
920+
921+
if err := service.repository.Store(ctx, message); err != nil {
922+
msg := fmt.Sprintf("cannot save missed call message with id [%s]", payload.MessageID)
923+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
924+
}
925+
926+
ctxLogger.Info(fmt.Sprintf("missed call message saved with id [%s]", payload.MessageID))
927+
return message, nil
928+
}
929+
851930
func (service *MessageService) createMessageSendExpiredEvent(source string, payload events.MessageSendExpiredPayload) (cloudevents.Event, error) {
852931
return service.createEvent(events.EventTypeMessageSendExpired, source, payload)
853932
}

api/pkg/validators/message_handler_validator.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,28 @@ func (validator MessageHandlerValidator) ValidateMessageEvent(_ context.Context,
228228
})
229229
return v.ValidateStruct()
230230
}
231+
232+
// ValidateCallMissed validates the requests.MessageCallMissed request
233+
func (validator MessageHandlerValidator) ValidateCallMissed(_ context.Context, request requests.MessageCallMissed) url.Values {
234+
v := govalidator.New(govalidator.Options{
235+
Data: &request,
236+
Rules: govalidator.MapData{
237+
"to": []string{
238+
"required",
239+
phoneNumberRule,
240+
},
241+
"from": []string{
242+
"required",
243+
},
244+
"sim": []string{
245+
"required",
246+
"in:" + strings.Join([]string{
247+
string(entities.SIM1),
248+
string(entities.SIM2),
249+
}, ","),
250+
},
251+
},
252+
})
253+
254+
return v.ValidateStruct()
255+
}

api/pkg/validators/validator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func init() {
8686
events.EventTypeMessageSendExpired: true,
8787
events.EventTypePhoneHeartbeatOnline: true,
8888
events.EventTypePhoneHeartbeatOffline: true,
89+
events.MessageCallMissed: true,
8990
}
9091

9192
for _, event := range input {

0 commit comments

Comments
 (0)