Use Go templates, update docs

This commit is contained in:
binwiederhier
2026-01-17 04:59:46 -05:00
parent 6bacf7dafc
commit b23f6632b1
6 changed files with 69 additions and 43 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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) * 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),
([#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)
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) * 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)
for the initial implementation)
### ntfy Android app v1.22.x (UNRELEASED) ### ntfy Android app v1.22.x (UNRELEASED)

View File

@@ -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,

View File

@@ -15,14 +15,13 @@ import (
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
) )
const ( // defaultTwilioCallFormatTemplate is the default TwiML template used for Twilio calls.
// defaultTwilioCallFormat is the default TwiML format used for Twilio calls. // It can be overridden in the server configuration's twilio-call-format field.
// It can be overridden in the server configuration's twilio-call-format field. //
// // The format uses Go template syntax with the following fields:
// The format uses Go template syntax with the following fields: // {{.Topic}}, {{.Title}}, {{.Message}}, {{.Priority}}, {{.Tags}}, {{.Sender}}
// {{.Topic}}, {{.Title}}, {{.Message}}, {{.Priority}}, {{.Tags}}, {{.Sender}} // String fields are automatically XML-escaped.
// String fields are automatically XML-escaped. var defaultTwilioCallFormatTemplate = template.Must(template.New("twiml").Parse(`
defaultTwilioCallFormat = `
<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 tmpl := defaultTwilioCallFormatTemplate
if s.config.TwilioCallFormat != "" { if s.config.TwilioCallFormat != nil {
templateStr = s.config.TwilioCallFormat tmpl = 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
} }
tags := make([]string, len(m.Tags)) tags := make([]string, len(m.Tags))
for i, tag := range m.Tags { for i, tag := range m.Tags {

View File

@@ -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)