Cont'd Twilio stuff
This commit is contained in:
parent
deb4f24856
commit
7c574d73de
16 changed files with 240 additions and 236 deletions
|
@ -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: "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-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-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.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: "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"}),
|
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")
|
return errors.New("cannot set enable-signup without also setting enable-login")
|
||||||
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
|
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
|
||||||
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
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 == "") {
|
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioFromNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
|
||||||
return errors.New("if stripe-account is set, twilio-auth-token, twilio-from-number and base-url must also be set")
|
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
|
// Backwards compatibility
|
||||||
|
|
|
@ -18,7 +18,7 @@ const (
|
||||||
defaultMessageLimit = 5000
|
defaultMessageLimit = 5000
|
||||||
defaultMessageExpiryDuration = "12h"
|
defaultMessageExpiryDuration = "12h"
|
||||||
defaultEmailLimit = 20
|
defaultEmailLimit = 20
|
||||||
defaultCallLimit = 10
|
defaultCallLimit = 0
|
||||||
defaultReservationLimit = 3
|
defaultReservationLimit = 3
|
||||||
defaultAttachmentFileSizeLimit = "15M"
|
defaultAttachmentFileSizeLimit = "15M"
|
||||||
defaultAttachmentTotalSizeLimit = "100M"
|
defaultAttachmentTotalSizeLimit = "100M"
|
||||||
|
|
41
log/event.go
41
log/event.go
|
@ -41,34 +41,34 @@ func newEvent() *Event {
|
||||||
|
|
||||||
// Fatal logs the event as FATAL, and exits the program with exit code 1
|
// Fatal logs the event as FATAL, and exits the program with exit code 1
|
||||||
func (e *Event) Fatal(message string, v ...any) {
|
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
|
fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs the event with log level error
|
// Error logs the event with log level error
|
||||||
func (e *Event) Error(message string, v ...any) {
|
func (e *Event) Error(message string, v ...any) *Event {
|
||||||
e.maybeLog(ErrorLevel, message, v...)
|
return e.Log(ErrorLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs the event with log level warn
|
// Warn logs the event with log level warn
|
||||||
func (e *Event) Warn(message string, v ...any) {
|
func (e *Event) Warn(message string, v ...any) *Event {
|
||||||
e.maybeLog(WarnLevel, message, v...)
|
return e.Log(WarnLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs the event with log level info
|
// Info logs the event with log level info
|
||||||
func (e *Event) Info(message string, v ...any) {
|
func (e *Event) Info(message string, v ...any) *Event {
|
||||||
e.maybeLog(InfoLevel, message, v...)
|
return e.Log(InfoLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs the event with log level debug
|
// Debug logs the event with log level debug
|
||||||
func (e *Event) Debug(message string, v ...any) {
|
func (e *Event) Debug(message string, v ...any) *Event {
|
||||||
e.maybeLog(DebugLevel, message, v...)
|
return e.Log(DebugLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace logs the event with log level trace
|
// Trace logs the event with log level trace
|
||||||
func (e *Event) Trace(message string, v ...any) {
|
func (e *Event) Trace(message string, v ...any) *Event {
|
||||||
e.maybeLog(TraceLevel, message, v...)
|
return e.Log(TraceLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag adds a "tag" field to the log event
|
// Tag adds a "tag" field to the log event
|
||||||
|
@ -108,6 +108,14 @@ func (e *Event) Field(key string, value any) *Event {
|
||||||
return e
|
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
|
// Fields adds a map of fields to the log event
|
||||||
func (e *Event) Fields(fields Context) *Event {
|
func (e *Event) Fields(fields Context) *Event {
|
||||||
if e.fields == nil {
|
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.
|
// to determine if they match. This is super complicated, but required for efficiency.
|
||||||
func (e *Event) Render(l Level, message string, v ...any) string {
|
func (e *Event) Render(l Level, message string, v ...any) string {
|
||||||
appliedContexters := e.maybeApplyContexters()
|
appliedContexters := e.maybeApplyContexters()
|
||||||
if !e.shouldLog(l) {
|
if !e.Loggable(l) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
e.Message = fmt.Sprintf(message, v...)
|
e.Message = fmt.Sprintf(message, v...)
|
||||||
|
@ -153,11 +161,12 @@ func (e *Event) Render(l Level, message string, v ...any) string {
|
||||||
return e.String()
|
return e.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// maybeLog logs the event to the defined output, or does nothing if Render returns an empty string
|
// Log 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) {
|
func (e *Event) Log(l Level, message string, v ...any) *Event {
|
||||||
if m := e.Render(l, message, v...); m != "" {
|
if m := e.Render(l, message, v...); m != "" {
|
||||||
log.Println(m)
|
log.Println(m)
|
||||||
}
|
}
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loggable returns true if the given log level is lower or equal to the current log level
|
// 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, ", "))
|
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 {
|
func (e *Event) globalLevelWithOverride() Level {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
l, ov := level, overrides
|
l, ov := level, overrides
|
||||||
|
|
|
@ -47,7 +47,6 @@ const (
|
||||||
DefaultVisitorMessageDailyLimit = 0
|
DefaultVisitorMessageDailyLimit = 0
|
||||||
DefaultVisitorEmailLimitBurst = 16
|
DefaultVisitorEmailLimitBurst = 16
|
||||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||||
DefaultVisitorCallDailyLimit = 10
|
|
||||||
DefaultVisitorAccountCreationLimitBurst = 3
|
DefaultVisitorAccountCreationLimitBurst = 3
|
||||||
DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
|
DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
|
||||||
DefaultVisitorAuthFailureLimitBurst = 30
|
DefaultVisitorAuthFailureLimitBurst = 30
|
||||||
|
@ -106,10 +105,10 @@ type Config struct {
|
||||||
SMTPServerListen string
|
SMTPServerListen string
|
||||||
SMTPServerDomain string
|
SMTPServerDomain string
|
||||||
SMTPServerAddrPrefix string
|
SMTPServerAddrPrefix string
|
||||||
TwilioMessagingBaseURL string
|
|
||||||
TwilioAccount string
|
TwilioAccount string
|
||||||
TwilioAuthToken string
|
TwilioAuthToken string
|
||||||
TwilioFromNumber string
|
TwilioFromNumber string
|
||||||
|
TwilioCallsBaseURL string
|
||||||
TwilioVerifyBaseURL string
|
TwilioVerifyBaseURL string
|
||||||
TwilioVerifyService string
|
TwilioVerifyService string
|
||||||
MetricsEnable bool
|
MetricsEnable bool
|
||||||
|
@ -190,7 +189,7 @@ func NewConfig() *Config {
|
||||||
SMTPServerListen: "",
|
SMTPServerListen: "",
|
||||||
SMTPServerDomain: "",
|
SMTPServerDomain: "",
|
||||||
SMTPServerAddrPrefix: "",
|
SMTPServerAddrPrefix: "",
|
||||||
TwilioMessagingBaseURL: "https://api.twilio.com", // Override for tests
|
TwilioCallsBaseURL: "https://api.twilio.com", // Override for tests
|
||||||
TwilioAccount: "",
|
TwilioAccount: "",
|
||||||
TwilioAuthToken: "",
|
TwilioAuthToken: "",
|
||||||
TwilioFromNumber: "",
|
TwilioFromNumber: "",
|
||||||
|
|
|
@ -91,6 +91,7 @@ var (
|
||||||
apiAccountSubscriptionPath = "/v1/account/subscription"
|
apiAccountSubscriptionPath = "/v1/account/subscription"
|
||||||
apiAccountReservationPath = "/v1/account/reservation"
|
apiAccountReservationPath = "/v1/account/reservation"
|
||||||
apiAccountPhonePath = "/v1/account/phone"
|
apiAccountPhonePath = "/v1/account/phone"
|
||||||
|
apiAccountPhoneVerifyPath = "/v1/account/phone/verify"
|
||||||
apiAccountBillingPortalPath = "/v1/account/billing/portal"
|
apiAccountBillingPortalPath = "/v1/account/billing/portal"
|
||||||
apiAccountBillingWebhookPath = "/v1/account/billing/webhook"
|
apiAccountBillingWebhookPath = "/v1/account/billing/webhook"
|
||||||
apiAccountBillingSubscriptionPath = "/v1/account/billing/subscription"
|
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)
|
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
|
||||||
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath {
|
} 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!
|
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 {
|
} else if r.Method == http.MethodPut && r.URL.Path == apiAccountPhonePath {
|
||||||
return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberAdd))(w, r, v)
|
return s.ensureUser(s.ensureCallsEnabled(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)
|
|
||||||
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
|
} 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 {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
|
||||||
return s.handleStats(w, r, v)
|
return s.handleStats(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
|
} 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
|
return false, false, "", "", false, errHTTPBadRequestEmailDisabled
|
||||||
}
|
}
|
||||||
call = readParam(r, "x-call", "call")
|
call = readParam(r, "x-call", "call")
|
||||||
if call != "" && s.config.TwilioAccount == "" {
|
if call != "" && s.config.TwilioAccount == "" && s.userManager == nil {
|
||||||
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
||||||
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
||||||
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
# smtp-server-domain:
|
# smtp-server-domain:
|
||||||
# smtp-server-addr-prefix:
|
# 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-account:
|
||||||
# twilio-auth-token:
|
# twilio-auth-token:
|
||||||
|
@ -225,17 +225,11 @@
|
||||||
# visitor-request-limit-exempt-hosts: ""
|
# visitor-request-limit-exempt-hosts: ""
|
||||||
|
|
||||||
# Rate limiting: Hard daily limit of messages per visitor and day. The limit is reset
|
# 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)
|
# every day at midnight UTC. If the limit is not set (or set to zero), the request
|
||||||
# governs the upper limit. SMS and calls are only supported if the twilio-settings are properly configured.
|
# limit (see above) governs the upper limit.
|
||||||
#
|
#
|
||||||
# visitor-message-daily-limit: 0
|
# 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:
|
# Rate limiting: Allowed emails per visitor:
|
||||||
# - visitor-email-limit-burst is the initial bucket of emails each visitor has
|
# - 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
|
# - visitor-email-limit-replenish is the rate at which the bucket is refilled
|
||||||
|
|
|
@ -521,7 +521,7 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi
|
||||||
return nil
|
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()
|
u := v.User()
|
||||||
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
|
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
if err != nil {
|
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
|
// Actually add the unverified number, and send verification
|
||||||
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number 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 err
|
||||||
}
|
}
|
||||||
return s.writeJSON(w, newSuccessResponse())
|
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()
|
u := v.User()
|
||||||
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
|
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -560,7 +560,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
|
||||||
if !phoneNumberRegex.MatchString(req.Number) {
|
if !phoneNumberRegex.MatchString(req.Number) {
|
||||||
return errHTTPBadRequestPhoneNumberInvalid
|
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
|
return err
|
||||||
}
|
}
|
||||||
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified")
|
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified")
|
||||||
|
|
|
@ -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 {
|
func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if s.config.StripeSecretKey == "" || s.stripe == nil {
|
if s.config.StripeSecretKey == "" || s.stripe == nil {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"heckel.io/ntfy/log"
|
"heckel.io/ntfy/log"
|
||||||
"heckel.io/ntfy/user"
|
"heckel.io/ntfy/user"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
|
@ -15,24 +14,26 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
twilioCallEndpoint = "Calls.json"
|
|
||||||
twilioCallFormat = `
|
twilioCallFormat = `
|
||||||
<Response>
|
<Response>
|
||||||
<Pause length="1"/>
|
<Pause length="1"/>
|
||||||
<Say loop="5">
|
<Say loop="3">
|
||||||
You have a notification from notify on topic %s. Message:
|
You have a notification from notify on topic %s. Message:
|
||||||
<break time="1s"/>
|
<break time="1s"/>
|
||||||
%s
|
%s
|
||||||
<break time="1s"/>
|
<break time="1s"/>
|
||||||
End message.
|
End message.
|
||||||
<break time="1s"/>
|
<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"/>
|
<break time="3s"/>
|
||||||
</Say>
|
</Say>
|
||||||
<Say>Goodbye.</Say>
|
<Say>Goodbye.</Say>
|
||||||
</Response>`
|
</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) {
|
func (s *Server) convertPhoneNumber(u *user.User, phoneNumber string) (string, *errHTTP) {
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return "", errHTTPBadRequestAnonymousCallsNotAllowed
|
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("From", s.config.TwilioFromNumber)
|
||||||
data.Set("To", to)
|
data.Set("To", to)
|
||||||
data.Set("Twiml", body)
|
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 {
|
func (s *Server) callPhoneInternal(data url.Values) (string, error) {
|
||||||
logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification")
|
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 := url.Values{}
|
||||||
data.Set("To", phoneNumber)
|
data.Set("To", phoneNumber)
|
||||||
data.Set("Channel", "sms")
|
data.Set("Channel", "sms")
|
||||||
|
@ -86,21 +114,16 @@ func (s *Server) verifyPhone(v *visitor, r *http.Request, phoneNumber string) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
response, err := io.ReadAll(resp.Body)
|
response, err := io.ReadAll(resp.Body)
|
||||||
ev := logvr(v, r).Tag(tagTwilio)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ev.Err(err).Warn("Error sending Twilio phone verification request")
|
ev.Err(err).Warn("Error sending Twilio phone verification request")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ev.IsTrace() {
|
ev.FieldIf("twilio_response", string(response), log.TraceLevel).Debug("Received Twilio phone verification response")
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code string) error {
|
func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, phoneNumber, code string) error {
|
||||||
logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Checking phone verification")
|
ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Checking phone verification")
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("To", phoneNumber)
|
data.Set("To", phoneNumber)
|
||||||
data.Set("Code", code)
|
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.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
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)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -144,54 +163,6 @@ func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code
|
||||||
return nil
|
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 {
|
func xmlEscapeText(text string) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
_ = xml.EscapeText(&buf, []byte(text))
|
_ = xml.EscapeText(&buf, []byte(text))
|
||||||
|
|
|
@ -11,37 +11,108 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_Twilio_SMS(t *testing.T) {
|
func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
|
||||||
var called atomic.Bool
|
var called, verified atomic.Bool
|
||||||
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var code atomic.Pointer[string]
|
||||||
|
twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
require.Nil(t, err)
|
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, "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))
|
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 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/Calls.json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
|
||||||
|
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)
|
called.Store(true)
|
||||||
}))
|
}))
|
||||||
defer twilioServer.Close()
|
defer twilioCallsServer.Close()
|
||||||
|
|
||||||
c := newTestConfig(t)
|
c := newTestConfigWithAuthFile(t)
|
||||||
c.BaseURL = "https://ntfy.sh"
|
c.TwilioVerifyBaseURL = twilioVerifyServer.URL
|
||||||
c.TwilioMessagingBaseURL = twilioServer.URL
|
c.TwilioCallsBaseURL = twilioCallsServer.URL
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
c.VisitorSMSDailyLimit = 1
|
c.TwilioVerifyService = "VA1234567890"
|
||||||
s := newTestServer(t, c)
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
// Add tier and user
|
||||||
"SMS": "+11122233344",
|
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)
|
||||||
|
|
||||||
|
// 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 {
|
waitFor(t, func() bool {
|
||||||
return called.Load()
|
return called.Load()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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, 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_SMS_With_User(t *testing.T) {
|
func TestServer_Twilio_Call_Success(t *testing.T) {
|
||||||
var called atomic.Bool
|
var called atomic.Bool
|
||||||
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if called.Load() {
|
if called.Load() {
|
||||||
|
@ -49,16 +120,15 @@ func TestServer_Twilio_SMS_With_User(t *testing.T) {
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
require.Nil(t, err)
|
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, "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=%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)
|
called.Store(true)
|
||||||
}))
|
}))
|
||||||
defer twilioServer.Close()
|
defer twilioServer.Close()
|
||||||
|
|
||||||
c := newTestConfigWithAuthFile(t)
|
c := newTestConfigWithAuthFile(t)
|
||||||
c.BaseURL = "https://ntfy.sh"
|
c.TwilioCallsBaseURL = twilioServer.URL
|
||||||
c.TwilioMessagingBaseURL = twilioServer.URL
|
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
@ -68,62 +138,26 @@ func TestServer_Twilio_SMS_With_User(t *testing.T) {
|
||||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
Code: "pro",
|
Code: "pro",
|
||||||
MessageLimit: 10,
|
MessageLimit: 10,
|
||||||
SMSLimit: 1,
|
CallLimit: 1,
|
||||||
}))
|
}))
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
u, err := s.userManager.User("phil")
|
||||||
// Do request with user
|
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
|
||||||
"Authorization": util.BasicAuth("phil", "phil"),
|
|
||||||
"SMS": "+11122233344",
|
|
||||||
})
|
|
||||||
require.Equal(t, "test", 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",
|
|
||||||
})
|
|
||||||
require.Equal(t, 42910, toHTTPError(t, response.Body.String()).Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_Twilio_Call(t *testing.T) {
|
|
||||||
var called atomic.Bool
|
|
||||||
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
|
require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
|
||||||
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))
|
|
||||||
called.Store(true)
|
|
||||||
}))
|
|
||||||
defer twilioServer.Close()
|
|
||||||
|
|
||||||
c := newTestConfig(t)
|
// Do the thing
|
||||||
c.TwilioMessagingBaseURL = twilioServer.URL
|
response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
|
||||||
c.TwilioAccount = "AC1234567890"
|
"authorization": util.BasicAuth("phil", "phil"),
|
||||||
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",
|
"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 {
|
waitFor(t, func() bool {
|
||||||
return called.Load()
|
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
|
var called atomic.Bool
|
||||||
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if called.Load() {
|
if called.Load() {
|
||||||
|
@ -133,13 +167,44 @@ func TestServer_Twilio_Call_With_User(t *testing.T) {
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.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, "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)
|
called.Store(true)
|
||||||
}))
|
}))
|
||||||
defer twilioServer.Close()
|
defer twilioServer.Close()
|
||||||
|
|
||||||
c := newTestConfigWithAuthFile(t)
|
c := newTestConfigWithAuthFile(t)
|
||||||
c.TwilioMessagingBaseURL = twilioServer.URL
|
c.TwilioCallsBaseURL = twilioServer.URL
|
||||||
|
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"))
|
||||||
|
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": "yes", // <<<------
|
||||||
|
})
|
||||||
|
require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return called.Load()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.TwilioCallsBaseURL = "http://dummy.invalid"
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
@ -155,19 +220,16 @@ func TestServer_Twilio_Call_With_User(t *testing.T) {
|
||||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
|
||||||
// Do the thing
|
// Do the thing
|
||||||
response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
"authorization": util.BasicAuth("phil", "phil"),
|
"authorization": util.BasicAuth("phil", "phil"),
|
||||||
"x-call": "+11122233344",
|
"x-call": "+11122233344",
|
||||||
})
|
})
|
||||||
require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
|
require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code)
|
||||||
waitFor(t, func() bool {
|
|
||||||
return called.Load()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
|
func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
c.TwilioMessagingBaseURL = "https://127.0.0.1"
|
c.TwilioCallsBaseURL = "https://127.0.0.1"
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
@ -176,29 +238,21 @@ func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
"x-call": "+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_InvalidNumber(t *testing.T) {
|
func TestServer_Twilio_Call_Anonymous(t *testing.T) {
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
c.TwilioMessagingBaseURL = "https://127.0.0.1"
|
c.TwilioCallsBaseURL = "https://127.0.0.1"
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
s := newTestServer(t, c)
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
"x-sms": "+invalid",
|
"x-call": "+123123",
|
||||||
})
|
})
|
||||||
require.Equal(t, 40031, toHTTPError(t, response.Body.String()).Code)
|
require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code)
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_Twilio_SMS_Unconfigured(t *testing.T) {
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
|
||||||
"x-sms": "+1234",
|
|
||||||
})
|
|
||||||
require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
|
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{
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
"x-call": "+1234",
|
"x-call": "+1234",
|
||||||
})
|
})
|
||||||
require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
|
require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code)
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,6 @@ type apiAccountLimits struct {
|
||||||
Messages int64 `json:"messages"`
|
Messages int64 `json:"messages"`
|
||||||
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
||||||
Emails int64 `json:"emails"`
|
Emails int64 `json:"emails"`
|
||||||
SMS int64 `json:"sms"`
|
|
||||||
Calls int64 `json:"calls"`
|
Calls int64 `json:"calls"`
|
||||||
Reservations int64 `json:"reservations"`
|
Reservations int64 `json:"reservations"`
|
||||||
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
||||||
|
@ -340,8 +339,6 @@ type apiAccountStats struct {
|
||||||
MessagesRemaining int64 `json:"messages_remaining"`
|
MessagesRemaining int64 `json:"messages_remaining"`
|
||||||
Emails int64 `json:"emails"`
|
Emails int64 `json:"emails"`
|
||||||
EmailsRemaining int64 `json:"emails_remaining"`
|
EmailsRemaining int64 `json:"emails_remaining"`
|
||||||
SMS int64 `json:"sms"`
|
|
||||||
SMSRemaining int64 `json:"sms_remaining"`
|
|
||||||
Calls int64 `json:"calls"`
|
Calls int64 `json:"calls"`
|
||||||
CallsRemaining int64 `json:"calls_remaining"`
|
CallsRemaining int64 `json:"calls_remaining"`
|
||||||
Reservations int64 `json:"reservations"`
|
Reservations int64 `json:"reservations"`
|
||||||
|
|
|
@ -130,7 +130,7 @@
|
||||||
"publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
|
"publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
|
||||||
"publish_dialog_email_reset": "Remove email forward",
|
"publish_dialog_email_reset": "Remove email forward",
|
||||||
"publish_dialog_call_label": "Phone call",
|
"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_call_reset": "Remove phone call",
|
||||||
"publish_dialog_attach_label": "Attachment URL",
|
"publish_dialog_attach_label": "Attachment URL",
|
||||||
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
|
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
accountBillingPortalUrl,
|
accountBillingPortalUrl,
|
||||||
accountBillingSubscriptionUrl,
|
accountBillingSubscriptionUrl,
|
||||||
accountPasswordUrl, accountPhoneUrl,
|
accountPasswordUrl, accountPhoneUrl, accountPhoneVerifyUrl,
|
||||||
accountReservationSingleUrl,
|
accountReservationSingleUrl,
|
||||||
accountReservationUrl,
|
accountReservationUrl,
|
||||||
accountSettingsUrl,
|
accountSettingsUrl,
|
||||||
|
@ -299,8 +299,8 @@ class AccountApi {
|
||||||
return await response.json(); // May throw SyntaxError
|
return await response.json(); // May throw SyntaxError
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyPhone(phoneNumber) {
|
async verifyPhoneNumber(phoneNumber) {
|
||||||
const url = accountPhoneUrl(config.base_url);
|
const url = accountPhoneVerifyUrl(config.base_url);
|
||||||
console.log(`[AccountApi] Sending phone verification ${url}`);
|
console.log(`[AccountApi] Sending phone verification ${url}`);
|
||||||
await fetchOrThrow(url, {
|
await fetchOrThrow(url, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -311,11 +311,11 @@ class AccountApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkVerifyPhone(phoneNumber, code) {
|
async addPhoneNumber(phoneNumber, code) {
|
||||||
const url = accountPhoneUrl(config.base_url);
|
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, {
|
await fetchOrThrow(url, {
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
headers: withBearerAuth({}, session.token()),
|
headers: withBearerAuth({}, session.token()),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
number: phoneNumber,
|
number: phoneNumber,
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/ac
|
||||||
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
||||||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||||
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
|
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 tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||||
|
|
|
@ -432,7 +432,7 @@ const AddPhoneNumberDialog = (props) => {
|
||||||
const verifyPhone = async () => {
|
const verifyPhone = async () => {
|
||||||
try {
|
try {
|
||||||
setSending(true);
|
setSending(true);
|
||||||
await accountApi.verifyPhone(phoneNumber);
|
await accountApi.verifyPhoneNumber(phoneNumber);
|
||||||
setVerificationCodeSent(true);
|
setVerificationCodeSent(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[Account] Error sending verification`, e);
|
console.log(`[Account] Error sending verification`, e);
|
||||||
|
@ -449,7 +449,7 @@ const AddPhoneNumberDialog = (props) => {
|
||||||
const checkVerifyPhone = async () => {
|
const checkVerifyPhone = async () => {
|
||||||
try {
|
try {
|
||||||
setSending(true);
|
setSending(true);
|
||||||
await accountApi.checkVerifyPhone(phoneNumber, code);
|
await accountApi.addPhoneNumber(phoneNumber, code);
|
||||||
props.onClose();
|
props.onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[Account] Error confirming verification`, e);
|
console.log(`[Account] Error confirming verification`, e);
|
||||||
|
|
|
@ -45,7 +45,6 @@ const PublishDialog = (props) => {
|
||||||
const [filename, setFilename] = useState("");
|
const [filename, setFilename] = useState("");
|
||||||
const [filenameEdited, setFilenameEdited] = useState(false);
|
const [filenameEdited, setFilenameEdited] = useState(false);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [sms, setSms] = useState("");
|
|
||||||
const [call, setCall] = useState("");
|
const [call, setCall] = useState("");
|
||||||
const [delay, setDelay] = useState("");
|
const [delay, setDelay] = useState("");
|
||||||
const [publishAnother, setPublishAnother] = useState(false);
|
const [publishAnother, setPublishAnother] = useState(false);
|
||||||
|
@ -54,7 +53,6 @@ const PublishDialog = (props) => {
|
||||||
const [showClickUrl, setShowClickUrl] = useState(false);
|
const [showClickUrl, setShowClickUrl] = useState(false);
|
||||||
const [showAttachUrl, setShowAttachUrl] = useState(false);
|
const [showAttachUrl, setShowAttachUrl] = useState(false);
|
||||||
const [showEmail, setShowEmail] = useState(false);
|
const [showEmail, setShowEmail] = useState(false);
|
||||||
const [showSms, setShowSms] = useState(false);
|
|
||||||
const [showCall, setShowCall] = useState(false);
|
const [showCall, setShowCall] = useState(false);
|
||||||
const [showDelay, setShowDelay] = useState(false);
|
const [showDelay, setShowDelay] = useState(false);
|
||||||
|
|
||||||
|
@ -128,9 +126,6 @@ const PublishDialog = (props) => {
|
||||||
if (email.trim()) {
|
if (email.trim()) {
|
||||||
url.searchParams.append("email", email.trim());
|
url.searchParams.append("email", email.trim());
|
||||||
}
|
}
|
||||||
if (sms.trim()) {
|
|
||||||
url.searchParams.append("sms", sms.trim());
|
|
||||||
}
|
|
||||||
if (call.trim()) {
|
if (call.trim()) {
|
||||||
url.searchParams.append("call", call.trim());
|
url.searchParams.append("call", call.trim());
|
||||||
}
|
}
|
||||||
|
@ -416,27 +411,6 @@ const PublishDialog = (props) => {
|
||||||
/>
|
/>
|
||||||
</ClosableRow>
|
</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 &&
|
{showCall &&
|
||||||
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_call_reset")} onClose={() => {
|
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_call_reset")} onClose={() => {
|
||||||
setCall("");
|
setCall("");
|
||||||
|
@ -562,7 +536,6 @@ const PublishDialog = (props) => {
|
||||||
<div>
|
<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}}/>}
|
{!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}}/>}
|
{!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}}/>}
|
{!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}}/>}
|
{!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}}/>}
|
{!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}}/>}
|
||||||
|
|
Loading…
Reference in a new issue