ntfy-server/server/server_twilio.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

215 lines
7.2 KiB
Go
Raw Normal View History

2023-05-05 16:22:54 -04:00
package server
import (
2023-05-05 20:14:46 -04:00
"bytes"
"encoding/xml"
2023-05-05 16:22:54 -04:00
"fmt"
"io"
"net/http"
"net/url"
"strings"
2026-01-17 04:34:32 -05:00
"text/template"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
2023-05-05 16:22:54 -04:00
)
2026-01-17 04:59:46 -05:00
// 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(`
2023-05-05 16:22:54 -04:00
<Response>
<Pause length="1"/>
2023-05-16 14:15:58 -04:00
<Say loop="3">
2026-01-17 04:34:32 -05:00
You have a message from notify on topic {{.Topic}}. Message:
2023-05-15 22:06:43 -04:00
<break time="1s"/>
2026-01-17 04:34:32 -05:00
{{.Message}}
2023-05-15 22:06:43 -04:00
<break time="1s"/>
2023-05-16 22:27:48 -04:00
End of message.
2023-05-15 22:06:43 -04:00
<break time="1s"/>
2026-01-17 04:34:32 -05:00
This message was sent by user {{.Sender}}. It will be repeated three times.
2023-05-16 22:27:48 -04:00
To unsubscribe from calls like this, remove your phone number in the notify web app.
2023-05-15 22:06:43 -04:00
<break time="3s"/>
</Say>
<Say>Goodbye.</Say>
2026-01-17 04:59:46 -05:00
</Response>`))
2023-05-05 16:22:54 -04:00
2026-01-17 04:34:32 -05:00
// twilioCallData holds the data passed to the Twilio call format template
type twilioCallData struct {
Topic string
Title string
Message string
Priority int
Tags []string
Sender string
}
2023-05-16 14:15:58 -04:00
// convertPhoneNumber checks if the given phone number is verified for the given user, and if so, returns the verified
// phone number. It also converts a boolean string ("yes", "1", "true") to the first verified phone number.
// If the user is anonymous, it will return an error.
func (s *Server) convertPhoneNumber(u *user.User, phoneNumber string) (string, *errHTTP) {
2023-05-13 12:26:14 -04:00
if u == nil {
return "", errHTTPBadRequestAnonymousCallsNotAllowed
2023-05-13 12:26:14 -04:00
}
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
if err != nil {
return "", errHTTPInternalError
} else if len(phoneNumbers) == 0 {
return "", errHTTPBadRequestPhoneNumberNotVerified
}
if toBool(phoneNumber) {
return phoneNumbers[0], nil
} else if util.Contains(phoneNumbers, phoneNumber) {
2023-05-13 12:26:14 -04:00
return phoneNumber, nil
}
for _, p := range phoneNumbers {
if p == phoneNumber {
return phoneNumber, nil
}
}
return "", errHTTPBadRequestPhoneNumberNotVerified
2023-05-13 12:26:14 -04:00
}
2023-05-17 11:19:48 -04:00
// callPhone calls the Twilio API to make a phone call to the given phone number, using the given message.
// Failures will be logged, but not returned to the caller.
2023-05-05 16:22:54 -04:00
func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
2023-05-15 22:06:43 -04:00
u, sender := v.User(), m.Sender.String()
if u != nil {
sender = u.Name
}
2026-01-17 04:59:46 -05:00
tmpl := defaultTwilioCallFormatTemplate
if s.config.TwilioCallFormat != nil {
tmpl = s.config.TwilioCallFormat
2026-01-17 04:34:32 -05:00
}
tags := make([]string, len(m.Tags))
for i, tag := range m.Tags {
tags[i] = xmlEscapeText(tag)
}
templateData := &twilioCallData{
Topic: xmlEscapeText(m.Topic),
Title: xmlEscapeText(m.Title),
Message: xmlEscapeText(m.Message),
Priority: m.Priority,
Tags: tags,
Sender: xmlEscapeText(sender),
}
var bodyBuf bytes.Buffer
if err := tmpl.Execute(&bodyBuf, templateData); err != nil {
logvrm(v, r, m).Tag(tagTwilio).Err(err).Warn("Error executing Twilio call format template")
minc(metricCallsMadeFailure)
return
}
2026-01-17 04:34:32 -05:00
body := bodyBuf.String()
2023-05-05 16:22:54 -04:00
data := url.Values{}
data.Set("From", s.config.TwilioPhoneNumber)
2023-05-05 16:22:54 -04:00
data.Set("To", to)
data.Set("Twiml", body)
2023-05-16 14:15:58 -04:00
ev := logvrm(v, r, m).Tag(tagTwilio).Field("twilio_to", to).FieldIf("twilio_body", body, log.TraceLevel).Debug("Sending Twilio request")
response, err := s.callPhoneInternal(data)
if err != nil {
ev.Field("twilio_response", response).Err(err).Warn("Error sending Twilio request")
minc(metricCallsMadeFailure)
return
}
ev.FieldIf("twilio_response", response, log.TraceLevel).Debug("Received successful Twilio response")
minc(metricCallsMadeSuccess)
2023-05-05 16:22:54 -04:00
}
2023-05-16 14:15:58 -04:00
func (s *Server) callPhoneInternal(data url.Values) (string, error) {
requestURL := fmt.Sprintf("%s/2010-04-01/Accounts/%s/Calls.json", s.config.TwilioCallsBaseURL, s.config.TwilioAccount)
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
2026-01-18 10:46:15 -05:00
req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
2023-05-16 14:15:58 -04:00
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
2023-05-18 13:08:10 -04:00
req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
2023-05-16 14:15:58 -04:00
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
response, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(response), nil
}
2023-05-16 22:27:48 -04:00
func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error {
ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification")
2023-05-11 13:50:10 -04:00
data := url.Values{}
data.Set("To", phoneNumber)
2023-05-16 22:27:48 -04:00
data.Set("Channel", channel)
2023-05-11 13:50:10 -04:00
requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService)
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
2026-01-18 10:46:15 -05:00
req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
2023-05-11 13:50:10 -04:00
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
2023-05-18 13:08:10 -04:00
req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
2023-05-11 13:50:10 -04:00
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
response, err := io.ReadAll(resp.Body)
if err != nil {
ev.Err(err).Warn("Error sending Twilio phone verification request")
return err
}
2023-05-16 14:15:58 -04:00
ev.FieldIf("twilio_response", string(response), log.TraceLevel).Debug("Received Twilio phone verification response")
2023-05-11 13:50:10 -04:00
return nil
}
2023-05-16 14:15:58 -04:00
func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, phoneNumber, code string) error {
ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Checking phone verification")
2023-05-11 13:50:10 -04:00
data := url.Values{}
data.Set("To", phoneNumber)
data.Set("Code", code)
2023-05-12 20:01:12 -04:00
requestURL := fmt.Sprintf("%s/v2/Services/%s/VerificationCheck", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService)
2023-05-11 13:50:10 -04:00
req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
2026-01-18 10:46:15 -05:00
req.Header.Set("User-Agent", "ntfy/"+s.config.BuildVersion)
2023-05-11 13:50:10 -04:00
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
2023-05-18 13:08:10 -04:00
req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
2023-05-11 13:50:10 -04:00
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
2023-05-12 20:01:12 -04:00
if ev.IsTrace() {
response, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
ev.Field("twilio_response", string(response))
}
ev.Warn("Twilio phone verification failed with status code %d", resp.StatusCode)
if resp.StatusCode == http.StatusNotFound {
return errHTTPGonePhoneVerificationExpired
}
return errHTTPInternalError
2023-05-11 13:50:10 -04:00
}
response, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if ev.IsTrace() {
ev.Field("twilio_response", string(response)).Trace("Received successful Twilio phone verification response")
} else if ev.IsDebug() {
ev.Debug("Received successful Twilio phone verification response")
}
return nil
}
2023-05-05 20:14:46 -04:00
func xmlEscapeText(text string) string {
var buf bytes.Buffer
_ = xml.EscapeText(&buf, []byte(text))
return buf.String()
2023-05-05 16:22:54 -04:00
}