Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin: allow Twilio messaging service SID as "from number" #1899

Merged
merged 37 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8cbbf5c
add messaging/sid support to mocktwilio
mastercactapus Aug 23, 2021
44d930c
inputTypes and helperText
dctalbot Sep 13, 2021
e9257e8
initial validation
KatieMSB Sep 13, 2021
03918af
refine validation
KatieMSB Sep 14, 2021
0455aad
update fields for sid support where needed
KatieMSB Sep 14, 2021
a18669c
server side validation
dctalbot Sep 14, 2021
984d3c1
refactor
dctalbot Sep 14, 2021
6616634
add plus sign for region code
dctalbot Sep 14, 2021
902532c
set type as tel for telelphone-only fields
dctalbot Sep 14, 2021
9dfbda4
merge latest
KatieMSB Sep 15, 2021
ecdad5a
refactor
dctalbot Sep 15, 2021
73cdf39
enforce format for telephone-only fields
dctalbot Sep 15, 2021
ef51f08
format displayName in backend
KatieMSB Sep 15, 2021
5b51f43
run generate
KatieMSB Sep 15, 2021
fb68e0f
Merge branch 'admin-toolbox-messaging-service-support' of github.com:…
KatieMSB Sep 15, 2021
4749dfc
run gofmt
KatieMSB Sep 15, 2021
b6096b7
fix: test debounced value for validation
dctalbot Sep 15, 2021
e9aa9e2
Merge branch 'admin-toolbox-messaging-service-support' of github.com:…
dctalbot Sep 15, 2021
583debc
refine regex pattern and handle special case
KatieMSB Sep 15, 2021
072528b
messenger sid -> twilio messaging service sid
KatieMSB Sep 15, 2021
e2f8fe7
fix config validation
KatieMSB Sep 16, 2021
59d0926
remove sid support for config From Number
KatieMSB Sep 16, 2021
f916ccf
undo changes to config display names
KatieMSB Sep 16, 2021
e20cd56
run gofmt
KatieMSB Sep 16, 2021
dd97346
fix formatting
KatieMSB Sep 16, 2021
86fce5b
Merge branch 'master' into admin-toolbox-messaging-service-support
dctalbot Sep 21, 2021
4a736ba
strip out non-alphanum chars
dctalbot Sep 22, 2021
9b19e97
update validation for from values
KatieMSB Sep 22, 2021
74a4e94
merge latest
KatieMSB Sep 22, 2021
dd6386c
sid -> twiliomessagesid
KatieMSB Sep 22, 2021
66654ec
update validation
KatieMSB Sep 22, 2021
1e8d4bb
don't assign From value until call/sms is processed
mastercactapus Sep 22, 2021
d299309
use single validation func
mastercactapus Sep 22, 2021
120b4e4
update validation func comment
mastercactapus Sep 22, 2021
fa679cd
don't require `type`
mastercactapus Sep 22, 2021
6c38638
swap field types for FromValue
mastercactapus Sep 22, 2021
0e27f2b
clarify default FromValueField mode
mastercactapus Sep 22, 2021
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
42 changes: 42 additions & 0 deletions devtools/mocktwilio/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -47,6 +48,7 @@ type Server struct {

messages map[string]*SMS
calls map[string]*VoiceCall
msgSvc map[string][]string

mux *http.ServeMux

Expand All @@ -71,6 +73,7 @@ func NewServer(cfg Config) *Server {
mux: http.NewServeMux(),
messages: make(map[string]*SMS),
calls: make(map[string]*VoiceCall),
msgSvc: make(map[string][]string),
smsCh: make(chan *SMS),
smsInCh: make(chan *SMS),
callCh: make(chan *VoiceCall),
Expand Down Expand Up @@ -196,6 +199,45 @@ func (s *Server) SetCarrierInfo(number string, info twilio.CarrierInfo) {
s.carrierInfo[number] = info
}

// getFromNumber will return a random number from the messaging service if ID is a
// messaging SID, or the value itself otherwise.
func (s *Server) getFromNumber(id string) string {
if !strings.HasPrefix(id, "MG") {
return id
}

s.mx.Lock()
defer s.mx.Unlock()

// select a random number from the message service
if len(s.msgSvc[id]) == 0 {
return ""
}

return s.msgSvc[id][rand.Intn(len(s.msgSvc[id]))]
}

// NewMessagingService registers a new Messaging SID for the given numbers.
func (s *Server) NewMessagingService(smsURL, voiceURL string, numbers ...string) (string, error) {
err := validate.Many(
validate.URL("SMS URL", smsURL),
validate.URL("Voice URL", voiceURL),
)
if err != nil {
return "", err
}
svcID := s.id("MG")

s.mx.Lock()
defer s.mx.Unlock()
for _, num := range numbers {
s.callbacks["SMS:"+num] = smsURL
s.callbacks["VOICE:"+num] = voiceURL
}

return svcID, nil
}

// RegisterSMSCallback will set/update a callback URL for SMS calls made to the given number.
func (s *Server) RegisterSMSCallback(number, url string) error {
err := validate.URL("URL", url)
Expand Down
21 changes: 18 additions & 3 deletions devtools/mocktwilio/sms.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type SMS struct {
doneCh chan struct{}
}

func (s *Server) sendSMS(from, to, body, statusURL, destURL string) (*SMS, error) {
func (s *Server) sendSMS(fromValue, to, body, statusURL, destURL string) (*SMS, error) {
fromNumber := s.getFromNumber(fromValue)
if statusURL != "" {
err := validate.URL("StatusCallback", statusURL)
if err != nil {
Expand All @@ -39,7 +40,7 @@ func (s *Server) sendSMS(from, to, body, statusURL, destURL string) (*SMS, error
}
}
s.mx.RLock()
_, hasCallback := s.callbacks["SMS:"+from]
_, hasCallback := s.callbacks["SMS:"+fromNumber]
s.mx.RUnlock()

if !hasCallback {
Expand All @@ -63,7 +64,6 @@ func (s *Server) sendSMS(from, to, body, statusURL, destURL string) (*SMS, error
s: s,
msg: twilio.Message{
To: to,
From: from,
Status: twilio.MessageStatusAccepted,
SID: s.id("SM"),
},
Expand All @@ -75,6 +75,12 @@ func (s *Server) sendSMS(from, to, body, statusURL, destURL string) (*SMS, error
doneCh: make(chan struct{}),
}

if strings.HasPrefix(fromValue, "MG") {
sms.msg.MessagingServiceSID = fromValue
} else {
sms.msg.From = fromValue
}

s.mx.Lock()
s.messages[sms.msg.SID] = sms
s.mx.Unlock()
Expand Down Expand Up @@ -130,6 +136,15 @@ func (s *Server) serveMessageStatus(w http.ResponseWriter, req *http.Request) {
func (sms *SMS) updateStatus(stat twilio.MessageStatus) {
sms.mx.Lock()
sms.msg.Status = stat
switch stat {
case twilio.MessageStatusAccepted, twilio.MessageStatusQueued:
default:
if sms.msg.MessagingServiceSID == "" {
break
}

sms.msg.From = sms.s.getFromNumber(sms.msg.MessagingServiceSID)
}
sms.mx.Unlock()

if sms.statusURL == "" {
Expand Down
28 changes: 23 additions & 5 deletions devtools/mocktwilio/voicecall.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,24 @@ func (s *Server) serveNewCall(w http.ResponseWriter, req *http.Request) {
hangupCh: make(chan struct{}),
}

vc.call.From = req.FormValue("From")
fromValue := req.FormValue("From")
fromNumber := s.getFromNumber(fromValue)
s.mx.RLock()
_, hasCallback := s.callbacks["VOICE:"+vc.call.From]
_, hasCallback := s.callbacks["VOICE:"+fromNumber]
s.mx.RUnlock()
if !hasCallback {
apiError(400, w, &twilio.Exception{
Message: "Wrong from number.",
})
return
}

if strings.HasPrefix(fromValue, "MG") {
vc.call.MessagingServiceSID = fromValue
} else {
vc.call.From = fromValue
}

vc.s = s
vc.call.To = req.FormValue("To")
vc.call.SID = s.id("CA")
Expand Down Expand Up @@ -211,10 +219,20 @@ func (vc *VoiceCall) updateStatus(stat twilio.CallStatus) {
// move to queued
vc.mx.Lock()
vc.call.Status = stat
if stat == twilio.CallStatusInProgress {
vc.callStart = time.Now()
switch stat {
case twilio.CallStatusQueued, twilio.CallStatusInitiated:
default:
if vc.call.MessagingServiceSID == "" {
break
}

vc.call.From = vc.s.getFromNumber(vc.call.MessagingServiceSID)
}
if stat == twilio.CallStatusCompleted {

switch stat {
case twilio.CallStatusInProgress:
vc.callStart = time.Now()
case twilio.CallStatusCompleted:
vc.call.CallDuration = time.Since(vc.callStart)
}
*vc.call.SequenceNumber++
Expand Down
4 changes: 2 additions & 2 deletions graphql2/graphqlapp/toolbox.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package graphqlapp

import (
context "context"
"context"
"fmt"
"net/url"

Expand All @@ -24,7 +24,7 @@ func (a *Mutation) DebugSendSms(ctx context.Context, input graphql2.DebugSendSMS

err = validate.Many(
validate.Phone("To", input.To),
validate.Phone("From", input.From),
validate.TwilioFromValue("From", input.From),
validate.Text("Body", input.Body, 1, 1000),
)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions notification/twilio/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type Call struct {
CallDuration time.Duration
ErrorMessage *string
ErrorCode *CallErrorCode

MessagingServiceSID string `json:"messaging_service_sid"`
}

func (call *Call) sentMessage() *notification.SentMessage {
Expand Down
2 changes: 2 additions & 0 deletions notification/twilio/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type Message struct {
Status MessageStatus
ErrorCode *MessageErrorCode
ErrorMessage *string

MessagingServiceSID string `json:"messaging_service_sid"`
}

func (msg *Message) sentMessage() *notification.SentMessage {
Expand Down
21 changes: 21 additions & 0 deletions smoketest/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ type Harness struct {
t *testing.T
closing bool

msgSvcID string

tw *twilioAssertionAPI
twS *httptest.Server

Expand Down Expand Up @@ -641,6 +643,25 @@ func (h *Harness) TwilioNumber(id string) string {
return num
}

// TwilioMessagingService will return the id and phone numbers for the mock messaging service.
func (h *Harness) TwilioMessagingService() string {
h.mx.Lock()
if h.msgSvcID != "" {
h.mx.Unlock()
return h.msgSvcID
}
defer h.mx.Unlock()

nums := []string{h.phoneCCG.Get(""), h.phoneCCG.Get(""), h.phoneCCG.Get("")}
newID, err := h.tw.NewMessagingService(h.URL()+"/v1/twilio/sms/messages", h.URL()+"/v1/twilio/voice/call", nums...)
if err != nil {
panic(err)
}

h.msgSvcID = newID
return newID
}

// CreateUser generates a random user.
func (h *Harness) CreateUser() (u *user.User) {
h.t.Helper()
Expand Down
19 changes: 19 additions & 0 deletions validation/validate/twiliofromvalue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package validate

import (
"strings"

"github.com/target/goalert/validation"
)

// TwlioFromValue will validate a from value as either a phone number, or messaging service SID starting with 'MG'.
func TwilioFromValue(fname, value string) error {
switch {
case strings.HasPrefix(value, "+"):
return Phone(fname, value)
case strings.HasPrefix(value, "MG"):
return ASCII(fname, value, 2, 64)
}

return validation.NewFieldError(fname, "must be a valid phone number or alphanumeric sender ID")
}
4 changes: 2 additions & 2 deletions web/src/app/admin/AdminFieldComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import VisibilityOff from '@material-ui/icons/VisibilityOff'
import TelTextField from '../util/TelTextField'

interface InputProps {
type: string
type?: string
name: string
value: string
password?: boolean
Expand All @@ -36,7 +36,7 @@ export function StringInput(props: InputProps): JSX.Element {
)
}

if (type === 'tel') {
if (props.name === 'Twilio.FromNumber') {
return <TelTextField onChange={(e) => onChange(e.target.value)} {...rest} />
}
return (
Expand Down
2 changes: 0 additions & 2 deletions web/src/app/admin/AdminNumberLookup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ export default function AdminNumberLookup(): JSX.Element {
}}
value={number}
label='Phone Number'
helperText='Please provide your country code e.g. +1 (USA)'
type='tel'
/>
</Grid>
</Grid>
Expand Down
18 changes: 7 additions & 11 deletions web/src/app/admin/AdminSMSSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import AppLink from '../util/AppLink'
import TelTextField from '../util/TelTextField'
import LoadingButton from '../loading/components/LoadingButton'
import DialogContentError from '../dialogs/components/DialogContentError'
import FromValueField from '../util/FromValueField'

const sendSMSMutation = gql`
mutation DebugSendSMS($input: DebugSendSMSInput!) {
Expand Down Expand Up @@ -65,13 +66,10 @@ export default function AdminSMSSend(): JSX.Element {
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} md={12} lg={6}>
<TelTextField
<FromValueField
onChange={(e) => setFromNumber(e.target.value)}
value={fromNumber}
fullWidth
label='From Number'
helperText='Please provide your country code e.g. +1 (USA)'
type='tel'
/>
</Grid>
<Grid item xs={12} sm={12} md={12} lg={6}>
Expand All @@ -80,8 +78,6 @@ export default function AdminSMSSend(): JSX.Element {
value={toNumber}
fullWidth
label='To Number'
helperText='Please provide your country code e.g. +1 (USA)'
type='tel'
/>
</Grid>
<Grid item xs={12}>
Expand All @@ -90,9 +86,6 @@ export default function AdminSMSSend(): JSX.Element {
value={body}
fullWidth
label='Body'
InputLabelProps={{
shrink: true,
}}
multiline
/>
</Grid>
Expand All @@ -112,8 +105,11 @@ export default function AdminSMSSend(): JSX.Element {
<AppLink to={sendStatus.data.debugSendSMS.providerURL} newTab>
<div className={classes.twilioLink}>
<Typography>
Sent from {sendStatus.data.debugSendSMS.fromNumber}. Open in
Twilio&nbsp;
{/* TODO: query for message status if from number / SID not immediately available */}
{sendStatus.data.debugSendSMS.fromNumber
? `Sent from ${sendStatus.data.debugSendSMS.fromNumber}. `
: ''}
Open in Twilio&nbsp;
</Typography>
<OpenInNewIcon fontSize='small' />
</div>
Expand Down
1 change: 0 additions & 1 deletion web/src/app/admin/AdminSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export default function AdminSection(props: AdminSectionProps): JSX.Element {
/>
<div className={classes.listItemAction}>
<Field
type={f.id === 'Twilio.FromNumber' ? 'tel' : 'text'}
name={f.id}
value={defaultTo(value[f.id], f.value)}
password={f.password}
Expand Down
9 changes: 2 additions & 7 deletions web/src/app/admin/AdminToolbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ const useStyles = makeStyles((theme) => ({
justifyContent: 'center',
},
},
gridItem: {
[theme.breakpoints.up('md')]: {
maxWidth: '65%',
},
},
groupTitle: {
fontSize: '1.1rem',
},
Expand All @@ -29,7 +24,7 @@ export default function AdminToolbox(): JSX.Element {

return (
<Grid container spacing={2} className={classes.gridContainer}>
<Grid container item xs={12} className={classes.gridItem}>
<Grid container item xs={12}>
<Grid item xs={12}>
<Typography
component='h2'
Expand All @@ -44,7 +39,7 @@ export default function AdminToolbox(): JSX.Element {
<AdminNumberLookup />
</Grid>
</Grid>
<Grid container item xs={12} className={classes.gridItem}>
<Grid container item xs={12}>
<Grid item xs={12}>
<Typography
component='h2'
Expand Down
Loading