Skip to content

Commit 950ba1e

Browse files
committed
Add optional twilio-call-format config option
To be able to set custom TwiML send to the Call API.
1 parent 630f295 commit 950ba1e

File tree

5 files changed

+96
-1
lines changed

5 files changed

+96
-1
lines changed

docs/config.md

+26
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,32 @@ are the easiest), and then configure the following options:
996996
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
997997
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
998998

999+
To customize your message send to Twilio's Call API, set the `twilio-call-format` option with [TwiML](https://www.twilio.com/docs/voice/twiml).
1000+
This is the default TwiML:
1001+
1002+
``` xml
1003+
<Response>
1004+
<Pause length="1"/>
1005+
<Say loop="3">
1006+
You have a message from notify on topic %s. Message:
1007+
<break time="1s"/>
1008+
%s
1009+
<break time="1s"/>
1010+
End of message.
1011+
<break time="1s"/>
1012+
This message was sent by user %s. It will be repeated three times.
1013+
To unsubscribe from calls like this, remove your phone number in the notify web app.
1014+
<break time="3s"/>
1015+
</Say>
1016+
<Say>Goodbye.</Say>
1017+
</Response>`
1018+
```
1019+
1020+
The TwiML is internaly used as a format string:
1021+
1. The first `%s` will be replaced with the topic.
1022+
1. The second `%s` will be replaced with the message.
1023+
1. The third `%s` will be replaced with the message`s sender name.
1024+
9991025
## Message limits
10001026
There are a few message limits that you can configure:
10011027

server/config.go

+2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type Config struct {
120120
TwilioCallsBaseURL string
121121
TwilioVerifyBaseURL string
122122
TwilioVerifyService string
123+
TwilioCallFormat string
123124
MetricsEnable bool
124125
MetricsListenHTTP string
125126
ProfileListenHTTP string
@@ -212,6 +213,7 @@ func NewConfig() *Config {
212213
TwilioPhoneNumber: "",
213214
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
214215
TwilioVerifyService: "",
216+
TwilioCallFormat: "",
215217
MessageSizeLimit: DefaultMessageSizeLimit,
216218
MessageDelayMin: DefaultMessageDelayMin,
217219
MessageDelayMax: DefaultMessageDelayMax,

server/server.yml

+2
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,13 @@
171171
# - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
172172
# - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
173173
# - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
174+
# - twilio-call-format (optional) is the custom TwiML send to the Call API. See: https://www.twilio.com/docs/voice/twiml
174175
#
175176
# twilio-account:
176177
# twilio-auth-token:
177178
# twilio-phone-number:
178179
# twilio-verify-service:
180+
# twilio-call-format:
179181

180182
# Interval in which keepalive messages are sent to the client. This is to prevent
181183
# intermediaries closing the connection for inactivity.

server/server_twilio.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
const (
17-
twilioCallFormat = `
17+
defaultTwilioCallFormat = `
1818
<Response>
1919
<Pause length="1"/>
2020
<Say loop="3">
@@ -65,6 +65,10 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
6565
if u != nil {
6666
sender = u.Name
6767
}
68+
twilioCallFormat := defaultTwilioCallFormat
69+
if len(s.config.TwilioCallFormat) > 0 {
70+
twilioCallFormat = s.config.TwilioCallFormat
71+
}
6872
body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(sender))
6973
data := url.Values{}
7074
data.Set("From", s.config.TwilioPhoneNumber)

server/server_twilio_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,67 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
202202
})
203203
}
204204

205+
func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
206+
var called atomic.Bool
207+
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
208+
if called.Load() {
209+
t.Fatal("Should be only called once")
210+
}
211+
body, err := io.ReadAll(r.Body)
212+
require.Nil(t, err)
213+
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
214+
require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
215+
require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+language%3D%22de-DE%22+loop%3D%223%22%3E%0A%09%09Du+hast+eine+Nachricht+von+notify+im+Thema+mytopic.+Nachricht%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Ende+der+Nachricht.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Diese+Nachricht+wurde+von+Benutzer+phil+gesendet.+Sie+wird+drei+Mal+wiederholt.%0A%09%09Um+dich+von+Anrufen+wie+diesen+abzumelden%2C+entferne+deine+Telefonnummer+in+der+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay+language%3D%22de-DE%22%3EAuf+Wiederh%C3%B6ren.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
216+
called.Store(true)
217+
}))
218+
defer twilioServer.Close()
219+
220+
c := newTestConfigWithAuthFile(t)
221+
c.TwilioCallsBaseURL = twilioServer.URL
222+
c.TwilioAccount = "AC1234567890"
223+
c.TwilioAuthToken = "AAEAA1234567890"
224+
c.TwilioPhoneNumber = "+1234567890"
225+
c.TwilioCallFormat = `
226+
<Response>
227+
<Pause length="1"/>
228+
<Say language="de-DE" loop="3">
229+
Du hast eine Nachricht von notify im Thema %s. Nachricht:
230+
<break time="1s"/>
231+
%s
232+
<break time="1s"/>
233+
Ende der Nachricht.
234+
<break time="1s"/>
235+
Diese Nachricht wurde von Benutzer %s gesendet. Sie wird drei Mal wiederholt.
236+
Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
237+
<break time="3s"/>
238+
</Say>
239+
<Say language="de-DE">Auf Wiederhören.</Say>
240+
</Response>`
241+
s := newTestServer(t, c)
242+
243+
// Add tier and user
244+
require.Nil(t, s.userManager.AddTier(&user.Tier{
245+
Code: "pro",
246+
MessageLimit: 10,
247+
CallLimit: 1,
248+
}))
249+
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
250+
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
251+
u, err := s.userManager.User("phil")
252+
require.Nil(t, err)
253+
require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
254+
255+
// Do the thing
256+
response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
257+
"authorization": util.BasicAuth("phil", "phil"),
258+
"x-call": "+11122233344",
259+
})
260+
require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
261+
waitFor(t, func() bool {
262+
return called.Load()
263+
})
264+
}
265+
205266
func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
206267
c := newTestConfigWithAuthFile(t)
207268
c.TwilioCallsBaseURL = "http://dummy.invalid"

0 commit comments

Comments
 (0)