Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ SECURITY_AUDIT_REPORT.md

*.exe

docs/
.output
.agents/
skills-lock.json
16 changes: 14 additions & 2 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3379,7 +3379,7 @@ const docTemplate = `{
},
"request_id": {
"type": "string",
"example": "bulk-csv-a1B2c3D4e5"
"example": "bulk-httpsms-file.csv"
},
"scheduled_count": {
"type": "integer",
Expand Down Expand Up @@ -3760,6 +3760,7 @@ const docTemplate = `{
"messages_per_minute",
"phone_number",
"sim",
"unarchive_thread",
"updated_at",
"user_id"
],
Expand Down Expand Up @@ -3804,6 +3805,11 @@ const docTemplate = `{
"sim": {
"$ref": "#/definitions/entities.SIM"
},
"unarchive_thread": {
"description": "UnarchiveThread moves an archived message thread back to the inbox when a new message is received on this phone.",
"type": "boolean",
"example": false
},
"updated_at": {
"type": "string",
"example": "2022-06-05T14:26:10.303278+03:00"
Expand Down Expand Up @@ -4436,7 +4442,8 @@ const docTemplate = `{
"messages_per_minute",
"missed_call_auto_reply",
"phone_number",
"sim"
"sim",
"unarchive_thread"
],
"properties": {
"fcm_token": {
Expand Down Expand Up @@ -4473,6 +4480,11 @@ const docTemplate = `{
"description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot",
"type": "string",
"example": "SIM1"
},
"unarchive_thread": {
"description": "UnarchiveThread moves an archived thread back to the inbox when a new message is received on this phone.",
"type": "boolean",
"example": false
}
}
},
Expand Down
16 changes: 14 additions & 2 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3376,7 +3376,7 @@
},
"request_id": {
"type": "string",
"example": "bulk-csv-a1B2c3D4e5"
"example": "bulk-httpsms-file.csv"
},
"scheduled_count": {
"type": "integer",
Expand Down Expand Up @@ -3757,6 +3757,7 @@
"messages_per_minute",
"phone_number",
"sim",
"unarchive_thread",
"updated_at",
"user_id"
],
Expand Down Expand Up @@ -3801,6 +3802,11 @@
"sim": {
"$ref": "#/definitions/entities.SIM"
},
"unarchive_thread": {
"description": "UnarchiveThread moves an archived message thread back to the inbox when a new message is received on this phone.",
"type": "boolean",
"example": false
},
"updated_at": {
"type": "string",
"example": "2022-06-05T14:26:10.303278+03:00"
Expand Down Expand Up @@ -4433,7 +4439,8 @@
"messages_per_minute",
"missed_call_auto_reply",
"phone_number",
"sim"
"sim",
"unarchive_thread"
],
"properties": {
"fcm_token": {
Expand Down Expand Up @@ -4470,6 +4477,11 @@
"description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot",
"type": "string",
"example": "SIM1"
},
"unarchive_thread": {
"description": "UnarchiveThread moves an archived thread back to the inbox when a new message is received on this phone.",
"type": "boolean",
"example": false
}
}
},
Expand Down
14 changes: 13 additions & 1 deletion api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ definitions:
example: 30
type: integer
request_id:
example: bulk-csv-a1B2c3D4e5
example: bulk-httpsms-file.csv
type: string
scheduled_count:
example: 50
Expand Down Expand Up @@ -388,6 +388,11 @@ definitions:
type: string
sim:
$ref: '#/definitions/entities.SIM'
unarchive_thread:
description: UnarchiveThread moves an archived message thread back to the
inbox when a new message is received on this phone.
example: false
type: boolean
updated_at:
example: "2022-06-05T14:26:10.303278+03:00"
type: string
Expand All @@ -402,6 +407,7 @@ definitions:
- messages_per_minute
- phone_number
- sim
- unarchive_thread
- updated_at
- user_id
type: object
Expand Down Expand Up @@ -908,6 +914,11 @@ definitions:
1 SIM slot
example: SIM1
type: string
unarchive_thread:
description: UnarchiveThread moves an archived thread back to the inbox when
a new message is received on this phone.
example: false
type: boolean
required:
- fcm_token
- max_send_attempts
Expand All @@ -916,6 +927,7 @@ definitions:
- missed_call_auto_reply
- phone_number
- sim
- unarchive_thread
type: object
requests.UserNotificationUpdate:
properties:
Expand Down
1 change: 1 addition & 0 deletions api/pkg/di/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ func (container *Container) MessageThreadService() (service *services.MessageThr
container.Logger(),
container.Tracer(),
container.MessageThreadRepository(),
container.PhoneRepository(),
container.EventDispatcher(),
)
}
Expand Down
3 changes: 3 additions & 0 deletions api/pkg/entities/phone.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type Phone struct {

MissedCallAutoReply *string `json:"missed_call_auto_reply" example:"This phone cannot receive calls. Please send an SMS instead." validate:"optional"`

// UnarchiveThread moves an archived message thread back to the inbox when a new message is received on this phone.
UnarchiveThread bool `json:"unarchive_thread" gorm:"default:false" example:"false"`

CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
}
Expand Down
4 changes: 2 additions & 2 deletions api/pkg/handlers/message_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func (h *MessageHandler) Index(c fiber.Ctx) error {

messages, err := h.service.GetMessages(ctx, request.ToGetParams(h.userIDFomContext(c)))
if err != nil {
msg := fmt.Sprintf("cannot get messgaes with params [%+#v]", request)
msg := fmt.Sprintf("cannot get messages with params [%+#v]", request)
ctxLogger.Error(stacktrace.Propagate(err, msg))
return h.responseInternalServerError(c)
}
Expand Down Expand Up @@ -383,7 +383,7 @@ func (h *MessageHandler) PostReceive(c fiber.Ctx) error {

message, err := h.service.ReceiveMessage(ctx, request.ToMessageReceiveParams(h.userIDFomContext(c), c.OriginalURL()))
if err != nil {
msg := fmt.Sprintf("cannot receive message with paylod [%s]", c.Body())
msg := fmt.Sprintf("cannot receive message with payload [%s]", c.Body())
ctxLogger.Error(stacktrace.Propagate(err, msg))
return h.responseInternalServerError(c)
}
Expand Down
9 changes: 9 additions & 0 deletions api/pkg/requests/phone_update_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ type PhoneUpsert struct {

MissedCallAutoReply *string `json:"missed_call_auto_reply" example:"e.g. This phone cannot receive calls. Please send an SMS instead."`

// UnarchiveThread moves an archived thread back to the inbox when a new message is received on this phone.
UnarchiveThread bool `json:"unarchive_thread" example:"false"`

Comment on lines 30 to +34

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the stated goal (keep the field optional in the OpenAPI schema) is not achievable in this repo. Swagger is generated with --requiredByDefault, which marks every field required regardless of pointer type. missed_call_auto_reply is a *string and is already listed under required for requests.PhoneUpsert, so switching unarchive_thread to *bool would not remove it from required. The non-pointer bool is consistent with the other partial-update fields (max_send_attempts, messages_per_minute), and the server never enforces required at runtime — absent keys are skipped via the fields presence check, so partial updates keep working.

// SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot
SIM string `json:"sim" example:"SIM1"`

Expand Down Expand Up @@ -75,6 +78,11 @@ func (input *PhoneUpsert) ToUpsertParams(user entities.AuthContext, source strin
maxSendAttempts = &input.MaxSendAttempts
}

var unarchiveThread *bool
if _, exists := fields["unarchive_thread"]; exists {
unarchiveThread = &input.UnarchiveThread
}

var scheduleID *uuid.UUID
if _, exists := fields["message_send_schedule_id"]; exists {
if parsed, err := uuid.Parse(strings.TrimSpace(input.MessageSendScheduleID)); err == nil {
Expand All @@ -92,6 +100,7 @@ func (input *PhoneUpsert) ToUpsertParams(user entities.AuthContext, source strin
FcmToken: fcmToken,
UserID: user.ID,
SIM: entities.SIM(input.SIM),
UnarchiveThread: unarchiveThread,
MessageSendScheduleID: scheduleID,
}
}
4 changes: 3 additions & 1 deletion api/pkg/services/message_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,13 @@ func (service *MessageService) ReceiveMessage(ctx context.Context, params *Messa
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
}

owner := phonenumbers.Format(&params.Owner, phonenumbers.E164)

eventPayload := events.MessagePhoneReceivedPayload{
MessageID: messageID,
UserID: params.UserID,
Encrypted: params.Encrypted,
Owner: phonenumbers.Format(&params.Owner, phonenumbers.E164),
Owner: owner,
Contact: params.Contact,
Timestamp: params.Timestamp,
Content: params.Content,
Expand Down
21 changes: 21 additions & 0 deletions api/pkg/services/message_thread_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type MessageThreadService struct {
logger telemetry.Logger
tracer telemetry.Tracer
repository repositories.MessageThreadRepository
phoneRepository repositories.PhoneRepository
eventDispatcher *EventDispatcher
}

Expand All @@ -29,13 +30,15 @@ func NewMessageThreadService(
logger telemetry.Logger,
tracer telemetry.Tracer,
repository repositories.MessageThreadRepository,
phoneRepository repositories.PhoneRepository,
eventDispatcher *EventDispatcher,
) (s *MessageThreadService) {
return &MessageThreadService{
logger: logger.WithService(fmt.Sprintf("%T", s)),
tracer: tracer,
eventDispatcher: eventDispatcher,
repository: repository,
phoneRepository: phoneRepository,
}
}

Expand All @@ -50,6 +53,14 @@ type MessageThreadUpdateParams struct {
Timestamp time.Time
}

// shouldCheckUnarchive reports whether a thread update is a new inbound message
// landing on an archived thread. Only in that case is the phone's
// UnarchiveThread setting consulted, so the phone is not loaded on the common
// path where the thread is not archived.
func (service *MessageThreadService) shouldCheckUnarchive(thread *entities.MessageThread, params MessageThreadUpdateParams) bool {
return thread.IsArchived && params.Status == entities.MessageStatusReceived
}

// DeleteAllForUser deletes all entities.MessageThread for an entities.UserID.
func (service *MessageThreadService) DeleteAllForUser(ctx context.Context, userID entities.UserID) error {
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
Expand Down Expand Up @@ -92,6 +103,16 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
return nil
}

if service.shouldCheckUnarchive(thread, params) {
phone, phoneErr := service.phoneRepository.Load(ctx, params.UserID, params.Owner)
if phoneErr != nil {
ctxLogger.Warn(stacktrace.Propagate(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID))
} else if phone.UnarchiveThread {
Comment on lines +106 to +110

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR description to match the implemented flow. The event-based approach was intentionally replaced with a lazy phone load in UpdateThread (gated by shouldCheckUnarchive) so the common non-archived path does zero extra DB reads; the phone is only read when an inbound message hits an already-archived thread.

thread.UpdateArchive(false)
ctxLogger.Info(fmt.Sprintf("unarchiving thread [%s] after inbound message [%s]", thread.ID, params.MessageID))
}
}

if err = service.repository.Update(ctx, thread.Update(params.Timestamp, params.MessageID, params.Content, params.Status)); err != nil {
msg := fmt.Sprintf("cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
Expand Down
22 changes: 22 additions & 0 deletions api/pkg/services/message_thread_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package services

import (
"testing"

"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/stretchr/testify/assert"
)

func TestShouldCheckUnarchive(t *testing.T) {
service := &MessageThreadService{}

archived := &entities.MessageThread{IsArchived: true}
notArchived := &entities.MessageThread{IsArchived: false}

received := MessageThreadUpdateParams{Status: entities.MessageStatusReceived}
sent := MessageThreadUpdateParams{Status: entities.MessageStatusSent}

assert.True(t, service.shouldCheckUnarchive(archived, received), "archived + inbound -> consult phone setting")
assert.False(t, service.shouldCheckUnarchive(archived, sent), "outbound status -> no check")
assert.False(t, service.shouldCheckUnarchive(notArchived, received), "already unarchived -> no check")
}
Comment on lines +19 to +23

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a4fc0af. TestShouldCheckUnarchive now covers not-archived + non-received.

5 changes: 5 additions & 0 deletions api/pkg/services/phone_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type PhoneUpsertParams struct {
WebhookURL *string
MessageExpirationDuration *time.Duration
MissedCallAutoReply *string
UnarchiveThread *bool
SIM entities.SIM
MessageSendScheduleID *uuid.UUID
Source string
Expand Down Expand Up @@ -312,6 +313,10 @@ func (service *PhoneService) update(phone *entities.Phone, params *PhoneUpsertPa
phone.MissedCallAutoReply = params.MissedCallAutoReply
}

if params.UnarchiveThread != nil {
phone.UnarchiveThread = *params.UnarchiveThread
}

phone.SIM = params.SIM
phone.MessageSendScheduleID = params.MessageSendScheduleID

Expand Down
Loading
Loading