From f99159ee5bed8189ad4ad83fdfab50bb9820319a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 12 May 2023 20:01:12 -0400 Subject: [PATCH] WIP calls, remove SMS --- cmd/serve.go | 5 +- cmd/tier.go | 8 -- docs/publish.md | 150 +++----------------------------- server/config.go | 2 - server/errors.go | 9 +- server/server.go | 42 ++++----- server/server_account.go | 14 ++- server/server_payments.go | 2 - server/server_twilio.go | 31 ++++--- server/types.go | 1 - server/visitor.go | 36 ++------ user/manager.go | 55 ++++++------ user/types.go | 4 +- web/public/config.js | 1 - web/public/static/langs/en.json | 11 +-- web/src/components/Account.js | 62 ++++++++----- 16 files changed, 132 insertions(+), 301 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 42b72332..9e020576 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -71,7 +71,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}), 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 SMS and calling, 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-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-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}), @@ -84,7 +84,6 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}), - altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-sms-daily-limit", Aliases: []string{"visitor_sms_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_SMS_DAILY_LIMIT"}, Value: server.DefaultVisitorSMSDailyLimit, Usage: "max number of SMS messages per visitor per day"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-call-daily-limit", Aliases: []string{"visitor_call_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_CALL_DAILY_LIMIT"}, Value: server.DefaultVisitorCallDailyLimit, Usage: "max number of phone calls per visitor per day"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), @@ -172,7 +171,6 @@ func execServe(c *cli.Context) error { visitorMessageDailyLimit := c.Int("visitor-message-daily-limit") visitorEmailLimitBurst := c.Int("visitor-email-limit-burst") visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish") - visitorSMSDailyLimit := c.Int("visitor-sms-daily-limit") visitorCallDailyLimit := c.Int("visitor-call-daily-limit") behindProxy := c.Bool("behind-proxy") stripeSecretKey := c.String("stripe-secret-key") @@ -336,7 +334,6 @@ func execServe(c *cli.Context) error { conf.VisitorMessageDailyLimit = visitorMessageDailyLimit conf.VisitorEmailLimitBurst = visitorEmailLimitBurst conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish - conf.VisitorSMSDailyLimit = visitorSMSDailyLimit conf.VisitorCallDailyLimit = visitorCallDailyLimit conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy diff --git a/cmd/tier.go b/cmd/tier.go index 6b95bdd2..e77927c5 100644 --- a/cmd/tier.go +++ b/cmd/tier.go @@ -18,7 +18,6 @@ const ( defaultMessageLimit = 5000 defaultMessageExpiryDuration = "12h" defaultEmailLimit = 20 - defaultSMSLimit = 10 defaultCallLimit = 10 defaultReservationLimit = 3 defaultAttachmentFileSizeLimit = "15M" @@ -50,7 +49,6 @@ var cmdTier = &cli.Command{ &cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"}, &cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"}, - &cli.Int64Flag{Name: "sms-limit", Value: defaultSMSLimit, Usage: "daily SMS limit"}, &cli.Int64Flag{Name: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"}, &cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"}, @@ -95,7 +93,6 @@ Examples: &cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"}, &cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"}, - &cli.Int64Flag{Name: "sms-limit", Usage: "daily SMS limit"}, &cli.Int64Flag{Name: "call-limit", Usage: "daily phone call limit"}, &cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"}, @@ -221,7 +218,6 @@ func execTierAdd(c *cli.Context) error { MessageLimit: c.Int64("message-limit"), MessageExpiryDuration: messageExpiryDuration, EmailLimit: c.Int64("email-limit"), - SMSLimit: c.Int64("sms-limit"), CallLimit: c.Int64("call-limit"), ReservationLimit: c.Int64("reservation-limit"), AttachmentFileSizeLimit: attachmentFileSizeLimit, @@ -275,9 +271,6 @@ func execTierChange(c *cli.Context) error { if c.IsSet("email-limit") { tier.EmailLimit = c.Int64("email-limit") } - if c.IsSet("sms-limit") { - tier.SMSLimit = c.Int64("sms-limit") - } if c.IsSet("call-limit") { tier.CallLimit = c.Int64("call-limit") } @@ -371,7 +364,6 @@ func printTier(c *cli.Context, tier *user.Tier) { fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit) fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds())) fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit) - fmt.Fprintf(c.App.ErrWriter, "- SMS limit: %d\n", tier.SMSLimit) fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit) fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit) fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit)) diff --git a/docs/publish.md b/docs/publish.md index 1d1109e4..72398a7c 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2695,51 +2695,48 @@ title `You've Got Mail` to topic `sometopic` (see [ntfy.sh/sometopic](https://nt
Publishing a message via e-mail
-## Text message (SMS) +## Phone calls _Supported on:_ :material-android: :material-apple: :material-firefox: -You can forward messages as text message (SMS) by specifying a phone number a header. Similar to email notifications, -this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have the ntfy app -installed on their phone. +You can use ntfy to call a phone and **read the message out loud using text-to-speech**, by specifying a phone number a header. +Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have +the ntfy app installed on their phone. -To forward a message as an SMS, pass a phone number in the `X-SMS` header (or its alias: `SMS`), prefixed with a plus sign -and the country code, e.g. `+12223334444`. +Phone numbers have to be previously verified (via the web app). To forward a message as a phone call, pass a phone number +in the `X-Call` header (or its alias: `Call`), prefixed with a plus sign and the country code, e.g. `+12223334444`. You may +also simply pass `yes` as a value if you only have one verified phone number. On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. === "Command line (curl)" ``` curl \ - -H "SMS: +12223334444" \ - -d "Your garage seems to be on fire 🔥. You should probably check that out, and call 0118 999 881 999 119 725 3." \ + -H "Call: +12223334444" \ + -d "Your garage seems to be on fire. You should probably check that out, and call 0118 999 881 999 119 725 3 for help." \ ntfy.sh/alerts ``` === "ntfy CLI" ``` ntfy publish \ - --email=phil@example.com \ - --tags=warning,skull,backup-host,ssh-login \ - --priority=high \ - alerts "Unknown login from 5.31.23.83 to backups.example.com" + --call=+12223334444 \ + alerts "Your garage seems to be on fire. You should probably check that out, and call 0118 999 881 999 119 725 3 for help." ``` === "HTTP" ``` http POST /alerts HTTP/1.1 Host: ntfy.sh - Email: phil@example.com - Tags: warning,skull,backup-host,ssh-login - Priority: high + Call: +12223334444 - Unknown login from 5.31.23.83 to backups.example.com + Your garage seems to be on fire. You should probably check that out, and call 0118 999 881 999 119 725 3 for help. ``` === "JavaScript" ``` javascript fetch('https://ntfy.sh/alerts', { method: 'POST', - body: "Unknown login from 5.31.23.83 to backups.example.com", + body: "Your garage seems to be on fire. You should probably check that out, and call 0118 999 881 999 119 725 3 for help.", headers: { 'Email': 'phil@example.com', 'Tags': 'warning,skull,backup-host,ssh-login', @@ -2807,125 +2804,6 @@ Here's what that looks like in Google Mail:
E-mail notification
- -## Phone calls -_Supported on:_ :material-android: :material-apple: :material-firefox: - -You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that -you'd like to persist longer, or to blast-notify yourself on all possible channels. - -Usage is easy: Simply pass the `X-Email` header (or any of its aliases: `X-E-mail`, `Email`, `E-mail`, `Mail`, or `e`). -Only one e-mail address is supported. - -Since ntfy does not provide auth (yet), the rate limiting is pretty strict (see [limitations](#limitations)). In the -default configuration, you get **16 e-mails per visitor** (IP address) and then after that one per hour. On top of -that, your IP address appears in the e-mail body. This is to prevent abuse. - -=== "Command line (curl)" - ``` - curl \ - -H "Email: phil@example.com" \ - -H "Tags: warning,skull,backup-host,ssh-login" \ - -H "Priority: high" \ - -d "Unknown login from 5.31.23.83 to backups.example.com" \ - ntfy.sh/alerts - curl -H "Email: phil@example.com" -d "You've Got Mail" - curl -d "You've Got Mail" "ntfy.sh/alerts?email=phil@example.com" - ``` - -=== "ntfy CLI" - ``` - ntfy publish \ - --email=phil@example.com \ - --tags=warning,skull,backup-host,ssh-login \ - --priority=high \ - alerts "Unknown login from 5.31.23.83 to backups.example.com" - ``` - -=== "HTTP" - ``` http - POST /alerts HTTP/1.1 - Host: ntfy.sh - Email: phil@example.com - Tags: warning,skull,backup-host,ssh-login - Priority: high - - Unknown login from 5.31.23.83 to backups.example.com - ``` - -=== "JavaScript" - ``` javascript - fetch('https://ntfy.sh/alerts', { - method: 'POST', - body: "Unknown login from 5.31.23.83 to backups.example.com", - headers: { - 'Email': 'phil@example.com', - 'Tags': 'warning,skull,backup-host,ssh-login', - 'Priority': 'high' - } - }) - ``` - -=== "Go" - ``` go - req, _ := http.NewRequest("POST", "https://ntfy.sh/alerts", - strings.NewReader("Unknown login from 5.31.23.83 to backups.example.com")) - req.Header.Set("Email", "phil@example.com") - req.Header.Set("Tags", "warning,skull,backup-host,ssh-login") - req.Header.Set("Priority", "high") - http.DefaultClient.Do(req) - ``` - -=== "PowerShell" - ``` powershell - $Request = @{ - Method = "POST" - URI = "https://ntfy.sh/alerts" - Headers = @{ - Title = "Low disk space alert" - Priority = "high" - Tags = "warning,skull,backup-host,ssh-login") - Email = "phil@example.com" - } - Body = "Unknown login from 5.31.23.83 to backups.example.com" - } - Invoke-RestMethod @Request - ``` - -=== "Python" - ``` python - requests.post("https://ntfy.sh/alerts", - data="Unknown login from 5.31.23.83 to backups.example.com", - headers={ - "Email": "phil@example.com", - "Tags": "warning,skull,backup-host,ssh-login", - "Priority": "high" - }) - ``` - -=== "PHP" - ``` php-inline - file_get_contents('https://ntfy.sh/alerts', false, stream_context_create([ - 'http' => [ - 'method' => 'POST', - 'header' => - "Content-Type: text/plain\r\n" . - "Email: phil@example.com\r\n" . - "Tags: warning,skull,backup-host,ssh-login\r\n" . - "Priority: high", - 'content' => 'Unknown login from 5.31.23.83 to backups.example.com' - ] - ])); - ``` - -Here's what that looks like in Google Mail: - -
- ![e-mail notification](static/img/screenshot-email.png){ width=600 } -
E-mail notification
-
- - ## Authentication Depending on whether the server is configured to support [access control](config.md#access-control), some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. diff --git a/server/config.go b/server/config.go index 352d91fc..3fd88e80 100644 --- a/server/config.go +++ b/server/config.go @@ -47,7 +47,6 @@ const ( DefaultVisitorMessageDailyLimit = 0 DefaultVisitorEmailLimitBurst = 16 DefaultVisitorEmailLimitReplenish = time.Hour - DefaultVisitorSMSDailyLimit = 10 DefaultVisitorCallDailyLimit = 10 DefaultVisitorAccountCreationLimitBurst = 3 DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour @@ -130,7 +129,6 @@ type Config struct { VisitorMessageDailyLimit int VisitorEmailLimitBurst int VisitorEmailLimitReplenish time.Duration - VisitorSMSDailyLimit int VisitorCallDailyLimit int VisitorAccountCreationLimitBurst int VisitorAccountCreationLimitReplenish time.Duration diff --git a/server/errors.go b/server/errors.go index d02fb071..7c4e6899 100644 --- a/server/errors.go +++ b/server/errors.go @@ -106,14 +106,16 @@ var ( errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil} errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil} errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil} - errHTTPBadRequestTwilioDisabled = &errHTTP{40030, http.StatusBadRequest, "invalid request: SMS and calling is disabled", "https://ntfy.sh/docs/publish/#sms", nil} - errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40031, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#sms", nil} + errHTTPBadRequestTwilioDisabled = &errHTTP{40030, http.StatusBadRequest, "invalid request: Calling is disabled", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40031, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", "", nil} errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil} errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil} + errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil} + errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil} errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil} errHTTPEntityTooLargeJSONBody = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", "", nil} @@ -126,8 +128,7 @@ var ( errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil} errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit - errHTTPTooManyRequestsLimitSMS = &errHTTP{42910, http.StatusTooManyRequests, "limit reached: daily SMS quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} - errHTTPTooManyRequestsLimitCalls = &errHTTP{42911, http.StatusTooManyRequests, "limit reached: daily phone call quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} + errHTTPTooManyRequestsLimitCalls = &errHTTP{42910, http.StatusTooManyRequests, "limit reached: daily phone call quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil} diff --git a/server/server.go b/server/server.go index 056e0a6d..b474da7f 100644 --- a/server/server.go +++ b/server/server.go @@ -534,7 +534,6 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi EnableLogin: s.config.EnableLogin, EnableSignup: s.config.EnableSignup, EnablePayments: s.config.StripeSecretKey != "", - EnableSMS: s.config.TwilioAccount != "", EnableCalls: s.config.TwilioAccount != "", EnableReservations: s.config.EnableReservations, BillingContact: s.config.BillingContact, @@ -676,7 +675,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, sms, call, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -690,8 +689,6 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, errHTTPTooManyRequestsLimitMessages.With(t) } else if email != "" && !vrate.EmailAllowed() { return nil, errHTTPTooManyRequestsLimitEmails.With(t) - } else if sms != "" && !vrate.SMSAllowed() { - return nil, errHTTPTooManyRequestsLimitSMS.With(t) } else if call != "" && !vrate.CallAllowed() { return nil, errHTTPTooManyRequestsLimitCalls.With(t) } @@ -734,9 +731,6 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if s.smtpSender != nil && email != "" { go s.sendEmail(v, m, email) } - if s.config.TwilioAccount != "" && sms != "" { - go s.sendSMS(v, r, m, sms) - } if s.config.TwilioAccount != "" && call != "" { go s.callPhone(v, r, m, call) } @@ -849,7 +843,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } } -func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, sms, call string, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) { cache = readBoolParam(r, true, "x-cache", "cache") firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = maybeDecodeHeader(readParam(r, "x-title", "title", "t")) @@ -865,7 +859,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -883,25 +877,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid + return false, false, "", "", false, errHTTPBadRequestIconURLInvalid } m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if s.smtpSender == nil && email != "" { - return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled - } - sms = readParam(r, "x-sms", "sms") - if sms != "" && s.config.TwilioAccount == "" { - return false, false, "", "", "", false, errHTTPBadRequestTwilioDisabled - } else if sms != "" && !phoneNumberRegex.MatchString(sms) { - return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", false, errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && s.config.TwilioAccount == "" { - return false, false, "", "", "", false, errHTTPBadRequestTwilioDisabled + return false, false, "", "", false, errHTTPBadRequestTwilioDisabled } else if call != "" && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -910,7 +898,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi var e error m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) if e != nil { - return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid + return false, false, "", "", false, errHTTPBadRequestPriorityInvalid } m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") for i, t := range m.Tags { @@ -919,18 +907,18 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache + return false, false, "", "", false, errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse + return false, false, "", "", false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() { - return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() { - return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -938,7 +926,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! @@ -952,7 +940,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, sms, call, unifiedpush, nil + return cache, firebase, email, call, unifiedpush, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. diff --git a/server/server_account.go b/server/server_account.go index cb3a52ee..a323bfe0 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -56,7 +56,6 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis Messages: limits.MessageLimit, MessagesExpiryDuration: int64(limits.MessageExpiryDuration.Seconds()), Emails: limits.EmailLimit, - SMS: limits.SMSLimit, Calls: limits.CallLimit, Reservations: limits.ReservationsLimit, AttachmentTotalSize: limits.AttachmentTotalSizeLimit, @@ -69,8 +68,6 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis MessagesRemaining: stats.MessagesRemaining, Emails: stats.Emails, EmailsRemaining: stats.EmailsRemaining, - SMS: stats.SMS, - SMSRemaining: stats.SMSRemaining, Calls: stats.Calls, CallsRemaining: stats.CallsRemaining, Reservations: stats.Reservations, @@ -542,7 +539,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ // Check user is allowed to add phone numbers if u == nil || (u.IsUser() && u.Tier == nil) { return errHTTPUnauthorized - } else if u.IsUser() && u.Tier.SMSLimit == 0 && u.Tier.CallLimit == 0 { + } else if u.IsUser() && u.Tier.CallLimit == 0 { return errHTTPUnauthorized } // Actually add the unverified number, and send verification @@ -553,6 +550,9 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ }). Debug("Adding phone number, and sending verification") if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil { + if err == user.ErrPhoneNumberExists { + return errHTTPConflictPhoneNumberExists + } return err } if err := s.verifyPhone(v, r, req.Number); err != nil { @@ -570,10 +570,6 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R if !phoneNumberRegex.MatchString(req.Number) { return errHTTPBadRequestPhoneNumberInvalid } - // Check user is allowed to add phone numbers - if u == nil { - return errHTTPUnauthorized - } // Get phone numbers, and check if it's in the list phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) if err != nil { @@ -581,7 +577,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R } found := false for _, phoneNumber := range phoneNumbers { - if phoneNumber.Number == req.Number && phoneNumber.Verified { + if phoneNumber.Number == req.Number && !phoneNumber.Verified { found = true break } diff --git a/server/server_payments.go b/server/server_payments.go index bd91338e..1e98d059 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -68,7 +68,6 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ Messages: freeTier.MessageLimit, MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()), Emails: freeTier.EmailLimit, - SMS: freeTier.SMSLimit, Calls: freeTier.CallLimit, Reservations: freeTier.ReservationsLimit, AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit, @@ -98,7 +97,6 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ Messages: tier.MessageLimit, MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()), Emails: tier.EmailLimit, - SMS: tier.SMSLimit, Calls: tier.CallLimit, Reservations: tier.ReservationLimit, AttachmentTotalSize: tier.AttachmentTotalSizeLimit, diff --git a/server/server_twilio.go b/server/server_twilio.go index 2f587735..a6b91097 100644 --- a/server/server_twilio.go +++ b/server/server_twilio.go @@ -15,7 +15,6 @@ import ( ) const ( - twilioMessageEndpoint = "Messages.json" twilioMessageFooterFormat = "This message was sent by %s via %s" twilioCallEndpoint = "Calls.json" twilioCallFormat = ` @@ -32,15 +31,6 @@ const ( ` ) -func (s *Server) sendSMS(v *visitor, r *http.Request, m *message, to string) { - body := fmt.Sprintf("%s\n\n--\n%s", m.Message, s.messageFooter(v.User(), m)) - data := url.Values{} - data.Set("From", s.config.TwilioFromNumber) - data.Set("To", to) - data.Set("Body", body) - s.twilioMessagingRequest(v, r, m, metricSMSSentSuccess, metricSMSSentFailure, twilioMessageEndpoint, to, body, data) -} - func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) { body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(s.messageFooter(v.User(), m))) data := url.Values{} @@ -85,25 +75,38 @@ func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code data := url.Values{} data.Set("To", phoneNumber) data.Set("Code", code) - requestURL := fmt.Sprintf("%s/v2/Services/%s/VerificationCheck", s.config.TwilioVerifyBaseURL, s.config.TwilioAccount) + requestURL := fmt.Sprintf("%s/v2/Services/%s/VerificationCheck", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) 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") + 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 } else if resp.StatusCode != http.StatusOK { - return + if ev.IsTrace() { + response, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + ev.Field("twilio_response", string(response)) + } + ev.Warn("Twilio phone verification failed with status code %d", resp.StatusCode) + if resp.StatusCode == http.StatusNotFound { + return errHTTPGonePhoneVerificationExpired + } + return errHTTPInternalError } response, err := io.ReadAll(resp.Body) if err != nil { return err } - - ev := logvr(v, r).Tag(tagTwilio) if ev.IsTrace() { ev.Field("twilio_response", string(response)).Trace("Received successful Twilio phone verification response") } else if ev.IsDebug() { diff --git a/server/types.go b/server/types.go index 17622949..9015a006 100644 --- a/server/types.go +++ b/server/types.go @@ -362,7 +362,6 @@ type apiConfigResponse struct { EnableLogin bool `json:"enable_login"` EnableSignup bool `json:"enable_signup"` EnablePayments bool `json:"enable_payments"` - EnableSMS bool `json:"enable_sms"` EnableCalls bool `json:"enable_calls"` EnableReservations bool `json:"enable_reservations"` BillingContact string `json:"billing_contact"` diff --git a/server/visitor.go b/server/visitor.go index 4de51e67..4895c3f0 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -56,7 +56,6 @@ type visitor struct { requestLimiter *rate.Limiter // Rate limiter for (almost) all requests (including messages) messagesLimiter *util.FixedLimiter // Rate limiter for messages emailsLimiter *util.RateLimiter // Rate limiter for emails - smsLimiter *util.FixedLimiter // Rate limiter for SMS callsLimiter *util.FixedLimiter // Rate limiter for calls subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections) bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads @@ -81,7 +80,6 @@ type visitorLimits struct { EmailLimit int64 EmailLimitBurst int EmailLimitReplenish rate.Limit - SMSLimit int64 CallLimit int64 ReservationsLimit int64 AttachmentTotalSizeLimit int64 @@ -95,8 +93,6 @@ type visitorStats struct { MessagesRemaining int64 Emails int64 EmailsRemaining int64 - SMS int64 - SMSRemaining int64 Calls int64 CallsRemaining int64 Reservations int64 @@ -115,11 +111,10 @@ const ( ) func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Manager, ip netip.Addr, user *user.User) *visitor { - var messages, emails, sms, calls int64 + var messages, emails, calls int64 if user != nil { messages = user.Stats.Messages emails = user.Stats.Emails - sms = user.Stats.SMS calls = user.Stats.Calls } v := &visitor{ @@ -134,13 +129,12 @@ func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Mana requestLimiter: nil, // Set in resetLimiters messagesLimiter: nil, // Set in resetLimiters, may be nil emailsLimiter: nil, // Set in resetLimiters - smsLimiter: nil, // Set in resetLimiters, may be nil callsLimiter: nil, // Set in resetLimiters, may be nil bandwidthLimiter: nil, // Set in resetLimiters accountLimiter: nil, // Set in resetLimiters, may be nil authLimiter: nil, // Set in resetLimiters, may be nil } - v.resetLimitersNoLock(messages, emails, sms, calls, false) + v.resetLimitersNoLock(messages, emails, calls, false) return v } @@ -168,9 +162,6 @@ func (v *visitor) contextNoLock() log.Context { fields["visitor_emails_remaining"] = info.Stats.EmailsRemaining } if v.config.TwilioAccount != "" { - fields["visitor_sms"] = info.Stats.SMS - fields["visitor_sms_limit"] = info.Limits.SMSLimit - fields["visitor_sms_remaining"] = info.Stats.SMSRemaining fields["visitor_calls"] = info.Stats.Calls fields["visitor_calls_limit"] = info.Limits.CallLimit fields["visitor_calls_remaining"] = info.Stats.CallsRemaining @@ -238,12 +229,6 @@ func (v *visitor) EmailAllowed() bool { return v.emailsLimiter.Allow() } -func (v *visitor) SMSAllowed() bool { - v.mu.RLock() // limiters could be replaced! - defer v.mu.RUnlock() - return v.smsLimiter.Allow() -} - func (v *visitor) CallAllowed() bool { v.mu.RLock() // limiters could be replaced! defer v.mu.RUnlock() @@ -330,7 +315,6 @@ func (v *visitor) Stats() *user.Stats { return &user.Stats{ Messages: v.messagesLimiter.Value(), Emails: v.emailsLimiter.Value(), - SMS: v.smsLimiter.Value(), Calls: v.callsLimiter.Value(), } } @@ -340,7 +324,6 @@ func (v *visitor) ResetStats() { defer v.mu.RUnlock() v.emailsLimiter.Reset() v.messagesLimiter.Reset() - v.smsLimiter.Reset() v.callsLimiter.Reset() } @@ -372,11 +355,11 @@ func (v *visitor) SetUser(u *user.User) { shouldResetLimiters := v.user.TierID() != u.TierID() // TierID works with nil receiver v.user = u // u may be nil! if shouldResetLimiters { - var messages, emails, sms, calls int64 + var messages, emails, calls int64 if u != nil { - messages, emails, sms, calls = u.Stats.Messages, u.Stats.Emails, u.Stats.SMS, u.Stats.Calls + messages, emails, calls = u.Stats.Messages, u.Stats.Emails, u.Stats.Calls } - v.resetLimitersNoLock(messages, emails, sms, calls, true) + v.resetLimitersNoLock(messages, emails, calls, true) } } @@ -391,12 +374,11 @@ func (v *visitor) MaybeUserID() string { return "" } -func (v *visitor) resetLimitersNoLock(messages, emails, sms, calls int64, enqueueUpdate bool) { +func (v *visitor) resetLimitersNoLock(messages, emails, calls int64, enqueueUpdate bool) { limits := v.limitsNoLock() v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst) v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages) v.emailsLimiter = util.NewRateLimiterWithValue(limits.EmailLimitReplenish, limits.EmailLimitBurst, emails) - v.smsLimiter = util.NewFixedLimiterWithValue(limits.SMSLimit, sms) v.callsLimiter = util.NewFixedLimiterWithValue(limits.CallLimit, calls) v.bandwidthLimiter = util.NewBytesLimiter(int(limits.AttachmentBandwidthLimit), oneDay) if v.user == nil { @@ -410,7 +392,6 @@ func (v *visitor) resetLimitersNoLock(messages, emails, sms, calls int64, enqueu go v.userManager.EnqueueUserStats(v.user.ID, &user.Stats{ Messages: messages, Emails: emails, - SMS: sms, Calls: calls, }) } @@ -440,7 +421,6 @@ func tierBasedVisitorLimits(conf *Config, tier *user.Tier) *visitorLimits { EmailLimit: tier.EmailLimit, EmailLimitBurst: util.MinMax(int(float64(tier.EmailLimit)*visitorEmailLimitBurstRate), conf.VisitorEmailLimitBurst, visitorEmailLimitBurstMax), EmailLimitReplenish: dailyLimitToRate(tier.EmailLimit), - SMSLimit: tier.SMSLimit, CallLimit: tier.CallLimit, ReservationsLimit: tier.ReservationLimit, AttachmentTotalSizeLimit: tier.AttachmentTotalSizeLimit, @@ -464,7 +444,6 @@ func configBasedVisitorLimits(conf *Config) *visitorLimits { EmailLimit: replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish), // Approximation! EmailLimitBurst: conf.VisitorEmailLimitBurst, EmailLimitReplenish: rate.Every(conf.VisitorEmailLimitReplenish), - SMSLimit: int64(conf.VisitorSMSDailyLimit), CallLimit: int64(conf.VisitorCallDailyLimit), ReservationsLimit: visitorDefaultReservationsLimit, AttachmentTotalSizeLimit: conf.VisitorAttachmentTotalSizeLimit, @@ -511,7 +490,6 @@ func (v *visitor) Info() (*visitorInfo, error) { func (v *visitor) infoLightNoLock() *visitorInfo { messages := v.messagesLimiter.Value() emails := v.emailsLimiter.Value() - sms := v.smsLimiter.Value() calls := v.callsLimiter.Value() limits := v.limitsNoLock() stats := &visitorStats{ @@ -519,8 +497,6 @@ func (v *visitor) infoLightNoLock() *visitorInfo { MessagesRemaining: zeroIfNegative(limits.MessageLimit - messages), Emails: emails, EmailsRemaining: zeroIfNegative(limits.EmailLimit - emails), - SMS: sms, - SMSRemaining: zeroIfNegative(limits.SMSLimit - sms), Calls: calls, CallsRemaining: zeroIfNegative(limits.CallLimit - calls), } diff --git a/user/manager.go b/user/manager.go index 824622ba..7c179cf5 100644 --- a/user/manager.go +++ b/user/manager.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3" // SQLite driver "github.com/stripe/stripe-go/v74" "golang.org/x/crypto/bcrypt" @@ -55,7 +56,6 @@ const ( messages_limit INT NOT NULL, messages_expiry_duration INT NOT NULL, emails_limit INT NOT NULL, - sms_limit INT NOT NULL, calls_limit INT NOT NULL, reservations_limit INT NOT NULL, attachment_file_size_limit INT NOT NULL, @@ -78,7 +78,6 @@ const ( sync_topic TEXT NOT NULL, stats_messages INT NOT NULL DEFAULT (0), stats_emails INT NOT NULL DEFAULT (0), - stats_sms INT NOT NULL DEFAULT (0), stats_calls INT NOT NULL DEFAULT (0), stripe_customer_id TEXT, stripe_subscription_id TEXT, @@ -135,26 +134,26 @@ const ( ` selectUserByIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.id = ? ` selectUserByNameQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE user = ? ` selectUserByTokenQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u JOIN user_token tk on u.id = tk.user_id LEFT JOIN tier t on t.id = u.tier_id WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?) ` selectUserByStripeCustomerIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.stripe_customer_id = ? @@ -185,8 +184,8 @@ const ( updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?` updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?` updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?` - updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_sms = ?, stats_calls = ? WHERE id = ?` - updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_sms = 0, stats_calls = 0` + updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_calls = ? WHERE id = ?` + updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_calls = 0` updateUserDeletedQuery = `UPDATE user SET deleted = ? WHERE id = ?` deleteUsersMarkedQuery = `DELETE FROM user WHERE deleted < ?` deleteUserQuery = `DELETE FROM user WHERE user = ?` @@ -274,25 +273,25 @@ const ( updatePhoneNumberVerifiedQuery = `UPDATE user_phone SET verified=1 WHERE user_id = ? AND phone_number = ?` insertTierQuery = ` - INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, sms_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` updateTierQuery = ` UPDATE tier - SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, sms_limit = ?, calls_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_monthly_price_id = ?, stripe_yearly_price_id = ? + SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, calls_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_monthly_price_id = ?, stripe_yearly_price_id = ? WHERE code = ? ` selectTiersQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, sms_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier ` selectTierByCodeQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, sms_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier WHERE code = ? ` selectTierByPriceIDQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, sms_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier WHERE (stripe_monthly_price_id = ? OR stripe_yearly_price_id = ?) ` @@ -410,9 +409,7 @@ const ( // 3 -> 4 migrate3To4UpdateQueries = ` - ALTER TABLE tier ADD COLUMN sms_limit INT NOT NULL DEFAULT (0); ALTER TABLE tier ADD COLUMN calls_limit INT NOT NULL DEFAULT (0); - ALTER TABLE user ADD COLUMN stats_sms INT NOT NULL DEFAULT (0); ALTER TABLE user ADD COLUMN stats_calls INT NOT NULL DEFAULT (0); CREATE TABLE IF NOT EXISTS user_phone ( user_id TEXT NOT NULL, @@ -689,6 +686,9 @@ func (a *Manager) readPhoneNumber(rows *sql.Rows) (*PhoneNumber, error) { func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil { + if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + return ErrPhoneNumberExists + } return err } return nil @@ -783,11 +783,10 @@ func (a *Manager) writeUserStatsQueue() error { "user_id": userID, "messages_count": update.Messages, "emails_count": update.Emails, - "sms_count": update.SMS, "calls_count": update.Calls, }). Trace("Updating stats for user %s", userID) - if _, err := tx.Exec(updateUserStatsQuery, update.Messages, update.Emails, update.SMS, update.Calls, userID); err != nil { + if _, err := tx.Exec(updateUserStatsQuery, update.Messages, update.Emails, update.Calls, userID); err != nil { return err } } @@ -869,6 +868,9 @@ func (a *Manager) AddUser(username, password string, role Role) error { userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { + if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + return ErrUserExists + } return err } return nil @@ -996,12 +998,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { defer rows.Close() var id, username, hash, role, prefs, syncTopic string var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString - var messages, emails, sms, calls int64 - var messagesLimit, messagesExpiryDuration, emailsLimit, smsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 + var messages, emails, calls int64 + var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 if !rows.Next() { return nil, ErrUserNotFound } - if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &sms, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &smsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { + if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -1016,7 +1018,6 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { Stats: &Stats{ Messages: messages, Emails: emails, - SMS: sms, Calls: calls, }, Billing: &Billing{ @@ -1041,7 +1042,6 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { MessageLimit: messagesLimit.Int64, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, EmailLimit: emailsLimit.Int64, - SMSLimit: smsLimit.Int64, CallLimit: callsLimit.Int64, ReservationLimit: reservationsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, @@ -1348,7 +1348,7 @@ func (a *Manager) AddTier(tier *Tier) error { if tier.ID == "" { tier.ID = util.RandomStringPrefix(tierIDPrefix, tierIDLength) } - if _, err := a.db.Exec(insertTierQuery, tier.ID, tier.Code, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.SMSLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID)); err != nil { + if _, err := a.db.Exec(insertTierQuery, tier.ID, tier.Code, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID)); err != nil { return err } return nil @@ -1356,7 +1356,7 @@ func (a *Manager) AddTier(tier *Tier) error { // UpdateTier updates a tier's properties in the database func (a *Manager) UpdateTier(tier *Tier) error { - if _, err := a.db.Exec(updateTierQuery, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.SMSLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID), tier.Code); err != nil { + if _, err := a.db.Exec(updateTierQuery, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID), tier.Code); err != nil { return err } return nil @@ -1425,11 +1425,11 @@ func (a *Manager) TierByStripePrice(priceID string) (*Tier, error) { func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { var id, code, name string var stripeMonthlyPriceID, stripeYearlyPriceID sql.NullString - var messagesLimit, messagesExpiryDuration, emailsLimit, smsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit sql.NullInt64 + var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit sql.NullInt64 if !rows.Next() { return nil, ErrTierNotFound } - if err := rows.Scan(&id, &code, &name, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &smsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { + if err := rows.Scan(&id, &code, &name, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -1442,7 +1442,6 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { MessageLimit: messagesLimit.Int64, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, EmailLimit: emailsLimit.Int64, - SMSLimit: smsLimit.Int64, CallLimit: callsLimit.Int64, ReservationLimit: reservationsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, diff --git a/user/types.go b/user/types.go index 8f579e89..51a2b3f3 100644 --- a/user/types.go +++ b/user/types.go @@ -91,7 +91,6 @@ type Tier struct { MessageLimit int64 // Daily message limit MessageExpiryDuration time.Duration // Cache duration for messages EmailLimit int64 // Daily email limit - SMSLimit int64 // Daily SMS limit CallLimit int64 // Daily phone call limit ReservationLimit int64 // Number of topic reservations allowed by user AttachmentFileSizeLimit int64 // Max file size per file (bytes) @@ -138,7 +137,6 @@ type NotificationPrefs struct { type Stats struct { Messages int64 Emails int64 - SMS int64 Calls int64 } @@ -285,8 +283,10 @@ var ( ErrUnauthorized = errors.New("unauthorized") ErrInvalidArgument = errors.New("invalid argument") ErrUserNotFound = errors.New("user not found") + ErrUserExists = errors.New("user already exists") ErrTierNotFound = errors.New("tier not found") ErrTokenNotFound = errors.New("token not found") ErrPhoneNumberNotFound = errors.New("phone number not found") ErrTooManyReservations = errors.New("new tier has lower reservation limit") + ErrPhoneNumberExists = errors.New("phone number already exists") ) diff --git a/web/public/config.js b/web/public/config.js index f5a5759c..b49e440b 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -12,7 +12,6 @@ var config = { enable_signup: true, enable_payments: true, enable_reservations: true, - enable_sms: true, enable_calls: true, billing_contact: "", disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 600994bb..86330f14 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -127,9 +127,6 @@ "publish_dialog_email_label": "Email", "publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com", "publish_dialog_email_reset": "Remove email forward", - "publish_dialog_sms_label": "SMS", - "publish_dialog_sms_placeholder": "Phone number to send SMS to, e.g. +12223334444", - "publish_dialog_sms_reset": "Remove SMS message", "publish_dialog_call_label": "Phone call", "publish_dialog_call_placeholder": "Phone number to call with the message, e.g. +12223334444", "publish_dialog_call_reset": "Remove phone call", @@ -144,7 +141,6 @@ "publish_dialog_other_features": "Other features:", "publish_dialog_chip_click_label": "Click URL", "publish_dialog_chip_email_label": "Forward to email", - "publish_dialog_chip_sms_label": "Send SMS", "publish_dialog_chip_call_label": "Phone call", "publish_dialog_chip_attach_url_label": "Attach file by URL", "publish_dialog_chip_attach_file_label": "Attach local file", @@ -190,6 +186,8 @@ "account_basics_password_dialog_confirm_password_label": "Confirm password", "account_basics_password_dialog_button_submit": "Change password", "account_basics_password_dialog_current_password_incorrect": "Password incorrect", + "account_basics_phone_numbers_title": "Phone numbers", + "account_basics_phone_numbers_description": "For phone call notifications", "account_usage_title": "Usage", "account_usage_of_limit": "of {{limit}}", "account_usage_unlimited": "Unlimited", @@ -211,8 +209,6 @@ "account_basics_tier_manage_billing_button": "Manage billing", "account_usage_messages_title": "Published messages", "account_usage_emails_title": "Emails sent", - "account_usage_sms_title": "SMS sent", - "account_usage_sms_none": "No SMS can be sent with this account", "account_usage_calls_title": "Phone calls made", "account_usage_calls_none": "No phone calls can be made with this account", "account_usage_reservations_title": "Reserved topics", @@ -244,9 +240,6 @@ "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daily messages", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} daily emails", - "account_upgrade_dialog_tier_features_sms_one": "{{sms}} daily SMS", - "account_upgrade_dialog_tier_features_sms_other": "{{sms}} daily SMS", - "account_upgrade_dialog_tier_features_no_sms": "No daily SMS", "account_upgrade_dialog_tier_features_calls_one": "{{calls}} daily phone calls", "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daily phone calls", "account_upgrade_dialog_tier_features_no_calls": "No daily phone calls", diff --git a/web/src/components/Account.js b/web/src/components/Account.js index dc80babf..b5294cd5 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -3,7 +3,7 @@ import {useContext, useState} from 'react'; import { Alert, CardActions, - CardContent, + CardContent, Chip, FormControl, LinearProgress, Link, @@ -52,6 +52,7 @@ import MenuItem from "@mui/material/MenuItem"; import DialogContentText from "@mui/material/DialogContentText"; import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; import {ProChip} from "./SubscriptionPopup"; +import AddIcon from "@mui/icons-material/Add"; const Account = () => { if (!session.exists()) { @@ -80,6 +81,7 @@ const Basics = () => { + @@ -320,6 +322,40 @@ const AccountType = () => { ) }; +const PhoneNumbers = () => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const labelId = "prefPhoneNumbers"; + + const handleAdd = () => { + + }; + + const handleClick = () => { + + }; + + const handleDelete = () => { + + }; + + return ( + +
+ {account?.phone_numbers.map(p => + navigator.clipboard.writeText(p.number)} + onDelete={() => handleDelete(p.number)} + /> + )} + handleAdd()}> +
+
+ ) +}; + const Stats = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); @@ -380,23 +416,6 @@ const Stats = () => { value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100} /> - {(account.role === Role.ADMIN || account.limits.sms > 0) && - - {t("account_usage_sms_title")} - - - }> -
- {account.stats.sms.toLocaleString()} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.sms.toLocaleString() }) : t("account_usage_unlimited")} -
- 0 ? normalize(account.stats.sms, account.limits.sms) : 100} - /> -
- } {(account.role === Role.ADMIN || account.limits.calls > 0) && @@ -410,7 +429,7 @@ const Stats = () => { 0 ? normalize(account.stats.calls, account.limits.calls) : 100} + value={account.role === Role.USER && account.limits.calls > 0 ? normalize(account.stats.calls, account.limits.calls) : 100} /> } @@ -439,11 +458,6 @@ const Stats = () => { {t("account_usage_reservations_none")} } - {config.enable_sms && account.role === Role.USER && account.limits.sms === 0 && - {t("account_usage_sms_title")}{config.enable_payments && }}> - {t("account_usage_sms_none")} - - } {config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && {t("account_usage_calls_title")}{config.enable_payments && }}> {t("account_usage_calls_none")}