binwiederhier 2 лет назад
Родитель
Сommit
7c574d73de

+ 3 - 3
cmd/serve.go

@@ -73,7 +73,7 @@ var flagsServe = append(
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
-	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-from-number", Aliases: []string{"twilio_from_number"}, EnvVars: []string{"NTFY_TWILIO_FROM_NUMBER"}, Usage: "Twilio number to use for outgoing calls and text messages"}),
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-from-number", Aliases: []string{"twilio_from_number"}, EnvVars: []string{"NTFY_TWILIO_FROM_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
 	altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
 	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
@@ -217,8 +217,8 @@ func execServe(c *cli.Context) error {
 		return errors.New("cannot set enable-signup without also setting enable-login")
 	} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
 		return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
-	} else if twilioAccount != "" && (twilioAuthToken == "" || twilioFromNumber == "" || baseURL == "") {
-		return errors.New("if stripe-account is set, twilio-auth-token, twilio-from-number and base-url must also be set")
+	} else if twilioAccount != "" && (twilioAuthToken == "" || twilioFromNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
+		return errors.New("if twilio-account is set, twilio-auth-token, twilio-from-number, twilio-verify-service, base-url, and auth-file must also be set")
 	}
 
 	// Backwards compatibility

+ 1 - 1
cmd/tier.go

@@ -18,7 +18,7 @@ const (
 	defaultMessageLimit             = 5000
 	defaultMessageExpiryDuration    = "12h"
 	defaultEmailLimit               = 20
-	defaultCallLimit                = 10
+	defaultCallLimit                = 0
 	defaultReservationLimit         = 3
 	defaultAttachmentFileSizeLimit  = "15M"
 	defaultAttachmentTotalSizeLimit = "100M"

+ 23 - 18
log/event.go

@@ -41,34 +41,34 @@ func newEvent() *Event {
 
 // Fatal logs the event as FATAL, and exits the program with exit code 1
 func (e *Event) Fatal(message string, v ...any) {
-	e.Field(fieldExitCode, 1).maybeLog(FatalLevel, message, v...)
+	e.Field(fieldExitCode, 1).Log(FatalLevel, message, v...)
 	fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr
 	os.Exit(1)
 }
 
 // Error logs the event with log level error
-func (e *Event) Error(message string, v ...any) {
-	e.maybeLog(ErrorLevel, message, v...)
+func (e *Event) Error(message string, v ...any) *Event {
+	return e.Log(ErrorLevel, message, v...)
 }
 
 // Warn logs the event with log level warn
-func (e *Event) Warn(message string, v ...any) {
-	e.maybeLog(WarnLevel, message, v...)
+func (e *Event) Warn(message string, v ...any) *Event {
+	return e.Log(WarnLevel, message, v...)
 }
 
 // Info logs the event with log level info
-func (e *Event) Info(message string, v ...any) {
-	e.maybeLog(InfoLevel, message, v...)
+func (e *Event) Info(message string, v ...any) *Event {
+	return e.Log(InfoLevel, message, v...)
 }
 
 // Debug logs the event with log level debug
-func (e *Event) Debug(message string, v ...any) {
-	e.maybeLog(DebugLevel, message, v...)
+func (e *Event) Debug(message string, v ...any) *Event {
+	return e.Log(DebugLevel, message, v...)
 }
 
 // Trace logs the event with log level trace
-func (e *Event) Trace(message string, v ...any) {
-	e.maybeLog(TraceLevel, message, v...)
+func (e *Event) Trace(message string, v ...any) *Event {
+	return e.Log(TraceLevel, message, v...)
 }
 
 // Tag adds a "tag" field to the log event
@@ -108,6 +108,14 @@ func (e *Event) Field(key string, value any) *Event {
 	return e
 }
 
+// FieldIf adds a custom field and value to the log event if the given level is loggable
+func (e *Event) FieldIf(key string, value any, level Level) *Event {
+	if e.Loggable(level) {
+		return e.Field(key, value)
+	}
+	return e
+}
+
 // Fields adds a map of fields to the log event
 func (e *Event) Fields(fields Context) *Event {
 	if e.fields == nil {
@@ -138,7 +146,7 @@ func (e *Event) With(contexters ...Contexter) *Event {
 // to determine if they match. This is super complicated, but required for efficiency.
 func (e *Event) Render(l Level, message string, v ...any) string {
 	appliedContexters := e.maybeApplyContexters()
-	if !e.shouldLog(l) {
+	if !e.Loggable(l) {
 		return ""
 	}
 	e.Message = fmt.Sprintf(message, v...)
@@ -153,11 +161,12 @@ func (e *Event) Render(l Level, message string, v ...any) string {
 	return e.String()
 }
 
-// maybeLog logs the event to the defined output, or does nothing if Render returns an empty string
-func (e *Event) maybeLog(l Level, message string, v ...any) {
+// Log logs the event to the defined output, or does nothing if Render returns an empty string
+func (e *Event) Log(l Level, message string, v ...any) *Event {
 	if m := e.Render(l, message, v...); m != "" {
 		log.Println(m)
 	}
+	return e
 }
 
 // Loggable returns true if the given log level is lower or equal to the current log level
@@ -199,10 +208,6 @@ func (e *Event) String() string {
 	return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", "))
 }
 
-func (e *Event) shouldLog(l Level) bool {
-	return e.globalLevelWithOverride() <= l
-}
-
 func (e *Event) globalLevelWithOverride() Level {
 	mu.RLock()
 	l, ov := level, overrides

+ 2 - 3
server/config.go

@@ -47,7 +47,6 @@ const (
 	DefaultVisitorMessageDailyLimit             = 0
 	DefaultVisitorEmailLimitBurst               = 16
 	DefaultVisitorEmailLimitReplenish           = time.Hour
-	DefaultVisitorCallDailyLimit                = 10
 	DefaultVisitorAccountCreationLimitBurst     = 3
 	DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
 	DefaultVisitorAuthFailureLimitBurst         = 30
@@ -106,10 +105,10 @@ type Config struct {
 	SMTPServerListen                     string
 	SMTPServerDomain                     string
 	SMTPServerAddrPrefix                 string
-	TwilioMessagingBaseURL               string
 	TwilioAccount                        string
 	TwilioAuthToken                      string
 	TwilioFromNumber                     string
+	TwilioCallsBaseURL                   string
 	TwilioVerifyBaseURL                  string
 	TwilioVerifyService                  string
 	MetricsEnable                        bool
@@ -190,7 +189,7 @@ func NewConfig() *Config {
 		SMTPServerListen:                     "",
 		SMTPServerDomain:                     "",
 		SMTPServerAddrPrefix:                 "",
-		TwilioMessagingBaseURL:               "https://api.twilio.com", // Override for tests
+		TwilioCallsBaseURL:                   "https://api.twilio.com", // Override for tests
 		TwilioAccount:                        "",
 		TwilioAuthToken:                      "",
 		TwilioFromNumber:                     "",

+ 6 - 5
server/server.go

@@ -91,6 +91,7 @@ var (
 	apiAccountSubscriptionPath                           = "/v1/account/subscription"
 	apiAccountReservationPath                            = "/v1/account/reservation"
 	apiAccountPhonePath                                  = "/v1/account/phone"
+	apiAccountPhoneVerifyPath                            = "/v1/account/phone/verify"
 	apiAccountBillingPortalPath                          = "/v1/account/billing/portal"
 	apiAccountBillingWebhookPath                         = "/v1/account/billing/webhook"
 	apiAccountBillingSubscriptionPath                    = "/v1/account/billing/subscription"
@@ -463,12 +464,12 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath {
 		return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingWebhook))(w, r, v) // This request comes from Stripe!
+	} else if r.Method == http.MethodPut && r.URL.Path == apiAccountPhoneVerifyPath {
+		return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberVerify)))(w, r, v)
 	} else if r.Method == http.MethodPut && r.URL.Path == apiAccountPhonePath {
-		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberAdd))(w, r, v)
-	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountPhonePath {
-		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberVerify))(w, r, v)
+		return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v)
 	} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
-		return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberDelete))(w, r, v)
+		return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
 		return s.handleStats(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
@@ -910,7 +911,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
 		return false, false, "", "", false, errHTTPBadRequestEmailDisabled
 	}
 	call = readParam(r, "x-call", "call")
-	if call != "" && s.config.TwilioAccount == "" {
+	if call != "" && s.config.TwilioAccount == "" && s.userManager == nil {
 		return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
 	} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
 		return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid

+ 3 - 9
server/server.yml

@@ -144,7 +144,7 @@
 # smtp-server-domain:
 # smtp-server-addr-prefix:
 
-# If enabled, ntfy can send SMS text messages and do voice calls via Twilio, and the "X-SMS" and "X-Call" headers.
+# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
 #
 # twilio-account:
 # twilio-auth-token:
@@ -225,17 +225,11 @@
 # visitor-request-limit-exempt-hosts: ""
 
 # Rate limiting: Hard daily limit of messages per visitor and day. The limit is reset
-# every day at midnight UTC. If the limit is not set (or set to zero), the request limit (see above)
-# governs the upper limit. SMS and calls are only supported if the twilio-settings are properly configured.
+# every day at midnight UTC. If the limit is not set (or set to zero), the request
+# limit (see above) governs the upper limit.
 #
 # visitor-message-daily-limit: 0
 
-# Rate limiting: Daily limit of SMS and calls per visitor and day. The limit is reset every day
-# at midnight UTC. SMS and calls are only supported if the twilio-settings are properly configured.
-#
-# visitor-sms-daily-limit: 10
-# visitor-call-daily-limit: 10
-
 # Rate limiting: Allowed emails per visitor:
 # - visitor-email-limit-burst is the initial bucket of emails each visitor has
 # - visitor-email-limit-replenish is the rate at which the bucket is refilled

+ 4 - 4
server/server_account.go

@@ -521,7 +521,7 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi
 	return nil
 }
 
-func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
+func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	u := v.User()
 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
 	if err != nil {
@@ -545,13 +545,13 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ
 	}
 	// Actually add the unverified number, and send verification
 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification")
-	if err := s.verifyPhone(v, r, req.Number); err != nil {
+	if err := s.verifyPhoneNumber(v, r, req.Number); err != nil {
 		return err
 	}
 	return s.writeJSON(w, newSuccessResponse())
 }
 
-func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error {
+func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	u := v.User()
 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
 	if err != nil {
@@ -560,7 +560,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
 	if !phoneNumberRegex.MatchString(req.Number) {
 		return errHTTPBadRequestPhoneNumberInvalid
 	}
-	if err := s.checkVerifyPhone(v, r, req.Number, req.Code); err != nil {
+	if err := s.verifyPhoneNumberCheck(v, r, req.Number, req.Code); err != nil {
 		return err
 	}
 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified")

+ 9 - 0
server/server_middleware.go

@@ -85,6 +85,15 @@ func (s *Server) ensureAdmin(next handleFunc) handleFunc {
 	})
 }
 
+func (s *Server) ensureCallsEnabled(next handleFunc) handleFunc {
+	return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
+		if s.config.TwilioAccount == "" {
+			return errHTTPNotFound
+		}
+		return next(w, r, v)
+	}
+}
+
 func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc {
 	return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
 		if s.config.StripeSecretKey == "" || s.stripe == nil {

+ 39 - 68
server/server_twilio.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/xml"
 	"fmt"
-	"github.com/prometheus/client_golang/prometheus"
 	"heckel.io/ntfy/log"
 	"heckel.io/ntfy/user"
 	"heckel.io/ntfy/util"
@@ -15,24 +14,26 @@ import (
 )
 
 const (
-	twilioCallEndpoint = "Calls.json"
-	twilioCallFormat   = `
+	twilioCallFormat = `
 <Response>
 	<Pause length="1"/>
-	<Say loop="5">
+	<Say loop="3">
 		You have a notification from notify on topic %s. Message:
 		<break time="1s"/>
 		%s
 		<break time="1s"/>
 		End message.
 		<break time="1s"/>
-		This message was sent by user %s. It will be repeated up to five times.
+		This message was sent by user %s. It will be repeated up to three times.
 		<break time="3s"/>
 	</Say>
 	<Say>Goodbye.</Say>
 </Response>`
 )
 
+// 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) {
 	if u == nil {
 		return "", errHTTPBadRequestAnonymousCallsNotAllowed
@@ -66,11 +67,38 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
 	data.Set("From", s.config.TwilioFromNumber)
 	data.Set("To", to)
 	data.Set("Twiml", body)
-	s.twilioMessagingRequest(v, r, m, metricCallsMadeSuccess, metricCallsMadeFailure, twilioCallEndpoint, to, body, data)
+	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)
 }
 
-func (s *Server) verifyPhone(v *visitor, r *http.Request, phoneNumber string) error {
-	logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification")
+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
+	}
+	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	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
+}
+
+func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber string) error {
+	ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification")
 	data := url.Values{}
 	data.Set("To", phoneNumber)
 	data.Set("Channel", "sms")
@@ -86,21 +114,16 @@ func (s *Server) verifyPhone(v *visitor, r *http.Request, phoneNumber string) er
 		return err
 	}
 	response, err := io.ReadAll(resp.Body)
-	ev := logvr(v, r).Tag(tagTwilio)
 	if err != nil {
 		ev.Err(err).Warn("Error sending Twilio phone verification request")
 		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")
-	}
+	ev.FieldIf("twilio_response", string(response), log.TraceLevel).Debug("Received Twilio phone verification response")
 	return nil
 }
 
-func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code string) error {
-	logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Checking phone verification")
+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")
 	data := url.Values{}
 	data.Set("To", phoneNumber)
 	data.Set("Code", code)
@@ -111,10 +134,6 @@ func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code
 	}
 	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	log.Fields(httpContext(req)).Field("http_body", data.Encode()).Info("Twilio call")
-	ev := logvr(v, r).
-		Tag(tagTwilio).
-		Field("twilio_to", phoneNumber)
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		return err
@@ -144,54 +163,6 @@ func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code
 	return nil
 }
 
-func (s *Server) twilioMessagingRequest(v *visitor, r *http.Request, m *message, msuccess, mfailure prometheus.Counter, endpoint, to, body string, data url.Values) {
-	logContext := log.Context{
-		"twilio_from": s.config.TwilioFromNumber,
-		"twilio_to":   to,
-	}
-	ev := logvrm(v, r, m).Tag(tagTwilio).Fields(logContext)
-	if ev.IsTrace() {
-		ev.Field("twilio_body", body).Trace("Sending Twilio request")
-	} else if ev.IsDebug() {
-		ev.Debug("Sending Twilio request")
-	}
-	response, err := s.performTwilioMessagingRequestInternal(endpoint, data)
-	if err != nil {
-		ev.
-			Field("twilio_body", body).
-			Field("twilio_response", response).
-			Err(err).
-			Warn("Error sending Twilio request")
-		minc(mfailure)
-		return
-	}
-	if ev.IsTrace() {
-		ev.Field("twilio_response", response).Trace("Received successful Twilio response")
-	} else if ev.IsDebug() {
-		ev.Debug("Received successful Twilio response")
-	}
-	minc(msuccess)
-}
-
-func (s *Server) performTwilioMessagingRequestInternal(endpoint string, data url.Values) (string, error) {
-	requestURL := fmt.Sprintf("%s/2010-04-01/Accounts/%s/%s", s.config.TwilioMessagingBaseURL, s.config.TwilioAccount, endpoint)
-	req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode()))
-	if err != nil {
-		return "", err
-	}
-	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
-	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	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
-}
-
 func xmlEscapeText(text string) string {
 	var buf bytes.Buffer
 	_ = xml.EscapeText(&buf, []byte(text))

+ 128 - 74
server/server_twilio_test.go

@@ -11,119 +11,153 @@ import (
 	"testing"
 )
 
-func TestServer_Twilio_SMS(t *testing.T) {
-	var called atomic.Bool
-	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
+	var called, verified atomic.Bool
+	var code atomic.Pointer[string]
+	twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		body, err := io.ReadAll(r.Body)
 		require.Nil(t, err)
-		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Messages.json", r.URL.Path)
 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
-		require.Equal(t, "Body=test%0A%0A--%0AThis+message+was+sent+by+9.9.9.9+via+ntfy.sh%2Fmytopic&From=%2B1234567890&To=%2B11122233344", string(body))
-		called.Store(true)
+		if r.URL.Path == "/v2/Services/VA1234567890/Verifications" {
+			if code.Load() != nil {
+				t.Fatal("Should be only called once")
+			}
+			require.Equal(t, "Channel=sms&To=%2B12223334444", string(body))
+			code.Store(util.String("123456"))
+		} else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" {
+			if verified.Load() {
+				t.Fatal("Should be only called once")
+			}
+			require.Equal(t, "Code=123456&To=%2B12223334444", string(body))
+			verified.Store(true)
+		} else {
+			t.Fatal("Unexpected path:", r.URL.Path)
+		}
 	}))
-	defer twilioServer.Close()
-
-	c := newTestConfig(t)
-	c.BaseURL = "https://ntfy.sh"
-	c.TwilioMessagingBaseURL = twilioServer.URL
-	c.TwilioAccount = "AC1234567890"
-	c.TwilioAuthToken = "AAEAA1234567890"
-	c.TwilioFromNumber = "+1234567890"
-	c.VisitorSMSDailyLimit = 1
-	s := newTestServer(t, c)
-
-	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"SMS": "+11122233344",
-	})
-	require.Equal(t, "test", toMessage(t, response.Body.String()).Message)
-	waitFor(t, func() bool {
-		return called.Load()
-	})
-}
-
-func TestServer_Twilio_SMS_With_User(t *testing.T) {
-	var called atomic.Bool
-	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	defer twilioVerifyServer.Close()
+	twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if called.Load() {
 			t.Fatal("Should be only called once")
 		}
 		body, err := io.ReadAll(r.Body)
 		require.Nil(t, err)
-		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Messages.json", r.URL.Path)
+		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
-		require.Equal(t, "Body=test%0A%0A--%0AThis+message+was+sent+by+phil+%289.9.9.9%29+via+ntfy.sh%2Fmytopic&From=%2B1234567890&To=%2B11122233344", string(body))
+		require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
 		called.Store(true)
 	}))
-	defer twilioServer.Close()
+	defer twilioCallsServer.Close()
 
 	c := newTestConfigWithAuthFile(t)
-	c.BaseURL = "https://ntfy.sh"
-	c.TwilioMessagingBaseURL = twilioServer.URL
+	c.TwilioVerifyBaseURL = twilioVerifyServer.URL
+	c.TwilioCallsBaseURL = twilioCallsServer.URL
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioFromNumber = "+1234567890"
+	c.TwilioVerifyService = "VA1234567890"
 	s := newTestServer(t, c)
 
 	// Add tier and user
 	require.Nil(t, s.userManager.AddTier(&user.Tier{
 		Code:         "pro",
 		MessageLimit: 10,
-		SMSLimit:     1,
+		CallLimit:    1,
 	}))
 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
 	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
+	u, err := s.userManager.User("phil")
+	require.Nil(t, err)
 
-	// Do request with user
-	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"Authorization": util.BasicAuth("phil", "phil"),
-		"SMS":           "+11122233344",
+	// Send verification code for phone number
+	response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444"}`, map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
 	})
-	require.Equal(t, "test", toMessage(t, response.Body.String()).Message)
+	require.Equal(t, 200, response.Code)
+	waitFor(t, func() bool {
+		return *code.Load() == "123456"
+	})
+
+	// Add phone number with code
+	response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
+	})
+	require.Equal(t, 200, response.Code)
+	waitFor(t, func() bool {
+		return verified.Load()
+	})
+	phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
+	require.Nil(t, err)
+	require.Equal(t, 1, len(phoneNumbers))
+	require.Equal(t, "+12223334444", phoneNumbers[0])
+
+	// Do the thing
+	response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
+		"x-call":        "yes",
+	})
+	require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
 	waitFor(t, func() bool {
 		return called.Load()
 	})
 
-	// Second one should fail due to rate limits
-	response = request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"Authorization": util.BasicAuth("phil", "phil"),
-		"SMS":           "+11122233344",
+	// Remove the phone number
+	response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
 	})
-	require.Equal(t, 42910, toHTTPError(t, response.Body.String()).Code)
+	require.Equal(t, 200, response.Code)
+
+	// Verify the phone number is gone from the DB
+	phoneNumbers, err = s.userManager.PhoneNumbers(u.ID)
+	require.Nil(t, err)
+	require.Equal(t, 0, len(phoneNumbers))
 }
 
-func TestServer_Twilio_Call(t *testing.T) {
+func TestServer_Twilio_Call_Success(t *testing.T) {
 	var called atomic.Bool
 	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if called.Load() {
+			t.Fatal("Should be only called once")
+		}
 		body, err := io.ReadAll(r.Body)
 		require.Nil(t, err)
 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
-		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EYou+have+a+message+from+notify+on+topic+mytopic.+Message%3A%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3Ethis+message+has%26%23xA%3Ba+new+line+and+%26lt%3Bbrackets%26gt%3B%21%26%23xA%3Band+%26%2334%3Bquotes+and+other+%26%2339%3Bquotes%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EEnd+message.%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EThis+message+was+sent+by+9.9.9.9+via+127.0.0.1%3A12345%2Fmytopic%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%3C%2FResponse%3E", string(body))
+		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
 		called.Store(true)
 	}))
 	defer twilioServer.Close()
 
-	c := newTestConfig(t)
-	c.TwilioMessagingBaseURL = twilioServer.URL
+	c := newTestConfigWithAuthFile(t)
+	c.TwilioCallsBaseURL = twilioServer.URL
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioFromNumber = "+1234567890"
-	c.VisitorCallDailyLimit = 1
 	s := newTestServer(t, c)
 
-	body := `this message has
-a new line and <brackets>!
-and "quotes and other 'quotes`
-	response := request(t, s, "POST", "/mytopic", body, map[string]string{
-		"x-call": "+11122233344",
+	// Add tier and user
+	require.Nil(t, s.userManager.AddTier(&user.Tier{
+		Code:         "pro",
+		MessageLimit: 10,
+		CallLimit:    1,
+	}))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
+	u, err := s.userManager.User("phil")
+	require.Nil(t, err)
+	require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
+
+	// Do the thing
+	response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
+		"authorization": util.BasicAuth("phil", "phil"),
+		"x-call":        "+11122233344",
 	})
-	require.Equal(t, "this message has\na new line and <brackets>!\nand \"quotes and other 'quotes", toMessage(t, response.Body.String()).Message)
+	require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
 	waitFor(t, func() bool {
 		return called.Load()
 	})
 }
 
-func TestServer_Twilio_Call_With_User(t *testing.T) {
+func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
 	var called atomic.Bool
 	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if called.Load() {
@@ -133,13 +167,13 @@ func TestServer_Twilio_Call_With_User(t *testing.T) {
 		require.Nil(t, err)
 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
-		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EYou+have+a+message+from+notify+on+topic+mytopic.+Message%3A%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3Ehi+there%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EEnd+message.%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EThis+message+was+sent+by+phil+%289.9.9.9%29+via+127.0.0.1%3A12345%2Fmytopic%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%3C%2FResponse%3E", string(body))
+		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
 		called.Store(true)
 	}))
 	defer twilioServer.Close()
 
 	c := newTestConfigWithAuthFile(t)
-	c.TwilioMessagingBaseURL = twilioServer.URL
+	c.TwilioCallsBaseURL = twilioServer.URL
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioFromNumber = "+1234567890"
@@ -153,11 +187,14 @@ func TestServer_Twilio_Call_With_User(t *testing.T) {
 	}))
 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
 	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
+	u, err := s.userManager.User("phil")
+	require.Nil(t, err)
+	require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
 
 	// Do the thing
 	response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
 		"authorization": util.BasicAuth("phil", "phil"),
-		"x-call":        "+11122233344",
+		"x-call":        "yes", // <<<------
 	})
 	require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
 	waitFor(t, func() bool {
@@ -165,40 +202,57 @@ func TestServer_Twilio_Call_With_User(t *testing.T) {
 	})
 }
 
-func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
-	c := newTestConfig(t)
-	c.TwilioMessagingBaseURL = "https://127.0.0.1"
+func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
+	c := newTestConfigWithAuthFile(t)
+	c.TwilioCallsBaseURL = "http://dummy.invalid"
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioFromNumber = "+1234567890"
 	s := newTestServer(t, c)
 
+	// Add tier and user
+	require.Nil(t, s.userManager.AddTier(&user.Tier{
+		Code:         "pro",
+		MessageLimit: 10,
+		CallLimit:    1,
+	}))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
+
+	// Do the thing
 	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"x-call": "+invalid",
+		"authorization": util.BasicAuth("phil", "phil"),
+		"x-call":        "+11122233344",
 	})
-	require.Equal(t, 40031, toHTTPError(t, response.Body.String()).Code)
+	require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code)
 }
 
-func TestServer_Twilio_SMS_InvalidNumber(t *testing.T) {
+func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
 	c := newTestConfig(t)
-	c.TwilioMessagingBaseURL = "https://127.0.0.1"
+	c.TwilioCallsBaseURL = "https://127.0.0.1"
 	c.TwilioAccount = "AC1234567890"
 	c.TwilioAuthToken = "AAEAA1234567890"
 	c.TwilioFromNumber = "+1234567890"
 	s := newTestServer(t, c)
 
 	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"x-sms": "+invalid",
+		"x-call": "+invalid",
 	})
-	require.Equal(t, 40031, toHTTPError(t, response.Body.String()).Code)
+	require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code)
 }
 
-func TestServer_Twilio_SMS_Unconfigured(t *testing.T) {
-	s := newTestServer(t, newTestConfig(t))
+func TestServer_Twilio_Call_Anonymous(t *testing.T) {
+	c := newTestConfig(t)
+	c.TwilioCallsBaseURL = "https://127.0.0.1"
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioFromNumber = "+1234567890"
+	s := newTestServer(t, c)
+
 	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
-		"x-sms": "+1234",
+		"x-call": "+123123",
 	})
-	require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
+	require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code)
 }
 
 func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
@@ -206,5 +260,5 @@ func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
 	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
 		"x-call": "+1234",
 	})
-	require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
+	require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code)
 }

+ 0 - 3
server/types.go

@@ -326,7 +326,6 @@ type apiAccountLimits struct {
 	Messages                 int64  `json:"messages"`
 	MessagesExpiryDuration   int64  `json:"messages_expiry_duration"`
 	Emails                   int64  `json:"emails"`
-	SMS                      int64  `json:"sms"`
 	Calls                    int64  `json:"calls"`
 	Reservations             int64  `json:"reservations"`
 	AttachmentTotalSize      int64  `json:"attachment_total_size"`
@@ -340,8 +339,6 @@ type apiAccountStats struct {
 	MessagesRemaining            int64 `json:"messages_remaining"`
 	Emails                       int64 `json:"emails"`
 	EmailsRemaining              int64 `json:"emails_remaining"`
-	SMS                          int64 `json:"sms"`
-	SMSRemaining                 int64 `json:"sms_remaining"`
 	Calls                        int64 `json:"calls"`
 	CallsRemaining               int64 `json:"calls_remaining"`
 	Reservations                 int64 `json:"reservations"`

+ 1 - 1
web/public/static/langs/en.json

@@ -130,7 +130,7 @@
   "publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
   "publish_dialog_email_reset": "Remove email forward",
   "publish_dialog_call_label": "Phone call",
-  "publish_dialog_call_placeholder": "Phone number to call with the message, e.g. +12223334444",
+  "publish_dialog_call_placeholder": "Phone number to call with the message, e.g. +12223334444, or 'yes'",
   "publish_dialog_call_reset": "Remove phone call",
   "publish_dialog_attach_label": "Attachment URL",
   "publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",

+ 6 - 6
web/src/app/AccountApi.js

@@ -1,7 +1,7 @@
 import {
     accountBillingPortalUrl,
     accountBillingSubscriptionUrl,
-    accountPasswordUrl, accountPhoneUrl,
+    accountPasswordUrl, accountPhoneUrl, accountPhoneVerifyUrl,
     accountReservationSingleUrl,
     accountReservationUrl,
     accountSettingsUrl,
@@ -299,8 +299,8 @@ class AccountApi {
         return await response.json(); // May throw SyntaxError
     }
 
-    async verifyPhone(phoneNumber) {
-        const url = accountPhoneUrl(config.base_url);
+    async verifyPhoneNumber(phoneNumber) {
+        const url = accountPhoneVerifyUrl(config.base_url);
         console.log(`[AccountApi] Sending phone verification ${url}`);
         await fetchOrThrow(url, {
             method: "PUT",
@@ -311,11 +311,11 @@ class AccountApi {
         });
     }
 
-    async checkVerifyPhone(phoneNumber, code) {
+    async addPhoneNumber(phoneNumber, code) {
         const url = accountPhoneUrl(config.base_url);
-        console.log(`[AccountApi] Checking phone verification code ${url}`);
+        console.log(`[AccountApi] Adding phone number with verification code ${url}`);
         await fetchOrThrow(url, {
-            method: "POST",
+            method: "PUT",
             headers: withBearerAuth({}, session.token()),
             body: JSON.stringify({
                 number: phoneNumber,

+ 1 - 0
web/src/app/utils.js

@@ -28,6 +28,7 @@ export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/ac
 export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
 export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
 export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
+export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
 export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
 export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
 export const expandUrl = (url) => [`https://${url}`, `http://${url}`];

+ 2 - 2
web/src/components/Account.js

@@ -432,7 +432,7 @@ const AddPhoneNumberDialog = (props) => {
     const verifyPhone = async () => {
         try {
             setSending(true);
-            await accountApi.verifyPhone(phoneNumber);
+            await accountApi.verifyPhoneNumber(phoneNumber);
             setVerificationCodeSent(true);
         } catch (e) {
             console.log(`[Account] Error sending verification`, e);
@@ -449,7 +449,7 @@ const AddPhoneNumberDialog = (props) => {
     const checkVerifyPhone = async () => {
         try {
             setSending(true);
-            await accountApi.checkVerifyPhone(phoneNumber, code);
+            await accountApi.addPhoneNumber(phoneNumber, code);
             props.onClose();
         } catch (e) {
             console.log(`[Account] Error confirming verification`, e);

+ 0 - 27
web/src/components/PublishDialog.js

@@ -45,7 +45,6 @@ const PublishDialog = (props) => {
     const [filename, setFilename] = useState("");
     const [filenameEdited, setFilenameEdited] = useState(false);
     const [email, setEmail] = useState("");
-    const [sms, setSms] = useState("");
     const [call, setCall] = useState("");
     const [delay, setDelay] = useState("");
     const [publishAnother, setPublishAnother] = useState(false);
@@ -54,7 +53,6 @@ const PublishDialog = (props) => {
     const [showClickUrl, setShowClickUrl] = useState(false);
     const [showAttachUrl, setShowAttachUrl] = useState(false);
     const [showEmail, setShowEmail] = useState(false);
-    const [showSms, setShowSms] = useState(false);
     const [showCall, setShowCall] = useState(false);
     const [showDelay, setShowDelay] = useState(false);
 
@@ -128,9 +126,6 @@ const PublishDialog = (props) => {
         if (email.trim()) {
             url.searchParams.append("email", email.trim());
         }
-        if (sms.trim()) {
-            url.searchParams.append("sms", sms.trim());
-        }
         if (call.trim()) {
             url.searchParams.append("call", call.trim());
         }
@@ -416,27 +411,6 @@ const PublishDialog = (props) => {
                             />
                         </ClosableRow>
                     }
-                    {showSms &&
-                        <ClosableRow disabled={disabled} closeLabel={t("publish_dialog_sms_reset")} onClose={() => {
-                            setSms("");
-                            setShowSms(false);
-                        }}>
-                            <TextField
-                                margin="dense"
-                                label={t("publish_dialog_sms_label")}
-                                placeholder={t("publish_dialog_sms_placeholder")}
-                                value={sms}
-                                onChange={ev => setSms(ev.target.value)}
-                                disabled={disabled}
-                                type="tel"
-                                variant="standard"
-                                fullWidth
-                                inputProps={{
-                                    "aria-label": t("publish_dialog_sms_label")
-                                }}
-                            />
-                        </ClosableRow>
-                    }
                     {showCall &&
                         <ClosableRow disabled={disabled} closeLabel={t("publish_dialog_call_reset")} onClose={() => {
                             setCall("");
@@ -562,7 +536,6 @@ const PublishDialog = (props) => {
                     <div>
                         {!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} aria-label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                         {!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} aria-label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showSms && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_sms_label")} aria-label={t("publish_dialog_chip_sms_label")} onClick={() => setShowSms(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                         {!showCall && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_call_label")} aria-label={t("publish_dialog_chip_call_label")} onClick={() => setShowCall(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                         {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} aria-label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                         {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} aria-label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}