Просмотр исходного кода

Add optional twilio-call-format config option

To be able to set custom TwiML send to the Call API.
Michael Nowak 11 месяцев назад
Родитель
Сommit
950ba1e2e1
5 измененных файлов с 96 добавлено и 1 удалено
  1. 26 0
      docs/config.md
  2. 2 0
      server/config.go
  3. 2 0
      server/server.yml
  4. 5 1
      server/server_twilio.go
  5. 61 0
      server/server_twilio_test.go

+ 26 - 0
docs/config.md

@@ -996,6 +996,32 @@ are the easiest), and then configure the following options:
 After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
 and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
 
+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).
+This is the default TwiML:
+
+``` xml
+<Response>
+	<Pause length="1"/>
+	<Say loop="3">
+		You have a message from notify on topic %s. Message:
+		<break time="1s"/>
+		%s
+		<break time="1s"/>
+		End of message.
+		<break time="1s"/>
+		This message was sent by user %s. It will be repeated three times.
+		To unsubscribe from calls like this, remove your phone number in the notify web app.
+		<break time="3s"/>
+	</Say>
+	<Say>Goodbye.</Say>
+</Response>`
+```
+
+The TwiML is internaly used as a format string:
+1. The first `%s` will be replaced with the topic.
+1. The second `%s` will be replaced with the message.
+1. The third `%s` will be replaced with the message`s sender name.
+
 ## Message limits
 There are a few message limits that you can configure:
 

+ 2 - 0
server/config.go

@@ -120,6 +120,7 @@ type Config struct {
 	TwilioCallsBaseURL                   string
 	TwilioVerifyBaseURL                  string
 	TwilioVerifyService                  string
+	TwilioCallFormat                     string
 	MetricsEnable                        bool
 	MetricsListenHTTP                    string
 	ProfileListenHTTP                    string
@@ -212,6 +213,7 @@ func NewConfig() *Config {
 		TwilioPhoneNumber:                    "",
 		TwilioVerifyBaseURL:                  "https://verify.twilio.com", // Override for tests
 		TwilioVerifyService:                  "",
+		TwilioCallFormat:                     "",
 		MessageSizeLimit:                     DefaultMessageSizeLimit,
 		MessageDelayMin:                      DefaultMessageDelayMin,
 		MessageDelayMax:                      DefaultMessageDelayMax,

+ 2 - 0
server/server.yml

@@ -171,11 +171,13 @@
 # - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
 # - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
 # - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
+# - twilio-call-format (optional) is the custom TwiML send to the Call API. See: https://www.twilio.com/docs/voice/twiml
 #
 # twilio-account:
 # twilio-auth-token:
 # twilio-phone-number:
 # twilio-verify-service:
+# twilio-call-format:
 
 # Interval in which keepalive messages are sent to the client. This is to prevent
 # intermediaries closing the connection for inactivity.

+ 5 - 1
server/server_twilio.go

@@ -14,7 +14,7 @@ import (
 )
 
 const (
-	twilioCallFormat = `
+	defaultTwilioCallFormat = `
 <Response>
 	<Pause length="1"/>
 	<Say loop="3">
@@ -65,6 +65,10 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
 	if u != nil {
 		sender = u.Name
 	}
+	twilioCallFormat := defaultTwilioCallFormat
+	if len(s.config.TwilioCallFormat) > 0 {
+		twilioCallFormat = s.config.TwilioCallFormat
+	}
 	body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(sender))
 	data := url.Values{}
 	data.Set("From", s.config.TwilioPhoneNumber)

+ 61 - 0
server/server_twilio_test.go

@@ -202,6 +202,67 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
 	})
 }
 
+func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
+	var called atomic.Bool
+	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if called.Load() {
+			t.Fatal("Should be only called once")
+		}
+		body, err := io.ReadAll(r.Body)
+		require.Nil(t, err)
+		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
+		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
+		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))
+		called.Store(true)
+	}))
+	defer twilioServer.Close()
+
+	c := newTestConfigWithAuthFile(t)
+	c.TwilioCallsBaseURL = twilioServer.URL
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioPhoneNumber = "+1234567890"
+	c.TwilioCallFormat = `
+<Response>
+	<Pause length="1"/>
+	<Say language="de-DE" loop="3">
+		Du hast eine Nachricht von notify im Thema %s. Nachricht:
+		<break time="1s"/>
+		%s
+		<break time="1s"/>
+		Ende der Nachricht.
+		<break time="1s"/>
+		Diese Nachricht wurde von Benutzer %s gesendet. Sie wird drei Mal wiederholt.
+		Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
+		<break time="3s"/>
+	</Say>
+	<Say language="de-DE">Auf Wiederhören.</Say>
+</Response>`
+	s := newTestServer(t, c)
+
+	// Add tier and user
+	require.Nil(t, s.userManager.AddTier(&user.Tier{
+		Code:         "pro",
+		MessageLimit: 10,
+		CallLimit:    1,
+	}))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
+	u, err := s.userManager.User("phil")
+	require.Nil(t, err)
+	require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
+
+	// Do the thing
+	response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
+		"x-call":        "+11122233344",
+	})
+	require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
+	waitFor(t, func() bool {
+		return called.Load()
+	})
+}
+
 func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
 	c := newTestConfigWithAuthFile(t)
 	c.TwilioCallsBaseURL = "http://dummy.invalid"