diff --git a/cmd/serve.go b/cmd/serve.go
index e4f432d2..4d2803d5 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -14,6 +14,7 @@ import (
"os/signal"
"strings"
"syscall"
+ "text/template"
"time"
"github.com/urfave/cli/v2"
@@ -458,7 +459,13 @@ func execServe(c *cli.Context) error {
conf.TwilioAuthToken = twilioAuthToken
conf.TwilioPhoneNumber = twilioPhoneNumber
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.MessageDelayMax = messageDelayLimit
conf.TotalTopicLimit = totalTopicLimit
diff --git a/docs/config.md b/docs/config.md
index 0b961b1f..8a125146 100644
--- a/docs/config.md
+++ b/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-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` 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 ...`),
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:
* `{{.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:
-=== English example
+=== "Custom TwiML (English)"
+ ``` yaml
+ twilio-account: "AC12345beefbeef67890beefbeef122586"
+ twilio-auth-token: "affebeef258625862586258625862586"
+ twilio-phone-number: "+18775132586"
+ twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
+ twilio-call-format: |
+
+
+
+ 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 }}
+
+ {{ if neq .Title "" }}
+ Bro, it's titled: {{.Title}}.
+ {{ end }}
+
+ {{.Message}}
+
+ That is all.
+
+ You know who this message is from? It is from {{.Sender}}.
+
+
+ See ya!
+
+ ```
+
+=== "Custom TwiML (German)"
``` yaml
twilio-account: "AC12345beefbeef67890beefbeef122586"
twilio-auth-token: "affebeef258625862586258625862586"
@@ -1310,11 +1340,6 @@ Here's an example:
```
-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:
diff --git a/docs/releases.md b/docs/releases.md
index 2f3f669e..4c950b3b 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1603,10 +1603,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**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)
diff --git a/server/config.go b/server/config.go
index 927c6b5a..c4c76bd1 100644
--- a/server/config.go
+++ b/server/config.go
@@ -3,6 +3,7 @@ package server
import (
"io/fs"
"net/netip"
+ "text/template"
"time"
"heckel.io/ntfy/v2/user"
@@ -128,7 +129,7 @@ type Config struct {
TwilioCallsBaseURL string
TwilioVerifyBaseURL string
TwilioVerifyService string
- TwilioCallFormat string
+ TwilioCallFormat *template.Template
MetricsEnable bool
MetricsListenHTTP string
ProfileListenHTTP string
@@ -227,7 +228,7 @@ func NewConfig() *Config {
TwilioPhoneNumber: "",
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
TwilioVerifyService: "",
- TwilioCallFormat: "",
+ TwilioCallFormat: nil,
MessageSizeLimit: DefaultMessageSizeLimit,
MessageDelayMin: DefaultMessageDelayMin,
MessageDelayMax: DefaultMessageDelayMax,
diff --git a/server/server_twilio.go b/server/server_twilio.go
index 0c5694d8..6a613d49 100644
--- a/server/server_twilio.go
+++ b/server/server_twilio.go
@@ -15,14 +15,13 @@ import (
"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(`
@@ -37,8 +36,7 @@ const (
Goodbye.
-`
-)
+`))
// twilioCallData holds the data passed to the Twilio call format template
type twilioCallData struct {
@@ -83,15 +81,9 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
if u != nil {
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))
for i, tag := range m.Tags {
diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go
index c1418de1..9b6dcff5 100644
--- a/server/server_twilio_test.go
+++ b/server/server_twilio_test.go
@@ -1,14 +1,16 @@
package server
import (
- "github.com/stretchr/testify/require"
- "heckel.io/ntfy/v2/user"
- "heckel.io/ntfy/v2/util"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"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) {
@@ -222,22 +224,22 @@ func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
c.TwilioAccount = "AC1234567890"
c.TwilioAuthToken = "AAEAA1234567890"
c.TwilioPhoneNumber = "+1234567890"
- c.TwilioCallFormat = `
+ c.TwilioCallFormat = template.Must(template.New("twiml").Parse(`
- Du hast eine Nachricht von notify im Thema %s. Nachricht:
+ Du hast eine Nachricht von notify im Thema {{.Topic}}. Nachricht:
- %s
+ {{.Message}}
Ende der Nachricht.
- 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.
Auf Wiederhören.
-`
+`))
s := newTestServer(t, c)
// Add tier and user
@@ -246,7 +248,7 @@ func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
MessageLimit: 10,
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"))
u, err := s.userManager.User("phil")
require.Nil(t, err)