Skip to content

Commit 31d14d4

Browse files
committed
Added ability to delete messages via the API. NdoleStudio#322
1 parent 2240beb commit 31d14d4

10 files changed

Lines changed: 184 additions & 8 deletions

api/pkg/entities/message.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const (
4444

4545
// MessageStatusExpired means the message could not be sent by the mobile phone after 5 minutes
4646
MessageStatusExpired = "expired"
47+
48+
// MessageStatusDeleted is for deleted messages and threads
49+
MessageStatusDeleted = "deleted"
4750
)
4851

4952
// MessageEventName is the type of event generated by the mobile phone for a message

api/pkg/entities/message_thread.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ type MessageThread struct {
1515
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1616
Color string `json:"color" example:"indigo"`
1717
Status MessageStatus `json:"status" example:"PENDING"`
18-
LastMessageContent string `json:"last_message_content" example:"This is a sample message content"`
19-
LastMessageID uuid.UUID `json:"last_message_id" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
18+
LastMessageContent *string `json:"last_message_content" example:"This is a sample message content"`
19+
LastMessageID *uuid.UUID `json:"last_message_id" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
2020
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:09.527976+03:00"`
2121
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:09.527976+03:00"`
2222
OrderTimestamp time.Time `json:"order_timestamp" example:"2022-06-05T14:26:09.527976+03:00"`
@@ -25,9 +25,9 @@ type MessageThread struct {
2525
// Update a message thread after a message event
2626
func (thread *MessageThread) Update(timestamp time.Time, messageID uuid.UUID, content string, status MessageStatus) *MessageThread {
2727
thread.OrderTimestamp = timestamp
28-
thread.LastMessageID = messageID
28+
thread.LastMessageID = &messageID
2929
thread.Status = status
30-
thread.LastMessageContent = content
30+
thread.LastMessageContent = &content
3131
return thread
3232
}
3333

@@ -36,3 +36,11 @@ func (thread *MessageThread) UpdateArchive(isArchived bool) *MessageThread {
3636
thread.IsArchived = isArchived
3737
return thread
3838
}
39+
40+
// HasLastMessage checks the last message in a thread by ID
41+
func (thread *MessageThread) HasLastMessage(id uuid.UUID) bool {
42+
if thread.LastMessageID == nil {
43+
return false
44+
}
45+
return *thread.LastMessageID == id
46+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
// MessageAPIDeleted is emitted when a new message is deleted
12+
const MessageAPIDeleted = "message.api.deleted"
13+
14+
// MessageAPIDeletedPayload is the payload of the MessageAPIDeleted event
15+
type MessageAPIDeletedPayload struct {
16+
MessageID uuid.UUID `json:"message_id"`
17+
UserID entities.UserID `json:"user_id"`
18+
Owner string `json:"owner"`
19+
RequestID *string `json:"request_id"`
20+
Contact string `json:"contact"`
21+
Timestamp time.Time `json:"timestamp"`
22+
Content string `json:"content"`
23+
SIM entities.SIM `json:"sim"`
24+
}

api/pkg/handlers/message_handler.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func (h *MessageHandler) RegisterRoutes(router fiber.Router) {
5555
router.Get("/messages/outstanding", h.GetOutstanding)
5656
router.Get("/messages", h.Index)
5757
router.Post("/messages/:messageID/events", h.PostEvent)
58+
router.Delete("/messages/:messageID", h.Delete)
5859
}
5960

6061
// PostSend a new entities.Message
@@ -368,3 +369,51 @@ func (h *MessageHandler) PostReceive(c *fiber.Ctx) error {
368369

369370
return h.responseOK(c, "message received successfully", message)
370371
}
372+
373+
// Delete a message
374+
// @Summary Delete a message from the database.
375+
// @Description Delete a message from the database and removes the message content from the list of threads.
376+
// @Security ApiKeyAuth
377+
// @Tags Messages
378+
// @Accept json
379+
// @Produce json
380+
// @Param messageID path string true "ID of the message" default(32343a19-da5e-4b1b-a767-3298a73703ca)
381+
// @Success 204 {object} responses.NoContent
382+
// @Failure 400 {object} responses.BadRequest
383+
// @Failure 401 {object} responses.Unauthorized
384+
// @Failure 404 {object} responses.NotFound
385+
// @Failure 422 {object} responses.UnprocessableEntity
386+
// @Failure 500 {object} responses.InternalServerError
387+
// @Router /messages/{messageID} [delete]
388+
func (h *MessageHandler) Delete(c *fiber.Ctx) error {
389+
ctx, span := h.tracer.StartFromFiberCtx(c)
390+
defer span.End()
391+
392+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
393+
394+
messageID := c.Params("messageID")
395+
if errors := h.validator.ValidateUUID(ctx, "messageID", messageID); len(errors) != 0 {
396+
msg := fmt.Sprintf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)
397+
ctxLogger.Warn(stacktrace.NewError(msg))
398+
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
399+
}
400+
401+
message, err := h.service.GetMessage(ctx, h.userIDFomContext(c), uuid.MustParse(messageID))
402+
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
403+
return h.responseNotFound(c, fmt.Sprintf("cannot find message with ID [%s]", messageID))
404+
}
405+
406+
if err != nil {
407+
msg := fmt.Sprintf("cannot find message with id [%s]", messageID)
408+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
409+
return h.responseInternalServerError(c)
410+
}
411+
412+
if err = h.service.DeleteMessage(ctx, c.OriginalURL(), message); err != nil {
413+
msg := fmt.Sprintf("cannot delete message with ID [%s] for user with ID [%s]", messageID, message.UserID)
414+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
415+
return h.responseInternalServerError(c)
416+
}
417+
418+
return h.responseNoContent(c, "message deleted successfully")
419+
}

api/pkg/repositories/gorm_message_repository.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ func NewGormMessageRepository(
3535
}
3636
}
3737

38+
// Delete a message by the ID
39+
func (repository *gormMessageRepository) Delete(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error {
40+
ctx, span := repository.tracer.Start(ctx)
41+
defer span.End()
42+
43+
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageID).Delete(&entities.Message{}).Error
44+
if err != nil {
45+
msg := fmt.Sprintf("cannot delete message with ID [%s] for user with ID [%s]", messageID, userID)
46+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
47+
}
48+
49+
return nil
50+
}
51+
3852
// Index entities.Message between 2 parties
3953
func (repository *gormMessageRepository) Index(ctx context.Context, userID entities.UserID, owner string, contact string, params IndexParams) (*[]entities.Message, error) {
4054
ctx, span := repository.tracer.Start(ctx)

api/pkg/repositories/gorm_message_thread_repository.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ func NewGormMessageThreadRepository(
3535
}
3636
}
3737

38+
// UpdateAfterDeletedMessage updates a thread after the original message has been deleted
39+
func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error {
40+
ctx, span := repository.tracer.Start(ctx)
41+
defer span.End()
42+
43+
err := repository.db.WithContext(ctx).Model(&entities.MessageThread{}).
44+
Where("user_id = ?", userID).
45+
Where("last_message_id = ?", messageID).
46+
Updates(map[string]any{
47+
"last_message_id": nil,
48+
"last_message_content": nil,
49+
"status": entities.MessageStatusDeleted,
50+
}).Error
51+
if err != nil {
52+
msg := fmt.Sprintf("cannot update thread after message is deleted with userID [%s] and messageID [%s]", userID, messageID)
53+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
54+
}
55+
56+
return nil
57+
}
58+
3859
// Store a new entities.MessageThread
3960
func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread *entities.MessageThread) error {
4061
ctx, span := repository.tracer.Start(ctx)

api/pkg/repositories/message_repository.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ type MessageRepository interface {
2323

2424
// GetOutstanding fetches an entities.Message which is outstanding
2525
GetOutstanding(ctx context.Context, userID entities.UserID, messageID uuid.UUID) (*entities.Message, error)
26+
27+
// Delete an entities.Message by ID
28+
Delete(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error
2629
}

api/pkg/repositories/message_thread_repository.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ type MessageThreadRepository interface {
2424

2525
// Index message threads for an owner
2626
Index(ctx context.Context, userID entities.UserID, owner string, archived bool, params IndexParams) (*[]entities.MessageThread, error)
27+
28+
// UpdateAfterDeletedMessage updates a thread after the original message has been deleted
29+
UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error
2730
}

api/pkg/services/message_service.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,43 @@ func (service *MessageService) GetOutstanding(ctx context.Context, params Messag
9292
return message, nil
9393
}
9494

95+
// DeleteMessage deletes a message from the database
96+
func (service *MessageService) DeleteMessage(ctx context.Context, source string, message *entities.Message) error {
97+
ctx, span := service.tracer.Start(ctx)
98+
defer span.End()
99+
100+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
101+
102+
if err := service.repository.Delete(ctx, message.UserID, message.ID); err != nil {
103+
msg := fmt.Sprintf("could not delete message with ID [%s] for user wit ID [%s]", message.ID, message.UserID)
104+
return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg))
105+
}
106+
107+
event, err := service.createEvent(events.MessageAPIDeleted, source, &events.MessageAPIDeletedPayload{
108+
MessageID: message.ID,
109+
UserID: message.UserID,
110+
Owner: message.Owner,
111+
RequestID: message.RequestID,
112+
Contact: message.Contact,
113+
Timestamp: time.Now().UTC(),
114+
Content: message.Content,
115+
SIM: message.SIM,
116+
})
117+
if err != nil {
118+
msg := fmt.Sprintf("cannot create [%T] for message with ID [%s]", event, message.ID)
119+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
120+
}
121+
122+
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
123+
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
124+
msg := fmt.Sprintf("cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)
125+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
126+
}
127+
128+
ctxLogger.Info(fmt.Sprintf("dispatched event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
129+
return nil
130+
}
131+
95132
// MessageGetParams parameters for sending a new message
96133
type MessageGetParams struct {
97134
repositories.IndexParams

api/pkg/services/message_thread_service.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
6262
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
6363
}
6464

65-
if thread.OrderTimestamp.Unix() > params.Timestamp.Unix() && thread.Status != entities.MessageStatusSending && thread.LastMessageID == params.MessageID {
65+
if thread.OrderTimestamp.Unix() > params.Timestamp.Unix() && thread.Status != entities.MessageStatusSending && thread.HasLastMessage(params.MessageID) {
6666
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("thread [%s] has timestamp [%s] and status [%s] which is greater than timestamp [%s] for message [%s] and status [%s]", thread.ID, thread.OrderTimestamp, thread.Status, params.Timestamp, params.MessageID, params.Status)))
6767
return nil
6868
}
6969

70-
if thread.Status == entities.MessageStatusDelivered && thread.LastMessageID == params.MessageID {
70+
if thread.Status == entities.MessageStatusDelivered && thread.LastMessageID != nil && thread.HasLastMessage(params.MessageID) {
7171
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("thread [%s] already has status [%s] not updating with status [%s] for message [%s]", thread.ID, thread.Status, params.Status, params.MessageID)))
7272
return nil
7373
}
@@ -110,6 +110,20 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me
110110
return thread, nil
111111
}
112112

113+
// DeleteLastMessage in thread after a message has been deleted
114+
func (service *MessageThreadService) DeleteLastMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error {
115+
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
116+
defer span.End()
117+
118+
if err := service.repository.UpdateAfterDeletedMessage(ctx, userID, messageID); err != nil {
119+
msg := fmt.Sprintf("cannot delete last message from thread with messageID [%s] and userID [%s]", messageID, userID)
120+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
121+
}
122+
123+
ctxLogger.Info(fmt.Sprintf("last message has been removed from thread with messageID [%s] and userID [%s]", messageID, userID))
124+
return nil
125+
}
126+
113127
func (service *MessageThreadService) createThread(ctx context.Context, params MessageThreadUpdateParams) error {
114128
ctx, span := service.tracer.Start(ctx)
115129
defer span.End()
@@ -123,9 +137,9 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me
123137
UserID: params.UserID,
124138
IsArchived: false,
125139
Color: service.getColor(),
126-
LastMessageContent: params.Content,
140+
LastMessageContent: &params.Content,
127141
Status: params.Status,
128-
LastMessageID: params.MessageID,
142+
LastMessageID: &params.MessageID,
129143
CreatedAt: time.Now().UTC(),
130144
UpdatedAt: time.Now().UTC(),
131145
OrderTimestamp: params.Timestamp,

0 commit comments

Comments
 (0)