From b23f6632b1230ceeec9f13cb4141053da0a432c0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 04:59:46 -0500 Subject: [PATCH] Use Go templates, update docs --- cmd/serve.go | 9 +++++++- docs/config.md | 41 +++++++++++++++++++++++++++++------- docs/releases.md | 7 +++--- server/config.go | 5 +++-- server/server_twilio.go | 30 ++++++++++---------------- server/server_twilio_test.go | 20 ++++++++++-------- 6 files changed, 69 insertions(+), 43 deletions(-) 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)