Skip to content

Commit 490f4c9

Browse files
committed
Add ability to auto-reply phone calls
1 parent 901b02b commit 490f4c9

File tree

6 files changed

+101
-9
lines changed

6 files changed

+101
-9
lines changed

api/pkg/entities/phone.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type Phone struct {
2020
// MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.
2121
MessageExpirationSeconds uint `json:"message_expiration_seconds"`
2222

23+
MissedCallAutoReply *string `json:"missed_call_auto_reply" example:"This phone cannot receive calls. Please send an SMS instead."`
24+
2325
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
2426
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
2527
}

api/pkg/listeners/message_listener.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func NewMessageListener(
4242
events.EventTypeMessageSendExpired: l.onMessageSendExpired,
4343
events.EventTypeMessageNotificationScheduled: l.onMessageNotificationScheduled,
4444
events.MessageThreadAPIDeleted: l.onMessageThreadAPIDeleted,
45+
events.MessageCallMissed: l.onMessageCallMissed,
4546
}
4647
}
4748

@@ -309,3 +310,22 @@ func (listener *MessageListener) onMessageThreadAPIDeleted(ctx context.Context,
309310

310311
return nil
311312
}
313+
314+
// onMessageThreadAPIDeleted handles the events.MessageThreadAPIDeleted event
315+
func (listener *MessageListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
316+
ctx, span := listener.tracer.Start(ctx)
317+
defer span.End()
318+
319+
payload := new(events.MessageCallMissedPayload)
320+
if err := event.DataAs(payload); err != nil {
321+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
322+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
323+
}
324+
325+
if err := listener.service.RespondToMissedCall(ctx, event.Source(), payload); err != nil {
326+
msg := fmt.Sprintf("cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID)
327+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
328+
}
329+
330+
return nil
331+
}

api/pkg/listeners/webhook_listener.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func NewWebhookListener(
3838
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
3939
events.EventTypePhoneHeartbeatOnline: l.onPhoneHeartbeatOnline,
4040
events.EventTypePhoneHeartbeatOffline: l.onPhoneHeartbeatOffline,
41+
events.MessageCallMissed: l.onMessageCallMissed,
4142
}
4243
}
4344

@@ -173,3 +174,22 @@ func (listener *WebhookListener) onPhoneHeartbeatOnline(ctx context.Context, eve
173174

174175
return nil
175176
}
177+
178+
// onMessageCallMissed handles the events.MessageCallMissed event
179+
func (listener *WebhookListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
180+
ctx, span := listener.tracer.Start(ctx)
181+
defer span.End()
182+
183+
var payload events.MessageCallMissedPayload
184+
if err := event.DataAs(&payload); err != nil {
185+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
186+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
187+
}
188+
189+
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
190+
msg := fmt.Sprintf("cannot process [%s] event with ID [%s]", event.Type(), event.ID())
191+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
192+
}
193+
194+
return nil
195+
}

api/pkg/requests/phone_update_request.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type PhoneUpsert struct {
2424

2525
FcmToken string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
2626

27+
MissedCallAutoReply *string `json:"missed_call_auto_reply" example:"e.g This phone cannot receive calls. Please send an SMS instead."`
28+
2729
// SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot
2830
SIM string `json:"sim" example:"SIM1"`
2931
}
@@ -33,11 +35,14 @@ func (input *PhoneUpsert) Sanitize() PhoneUpsert {
3335
input.FcmToken = strings.TrimSpace(input.FcmToken)
3436
input.PhoneNumber = input.sanitizeAddress(input.PhoneNumber)
3537
input.SIM = input.sanitizeSIM(input.SIM)
38+
if input.MissedCallAutoReply != nil {
39+
input.MissedCallAutoReply = input.sanitizeStringPointer(*input.MissedCallAutoReply)
40+
}
3641
return *input
3742
}
3843

3944
// ToUpsertParams converts PhoneUpsert to services.PhoneUpsertParams
40-
func (input *PhoneUpsert) ToUpsertParams(user entities.AuthUser, source string) services.PhoneUpsertParams {
45+
func (input *PhoneUpsert) ToUpsertParams(user entities.AuthUser, source string) *services.PhoneUpsertParams {
4146
phone, _ := phonenumbers.Parse(input.PhoneNumber, phonenumbers.UNKNOWN_REGION)
4247

4348
// ignore value if it's default
@@ -64,9 +69,9 @@ func (input *PhoneUpsert) ToUpsertParams(user entities.AuthUser, source string)
6469
maxSendAttempts = &input.MaxSendAttempts
6570
}
6671

67-
return services.PhoneUpsertParams{
72+
return &services.PhoneUpsertParams{
6873
Source: source,
69-
PhoneNumber: *phone,
74+
PhoneNumber: phone,
7075
MessagesPerMinute: messagesPerMinute,
7176
MessageExpirationDuration: timeout,
7277
MaxSendAttempts: maxSendAttempts,

api/pkg/services/message_service.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package services
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"time"
78

89
"github.com/davecgh/go-spew/spew"
@@ -147,6 +148,44 @@ func (service *MessageService) DeleteByOwnerAndContact(ctx context.Context, user
147148
return nil
148149
}
149150

151+
// RespondToMissedCall creates an SMS response to a missed phone call on the android phone
152+
func (service *MessageService) RespondToMissedCall(ctx context.Context, source string, payload *events.MessageCallMissedPayload) error {
153+
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
154+
defer span.End()
155+
156+
phone, err := service.phoneService.Load(ctx, payload.UserID, payload.Owner)
157+
if err != nil {
158+
msg := fmt.Sprintf("cannot find phone with owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)
159+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
160+
}
161+
162+
if phone.MissedCallAutoReply == nil || strings.TrimSpace(*phone.MissedCallAutoReply) == "" {
163+
ctxLogger.Info(fmt.Sprintf("no auto reply set for phone [%s] for message [%s] with user [%s]", payload.Owner, payload.MessageID, payload.UserID))
164+
return nil
165+
}
166+
167+
requestID := fmt.Sprintf("missed-call-%s", payload.MessageID)
168+
owner, _ := phonenumbers.Parse(payload.Owner, phonenumbers.UNKNOWN_REGION)
169+
message, err := service.SendMessage(ctx, MessageSendParams{
170+
Owner: owner,
171+
Contact: payload.Contact,
172+
Encrypted: false,
173+
Content: *phone.MissedCallAutoReply,
174+
Source: source,
175+
SendAt: nil,
176+
RequestID: &requestID,
177+
UserID: payload.UserID,
178+
RequestReceivedAt: time.Now().UTC(),
179+
})
180+
if err != nil {
181+
msg := fmt.Sprintf("cannot send auto response message for owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)
182+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
183+
}
184+
185+
ctxLogger.Info(fmt.Sprintf("created response message with ID [%s] for missed call event [%s] for user [%s]", message.ID, payload.MessageID, message.UserID))
186+
return nil
187+
}
188+
150189
// MessageGetParams parameters for sending a new message
151190
type MessageGetParams struct {
152191
repositories.IndexParams

api/pkg/services/phone_service.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,26 @@ func (service *PhoneService) Load(ctx context.Context, userID entities.UserID, o
6969

7070
// PhoneUpsertParams are parameters for creating a new entities.Phone
7171
type PhoneUpsertParams struct {
72-
PhoneNumber phonenumbers.PhoneNumber
72+
PhoneNumber *phonenumbers.PhoneNumber
7373
FcmToken *string
7474
MessagesPerMinute *uint
7575
MaxSendAttempts *uint
7676
WebhookURL *string
7777
MessageExpirationDuration *time.Duration
78+
MissedCallAutoReply *string
7879
SIM entities.SIM
7980
Source string
8081
UserID entities.UserID
8182
}
8283

8384
// Upsert a new entities.Phone
84-
func (service *PhoneService) Upsert(ctx context.Context, params PhoneUpsertParams) (*entities.Phone, error) {
85+
func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertParams) (*entities.Phone, error) {
8586
ctx, span := service.tracer.Start(ctx)
8687
defer span.End()
8788

8889
ctxLogger := service.tracer.CtxLogger(service.logger, span)
8990

90-
phone, err := service.repository.Load(ctx, params.UserID, phonenumbers.Format(&params.PhoneNumber, phonenumbers.E164))
91+
phone, err := service.repository.Load(ctx, params.UserID, phonenumbers.Format(params.PhoneNumber, phonenumbers.E164))
9192
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
9293
return service.createPhone(ctx, params)
9394
}
@@ -169,7 +170,7 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e
169170
return nil
170171
}
171172

172-
func (service *PhoneService) createPhone(ctx context.Context, params PhoneUpsertParams) (*entities.Phone, error) {
173+
func (service *PhoneService) createPhone(ctx context.Context, params *PhoneUpsertParams) (*entities.Phone, error) {
173174
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
174175
defer span.End()
175176

@@ -183,7 +184,8 @@ func (service *PhoneService) createPhone(ctx context.Context, params PhoneUpsert
183184
MessageExpirationSeconds: 10 * 60, // 10 minutes
184185
MaxSendAttempts: 2,
185186
SIM: params.SIM,
186-
PhoneNumber: phonenumbers.Format(&params.PhoneNumber, phonenumbers.E164),
187+
MissedCallAutoReply: nil,
188+
PhoneNumber: phonenumbers.Format(params.PhoneNumber, phonenumbers.E164),
187189
CreatedAt: time.Now().UTC(),
188190
UpdatedAt: time.Now().UTC(),
189191
}
@@ -205,7 +207,7 @@ func (service *PhoneService) createPhoneDeletedEvent(source string, payload even
205207
return service.createEvent(events.EventTypePhoneDeleted, source, payload)
206208
}
207209

208-
func (service *PhoneService) update(phone *entities.Phone, params PhoneUpsertParams) *entities.Phone {
210+
func (service *PhoneService) update(phone *entities.Phone, params *PhoneUpsertParams) *entities.Phone {
209211
if phone.FcmToken != nil {
210212
phone.FcmToken = params.FcmToken
211213
}
@@ -221,6 +223,10 @@ func (service *PhoneService) update(phone *entities.Phone, params PhoneUpsertPar
221223
phone.MessageExpirationSeconds = uint(params.MessageExpirationDuration.Seconds())
222224
}
223225

226+
if params.MissedCallAutoReply != nil {
227+
phone.MissedCallAutoReply = params.MissedCallAutoReply
228+
}
229+
224230
phone.SIM = params.SIM
225231

226232
return phone

0 commit comments

Comments
 (0)