binwiederhier 1 месяц назад
Родитель
Сommit
b23f6632b1
6 измененных файлов с 69 добавлено и 43 удалено
  1. 8 1
      cmd/serve.go
  2. 33 8
      docs/config.md
  3. 3 4
      docs/releases.md
  4. 3 2
      server/config.go
  5. 11 19
      server/server_twilio.go
  6. 11 9
      server/server_twilio_test.go

+ 8 - 1
cmd/serve.go

@@ -14,6 +14,7 @@ import (
 	"os/signal"
 	"os/signal"
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
+	"text/template"
 	"time"
 	"time"
 
 
 	"github.com/urfave/cli/v2"
 	"github.com/urfave/cli/v2"
@@ -458,7 +459,13 @@ func execServe(c *cli.Context) error {
 	conf.TwilioAuthToken = twilioAuthToken
 	conf.TwilioAuthToken = twilioAuthToken
 	conf.TwilioPhoneNumber = twilioPhoneNumber
 	conf.TwilioPhoneNumber = twilioPhoneNumber
 	conf.TwilioVerifyService = twilioVerifyService
 	conf.TwilioVerifyService = twilioVerifyService
-	conf.TwilioCallFormat = twilioCallFormat
+	if twilioCallFormat != "" {
+		tmpl, err := template.New("twiml").Parse(twilioCallFormat)
+		if err != nil {
+			return fmt.Errorf("failed to parse twilio-call-format template: %w", err)
+		}
+		conf.TwilioCallFormat = tmpl
+	}
 	conf.MessageSizeLimit = int(messageSizeLimit)
 	conf.MessageSizeLimit = int(messageSizeLimit)
 	conf.MessageDelayMax = messageDelayLimit
 	conf.MessageDelayMax = messageDelayLimit
 	conf.TotalTopicLimit = totalTopicLimit
 	conf.TotalTopicLimit = totalTopicLimit

+ 33 - 8
docs/config.md

@@ -1261,12 +1261,12 @@ are the easiest), and then configure the following options:
 * `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
 * `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-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-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
-* `twilio-call-format` is the custom TwiML send to the Call API (optional, see [TwiML](https://www.twilio.com/docs/voice/twiml))
+* `twilio-call-format` is the custom Twilio markup ([TwiML](https://www.twilio.com/docs/voice/twiml)) to use for phone calls (optional)
 
 
 After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
 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.
 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). The format is
+To customize the message that is spoken out loud, set the `twilio-call-format` option with [TwiML](https://www.twilio.com/docs/voice/twiml). The format is
 rendered as a [Go template](https://pkg.go.dev/text/template), so you can use the following fields from the message:
 rendered as a [Go template](https://pkg.go.dev/text/template), so you can use the following fields from the message:
 
 
 * `{{.Topic}}` is the topic name
 * `{{.Topic}}` is the topic name
@@ -1278,7 +1278,37 @@ rendered as a [Go template](https://pkg.go.dev/text/template), so you can use th
 
 
 Here's an example:
 Here's an example:
 
 
-=== English example
+=== "Custom TwiML (English)"
+    ``` yaml
+    twilio-account: "AC12345beefbeef67890beefbeef122586"
+    twilio-auth-token: "affebeef258625862586258625862586"
+    twilio-phone-number: "+18775132586"
+    twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
+    twilio-call-format: |
+      <Response>
+        <Pause length="1"/>
+        <Say loop="3">
+          Yo yo yo, you should totally check out this message for {{.Topic}}.
+          {{ if eq .Priority 5 }}
+            It's really really important, dude. So listen up!
+          {{ end }}
+          <break time="1s"/>
+          {{ if neq .Title "" }}
+            Bro, it's titled: {{.Title}}.
+          {{ end }}
+          <break time="1s"/>
+          {{.Message}}
+          <break time="1s"/>
+          That is all.
+          <break time="1s"/>
+          You know who this message is from? It is from {{.Sender}}.
+          <break time="3s"/>
+        </Say>
+        <Say>See ya!</Say>
+      </Response>
+    ```
+
+=== "Custom TwiML (German)"
     ``` yaml
     ``` yaml
     twilio-account: "AC12345beefbeef67890beefbeef122586"
     twilio-account: "AC12345beefbeef67890beefbeef122586"
     twilio-auth-token: "affebeef258625862586258625862586"
     twilio-auth-token: "affebeef258625862586258625862586"
@@ -1310,11 +1340,6 @@ Here's an example:
       </Response>
       </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
 ## Message limits
 There are a few message limits that you can configure:
 There are a few message limits that you can configure:
 
 

+ 3 - 4
docs/releases.md

@@ -1603,10 +1603,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 
 **Features:**
 **Features:**
 
 
-* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) 
-  ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
-  [ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8)
-  for the initial implementation)
+* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
+  [ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
+* Support for a [custom Twilio call format](config.md#phone-calls) ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation) 
 
 
 ### ntfy Android app v1.22.x (UNRELEASED)
 ### ntfy Android app v1.22.x (UNRELEASED)
 
 

+ 3 - 2
server/config.go

@@ -3,6 +3,7 @@ package server
 import (
 import (
 	"io/fs"
 	"io/fs"
 	"net/netip"
 	"net/netip"
+	"text/template"
 	"time"
 	"time"
 
 
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/user"
@@ -128,7 +129,7 @@ type Config struct {
 	TwilioCallsBaseURL                   string
 	TwilioCallsBaseURL                   string
 	TwilioVerifyBaseURL                  string
 	TwilioVerifyBaseURL                  string
 	TwilioVerifyService                  string
 	TwilioVerifyService                  string
-	TwilioCallFormat                     string
+	TwilioCallFormat                     *template.Template
 	MetricsEnable                        bool
 	MetricsEnable                        bool
 	MetricsListenHTTP                    string
 	MetricsListenHTTP                    string
 	ProfileListenHTTP                    string
 	ProfileListenHTTP                    string
@@ -227,7 +228,7 @@ func NewConfig() *Config {
 		TwilioPhoneNumber:                    "",
 		TwilioPhoneNumber:                    "",
 		TwilioVerifyBaseURL:                  "https://verify.twilio.com", // Override for tests
 		TwilioVerifyBaseURL:                  "https://verify.twilio.com", // Override for tests
 		TwilioVerifyService:                  "",
 		TwilioVerifyService:                  "",
-		TwilioCallFormat:                     "",
+		TwilioCallFormat:                     nil,
 		MessageSizeLimit:                     DefaultMessageSizeLimit,
 		MessageSizeLimit:                     DefaultMessageSizeLimit,
 		MessageDelayMin:                      DefaultMessageDelayMin,
 		MessageDelayMin:                      DefaultMessageDelayMin,
 		MessageDelayMax:                      DefaultMessageDelayMax,
 		MessageDelayMax:                      DefaultMessageDelayMax,

+ 11 - 19
server/server_twilio.go

@@ -15,14 +15,13 @@ import (
 	"heckel.io/ntfy/v2/util"
 	"heckel.io/ntfy/v2/util"
 )
 )
 
 
-const (
-	// defaultTwilioCallFormat is the default TwiML format used for Twilio calls.
-	// It can be overridden in the server configuration's twilio-call-format field.
-	//
-	// The format uses Go template syntax with the following fields:
-	// {{.Topic}}, {{.Title}}, {{.Message}}, {{.Priority}}, {{.Tags}}, {{.Sender}}
-	// String fields are automatically XML-escaped.
-	defaultTwilioCallFormat = `
+// defaultTwilioCallFormatTemplate is the default TwiML template used for Twilio calls.
+// It can be overridden in the server configuration's twilio-call-format field.
+//
+// The format uses Go template syntax with the following fields:
+// {{.Topic}}, {{.Title}}, {{.Message}}, {{.Priority}}, {{.Tags}}, {{.Sender}}
+// String fields are automatically XML-escaped.
+var defaultTwilioCallFormatTemplate = template.Must(template.New("twiml").Parse(`
 <Response>
 <Response>
 	<Pause length="1"/>
 	<Pause length="1"/>
 	<Say loop="3">
 	<Say loop="3">
@@ -37,8 +36,7 @@ const (
 		<break time="3s"/>
 		<break time="3s"/>
 	</Say>
 	</Say>
 	<Say>Goodbye.</Say>
 	<Say>Goodbye.</Say>
-</Response>`
-)
+</Response>`))
 
 
 // twilioCallData holds the data passed to the Twilio call format template
 // twilioCallData holds the data passed to the Twilio call format template
 type twilioCallData struct {
 type twilioCallData struct {
@@ -83,15 +81,9 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
 	if u != nil {
 	if u != nil {
 		sender = u.Name
 		sender = u.Name
 	}
 	}
-	templateStr := defaultTwilioCallFormat
-	if s.config.TwilioCallFormat != "" {
-		templateStr = s.config.TwilioCallFormat
-	}
-	tmpl, err := template.New("twiml").Parse(templateStr)
-	if err != nil {
-		logvrm(v, r, m).Tag(tagTwilio).Err(err).Warn("Error parsing Twilio call format template")
-		minc(metricCallsMadeFailure)
-		return
+	tmpl := defaultTwilioCallFormatTemplate
+	if s.config.TwilioCallFormat != nil {
+		tmpl = s.config.TwilioCallFormat
 	}
 	}
 	tags := make([]string, len(m.Tags))
 	tags := make([]string, len(m.Tags))
 	for i, tag := range m.Tags {
 	for i, tag := range m.Tags {

+ 11 - 9
server/server_twilio_test.go

@@ -1,14 +1,16 @@
 package server
 package server
 
 
 import (
 import (
-	"github.com/stretchr/testify/require"
-	"heckel.io/ntfy/v2/user"
-	"heckel.io/ntfy/v2/util"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
 	"sync/atomic"
 	"sync/atomic"
 	"testing"
 	"testing"
+	"text/template"
+
+	"github.com/stretchr/testify/require"
+	"heckel.io/ntfy/v2/user"
+	"heckel.io/ntfy/v2/util"
 )
 )
 
 
 func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
 func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
@@ -222,22 +224,22 @@ func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioPhoneNumber = "+1234567890"
 	c.TwilioPhoneNumber = "+1234567890"
-	c.TwilioCallFormat = `
+	c.TwilioCallFormat = template.Must(template.New("twiml").Parse(`
 <Response>
 <Response>
 	<Pause length="1"/>
 	<Pause length="1"/>
 	<Say language="de-DE" loop="3">
 	<Say language="de-DE" loop="3">
-		Du hast eine Nachricht von notify im Thema %s. Nachricht:
+		Du hast eine Nachricht von notify im Thema {{.Topic}}. Nachricht:
 		<break time="1s"/>
 		<break time="1s"/>
-		%s
+		{{.Message}}
 		<break time="1s"/>
 		<break time="1s"/>
 		Ende der Nachricht.
 		Ende der Nachricht.
 		<break time="1s"/>
 		<break time="1s"/>
-		Diese Nachricht wurde von Benutzer %s gesendet. Sie wird drei Mal wiederholt.
+		Diese Nachricht wurde von Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
 		Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
 		Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
 		<break time="3s"/>
 		<break time="3s"/>
 	</Say>
 	</Say>
 	<Say language="de-DE">Auf Wiederhören.</Say>
 	<Say language="de-DE">Auf Wiederhören.</Say>
-</Response>`
+</Response>`))
 	s := newTestServer(t, c)
 	s := newTestServer(t, c)
 
 
 	// Add tier and user
 	// Add tier and user
@@ -246,7 +248,7 @@ func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
 		MessageLimit: 10,
 		MessageLimit: 10,
 		CallLimit:    1,
 		CallLimit:    1,
 	}))
 	}))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
 	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
 	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
 	u, err := s.userManager.User("phil")
 	u, err := s.userManager.User("phil")
 	require.Nil(t, err)
 	require.Nil(t, err)