WIP calls, remove SMS

This commit is contained in:
binwiederhier 2023-05-12 20:01:12 -04:00
parent d4767caf30
commit f99159ee5b
16 changed files with 132 additions and 301 deletions

View file

@ -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-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-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: "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-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 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"}), 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.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-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-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.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.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"}), 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") visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst") visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish") visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
visitorSMSDailyLimit := c.Int("visitor-sms-daily-limit")
visitorCallDailyLimit := c.Int("visitor-call-daily-limit") visitorCallDailyLimit := c.Int("visitor-call-daily-limit")
behindProxy := c.Bool("behind-proxy") behindProxy := c.Bool("behind-proxy")
stripeSecretKey := c.String("stripe-secret-key") stripeSecretKey := c.String("stripe-secret-key")
@ -336,7 +334,6 @@ func execServe(c *cli.Context) error {
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
conf.VisitorSMSDailyLimit = visitorSMSDailyLimit
conf.VisitorCallDailyLimit = visitorCallDailyLimit conf.VisitorCallDailyLimit = visitorCallDailyLimit
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
conf.BehindProxy = behindProxy conf.BehindProxy = behindProxy

View file

@ -18,7 +18,6 @@ const (
defaultMessageLimit = 5000 defaultMessageLimit = 5000
defaultMessageExpiryDuration = "12h" defaultMessageExpiryDuration = "12h"
defaultEmailLimit = 20 defaultEmailLimit = 20
defaultSMSLimit = 10
defaultCallLimit = 10 defaultCallLimit = 10
defaultReservationLimit = 3 defaultReservationLimit = 3
defaultAttachmentFileSizeLimit = "15M" defaultAttachmentFileSizeLimit = "15M"
@ -50,7 +49,6 @@ var cmdTier = &cli.Command{
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"}, &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.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: "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: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"},
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation 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"}, &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.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
&cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"}, &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: "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: "call-limit", Usage: "daily phone call limit"},
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"}, &cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size 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"), MessageLimit: c.Int64("message-limit"),
MessageExpiryDuration: messageExpiryDuration, MessageExpiryDuration: messageExpiryDuration,
EmailLimit: c.Int64("email-limit"), EmailLimit: c.Int64("email-limit"),
SMSLimit: c.Int64("sms-limit"),
CallLimit: c.Int64("call-limit"), CallLimit: c.Int64("call-limit"),
ReservationLimit: c.Int64("reservation-limit"), ReservationLimit: c.Int64("reservation-limit"),
AttachmentFileSizeLimit: attachmentFileSizeLimit, AttachmentFileSizeLimit: attachmentFileSizeLimit,
@ -275,9 +271,6 @@ func execTierChange(c *cli.Context) error {
if c.IsSet("email-limit") { if c.IsSet("email-limit") {
tier.EmailLimit = c.Int64("email-limit") tier.EmailLimit = c.Int64("email-limit")
} }
if c.IsSet("sms-limit") {
tier.SMSLimit = c.Int64("sms-limit")
}
if c.IsSet("call-limit") { if c.IsSet("call-limit") {
tier.CallLimit = c.Int64("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 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, "- 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, "- 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, "- Phone call limit: %d\n", tier.CallLimit)
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit) 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)) fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))

View file

@ -2695,51 +2695,48 @@ title `You've Got Mail` to topic `sometopic` (see [ntfy.sh/sometopic](https://nt
<figcaption>Publishing a message via e-mail</figcaption> <figcaption>Publishing a message via e-mail</figcaption>
</figure> </figure>
## Text message (SMS) ## Phone calls
_Supported on:_ :material-android: :material-apple: :material-firefox: _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, 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.
this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have the ntfy app Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have
installed on their phone. 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 Phone numbers have to be previously verified (via the web app). To forward a message as a phone call, pass a phone number
and the country code, e.g. `+12223334444`. 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. On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans.
=== "Command line (curl)" === "Command line (curl)"
``` ```
curl \ curl \
-H "SMS: +12223334444" \ -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." \ -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.sh/alerts
``` ```
=== "ntfy CLI" === "ntfy CLI"
``` ```
ntfy publish \ ntfy publish \
--email=phil@example.com \ --call=+12223334444 \
--tags=warning,skull,backup-host,ssh-login \ 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."
--priority=high \
alerts "Unknown login from 5.31.23.83 to backups.example.com"
``` ```
=== "HTTP" === "HTTP"
``` http ``` http
POST /alerts HTTP/1.1 POST /alerts HTTP/1.1
Host: ntfy.sh Host: ntfy.sh
Email: phil@example.com Call: +12223334444
Tags: warning,skull,backup-host,ssh-login
Priority: high
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"
``` javascript ``` javascript
fetch('https://ntfy.sh/alerts', { fetch('https://ntfy.sh/alerts', {
method: 'POST', 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: { headers: {
'Email': 'phil@example.com', 'Email': 'phil@example.com',
'Tags': 'warning,skull,backup-host,ssh-login', 'Tags': 'warning,skull,backup-host,ssh-login',
@ -2807,125 +2804,6 @@ Here's what that looks like in Google Mail:
<figcaption>E-mail notification</figcaption> <figcaption>E-mail notification</figcaption>
</figure> </figure>
## 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:
<figure markdown>
![e-mail notification](static/img/screenshot-email.png){ width=600 }
<figcaption>E-mail notification</figcaption>
</figure>
## Authentication ## Authentication
Depending on whether the server is configured to support [access control](config.md#access-control), some topics 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. may be read/write protected so that only users with the correct credentials can subscribe or publish to them.

View file

@ -47,7 +47,6 @@ const (
DefaultVisitorMessageDailyLimit = 0 DefaultVisitorMessageDailyLimit = 0
DefaultVisitorEmailLimitBurst = 16 DefaultVisitorEmailLimitBurst = 16
DefaultVisitorEmailLimitReplenish = time.Hour DefaultVisitorEmailLimitReplenish = time.Hour
DefaultVisitorSMSDailyLimit = 10
DefaultVisitorCallDailyLimit = 10 DefaultVisitorCallDailyLimit = 10
DefaultVisitorAccountCreationLimitBurst = 3 DefaultVisitorAccountCreationLimitBurst = 3
DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
@ -130,7 +129,6 @@ type Config struct {
VisitorMessageDailyLimit int VisitorMessageDailyLimit int
VisitorEmailLimitBurst int VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration VisitorEmailLimitReplenish time.Duration
VisitorSMSDailyLimit int
VisitorCallDailyLimit int VisitorCallDailyLimit int
VisitorAccountCreationLimitBurst int VisitorAccountCreationLimitBurst int
VisitorAccountCreationLimitReplenish time.Duration VisitorAccountCreationLimitReplenish time.Duration

View file

@ -106,14 +106,16 @@ var (
errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil} 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} 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} 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} 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/#sms", 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} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", 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} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", "", 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} 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} 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} 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} 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} 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} 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} 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 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{42910, http.StatusTooManyRequests, "limit reached: daily phone call 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}
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil}
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", 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} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil}

View file

@ -534,7 +534,6 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
EnableLogin: s.config.EnableLogin, EnableLogin: s.config.EnableLogin,
EnableSignup: s.config.EnableSignup, EnableSignup: s.config.EnableSignup,
EnablePayments: s.config.StripeSecretKey != "", EnablePayments: s.config.StripeSecretKey != "",
EnableSMS: s.config.TwilioAccount != "",
EnableCalls: s.config.TwilioAccount != "", EnableCalls: s.config.TwilioAccount != "",
EnableReservations: s.config.EnableReservations, EnableReservations: s.config.EnableReservations,
BillingContact: s.config.BillingContact, BillingContact: s.config.BillingContact,
@ -676,7 +675,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
return nil, err return nil, err
} }
m := newDefaultMessage(t.ID, "") 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 { if e != nil {
return nil, e.With(t) 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) return nil, errHTTPTooManyRequestsLimitMessages.With(t)
} else if email != "" && !vrate.EmailAllowed() { } else if email != "" && !vrate.EmailAllowed() {
return nil, errHTTPTooManyRequestsLimitEmails.With(t) return nil, errHTTPTooManyRequestsLimitEmails.With(t)
} else if sms != "" && !vrate.SMSAllowed() {
return nil, errHTTPTooManyRequestsLimitSMS.With(t)
} else if call != "" && !vrate.CallAllowed() { } else if call != "" && !vrate.CallAllowed() {
return nil, errHTTPTooManyRequestsLimitCalls.With(t) 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 != "" { if s.smtpSender != nil && email != "" {
go s.sendEmail(v, m, email) go s.sendEmail(v, m, email)
} }
if s.config.TwilioAccount != "" && sms != "" {
go s.sendSMS(v, r, m, sms)
}
if s.config.TwilioAccount != "" && call != "" { if s.config.TwilioAccount != "" && call != "" {
go s.callPhone(v, r, m, 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") cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase") firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = maybeDecodeHeader(readParam(r, "x-title", "title", "t")) 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 attach != "" {
if !urlRegex.MatchString(attach) { if !urlRegex.MatchString(attach) {
return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
} }
m.Attachment.URL = attach m.Attachment.URL = attach
if m.Attachment.Name == "" { if m.Attachment.Name == "" {
@ -883,25 +877,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
} }
if icon != "" { if icon != "" {
if !urlRegex.MatchString(icon) { if !urlRegex.MatchString(icon) {
return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid return false, false, "", "", false, errHTTPBadRequestIconURLInvalid
} }
m.Icon = icon m.Icon = icon
} }
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if s.smtpSender == nil && email != "" { if s.smtpSender == nil && email != "" {
return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled 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
} }
call = readParam(r, "x-call", "call") call = readParam(r, "x-call", "call")
if call != "" && s.config.TwilioAccount == "" { if call != "" && s.config.TwilioAccount == "" {
return false, false, "", "", "", false, errHTTPBadRequestTwilioDisabled return false, false, "", "", false, errHTTPBadRequestTwilioDisabled
} else if call != "" && !phoneNumberRegex.MatchString(call) { } 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") messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" { if messageStr != "" {
@ -910,7 +898,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
var e error var e error
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if e != nil { if e != nil {
return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
} }
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
for i, t := range m.Tags { 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") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
if delayStr != "" { if delayStr != "" {
if !cache { if !cache {
return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache return false, false, "", "", false, errHTTPBadRequestDelayNoCache
} }
if email != "" { 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()) delay, err := util.ParseFutureTime(delayStr, time.Now())
if err != nil { if err != nil {
return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() { } 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() { } 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() m.Time = delay.Unix()
} }
@ -938,7 +926,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if actionsStr != "" { if actionsStr != "" {
m.Actions, e = parseActions(actionsStr) m.Actions, e = parseActions(actionsStr)
if e != nil { 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! 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 cache = false
email = "" 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. // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.

View file

@ -56,7 +56,6 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
Messages: limits.MessageLimit, Messages: limits.MessageLimit,
MessagesExpiryDuration: int64(limits.MessageExpiryDuration.Seconds()), MessagesExpiryDuration: int64(limits.MessageExpiryDuration.Seconds()),
Emails: limits.EmailLimit, Emails: limits.EmailLimit,
SMS: limits.SMSLimit,
Calls: limits.CallLimit, Calls: limits.CallLimit,
Reservations: limits.ReservationsLimit, Reservations: limits.ReservationsLimit,
AttachmentTotalSize: limits.AttachmentTotalSizeLimit, AttachmentTotalSize: limits.AttachmentTotalSizeLimit,
@ -69,8 +68,6 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
MessagesRemaining: stats.MessagesRemaining, MessagesRemaining: stats.MessagesRemaining,
Emails: stats.Emails, Emails: stats.Emails,
EmailsRemaining: stats.EmailsRemaining, EmailsRemaining: stats.EmailsRemaining,
SMS: stats.SMS,
SMSRemaining: stats.SMSRemaining,
Calls: stats.Calls, Calls: stats.Calls,
CallsRemaining: stats.CallsRemaining, CallsRemaining: stats.CallsRemaining,
Reservations: stats.Reservations, 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 // Check user is allowed to add phone numbers
if u == nil || (u.IsUser() && u.Tier == nil) { if u == nil || (u.IsUser() && u.Tier == nil) {
return errHTTPUnauthorized return errHTTPUnauthorized
} else if u.IsUser() && u.Tier.SMSLimit == 0 && u.Tier.CallLimit == 0 { } else if u.IsUser() && u.Tier.CallLimit == 0 {
return errHTTPUnauthorized return errHTTPUnauthorized
} }
// Actually add the unverified number, and send verification // 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") Debug("Adding phone number, and sending verification")
if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil { if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil {
if err == user.ErrPhoneNumberExists {
return errHTTPConflictPhoneNumberExists
}
return err return err
} }
if err := s.verifyPhone(v, r, req.Number); err != nil { 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) { if !phoneNumberRegex.MatchString(req.Number) {
return errHTTPBadRequestPhoneNumberInvalid 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 // Get phone numbers, and check if it's in the list
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
if err != nil { if err != nil {
@ -581,7 +577,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
} }
found := false found := false
for _, phoneNumber := range phoneNumbers { for _, phoneNumber := range phoneNumbers {
if phoneNumber.Number == req.Number && phoneNumber.Verified { if phoneNumber.Number == req.Number && !phoneNumber.Verified {
found = true found = true
break break
} }

View file

@ -68,7 +68,6 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _
Messages: freeTier.MessageLimit, Messages: freeTier.MessageLimit,
MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()), MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()),
Emails: freeTier.EmailLimit, Emails: freeTier.EmailLimit,
SMS: freeTier.SMSLimit,
Calls: freeTier.CallLimit, Calls: freeTier.CallLimit,
Reservations: freeTier.ReservationsLimit, Reservations: freeTier.ReservationsLimit,
AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit, AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit,
@ -98,7 +97,6 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _
Messages: tier.MessageLimit, Messages: tier.MessageLimit,
MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()), MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()),
Emails: tier.EmailLimit, Emails: tier.EmailLimit,
SMS: tier.SMSLimit,
Calls: tier.CallLimit, Calls: tier.CallLimit,
Reservations: tier.ReservationLimit, Reservations: tier.ReservationLimit,
AttachmentTotalSize: tier.AttachmentTotalSizeLimit, AttachmentTotalSize: tier.AttachmentTotalSizeLimit,

View file

@ -15,7 +15,6 @@ import (
) )
const ( const (
twilioMessageEndpoint = "Messages.json"
twilioMessageFooterFormat = "This message was sent by %s via %s" twilioMessageFooterFormat = "This message was sent by %s via %s"
twilioCallEndpoint = "Calls.json" twilioCallEndpoint = "Calls.json"
twilioCallFormat = ` twilioCallFormat = `
@ -32,15 +31,6 @@ const (
</Response>` </Response>`
) )
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) { 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))) body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(s.messageFooter(v.User(), m)))
data := url.Values{} data := url.Values{}
@ -85,25 +75,38 @@ func (s *Server) checkVerifyPhone(v *visitor, r *http.Request, phoneNumber, code
data := url.Values{} data := url.Values{}
data.Set("To", phoneNumber) data.Set("To", phoneNumber)
data.Set("Code", code) 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())) req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode()))
if err != nil { if err != nil {
return err return err
} }
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
} else if resp.StatusCode != http.StatusOK { } 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) response, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
ev := logvr(v, r).Tag(tagTwilio)
if ev.IsTrace() { if ev.IsTrace() {
ev.Field("twilio_response", string(response)).Trace("Received successful Twilio phone verification response") ev.Field("twilio_response", string(response)).Trace("Received successful Twilio phone verification response")
} else if ev.IsDebug() { } else if ev.IsDebug() {

View file

@ -362,7 +362,6 @@ type apiConfigResponse struct {
EnableLogin bool `json:"enable_login"` EnableLogin bool `json:"enable_login"`
EnableSignup bool `json:"enable_signup"` EnableSignup bool `json:"enable_signup"`
EnablePayments bool `json:"enable_payments"` EnablePayments bool `json:"enable_payments"`
EnableSMS bool `json:"enable_sms"`
EnableCalls bool `json:"enable_calls"` EnableCalls bool `json:"enable_calls"`
EnableReservations bool `json:"enable_reservations"` EnableReservations bool `json:"enable_reservations"`
BillingContact string `json:"billing_contact"` BillingContact string `json:"billing_contact"`

View file

@ -56,7 +56,6 @@ type visitor struct {
requestLimiter *rate.Limiter // Rate limiter for (almost) all requests (including messages) requestLimiter *rate.Limiter // Rate limiter for (almost) all requests (including messages)
messagesLimiter *util.FixedLimiter // Rate limiter for messages messagesLimiter *util.FixedLimiter // Rate limiter for messages
emailsLimiter *util.RateLimiter // Rate limiter for emails emailsLimiter *util.RateLimiter // Rate limiter for emails
smsLimiter *util.FixedLimiter // Rate limiter for SMS
callsLimiter *util.FixedLimiter // Rate limiter for calls callsLimiter *util.FixedLimiter // Rate limiter for calls
subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections) subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections)
bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads
@ -81,7 +80,6 @@ type visitorLimits struct {
EmailLimit int64 EmailLimit int64
EmailLimitBurst int EmailLimitBurst int
EmailLimitReplenish rate.Limit EmailLimitReplenish rate.Limit
SMSLimit int64
CallLimit int64 CallLimit int64
ReservationsLimit int64 ReservationsLimit int64
AttachmentTotalSizeLimit int64 AttachmentTotalSizeLimit int64
@ -95,8 +93,6 @@ type visitorStats struct {
MessagesRemaining int64 MessagesRemaining int64
Emails int64 Emails int64
EmailsRemaining int64 EmailsRemaining int64
SMS int64
SMSRemaining int64
Calls int64 Calls int64
CallsRemaining int64 CallsRemaining int64
Reservations int64 Reservations int64
@ -115,11 +111,10 @@ const (
) )
func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Manager, ip netip.Addr, user *user.User) *visitor { 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 { if user != nil {
messages = user.Stats.Messages messages = user.Stats.Messages
emails = user.Stats.Emails emails = user.Stats.Emails
sms = user.Stats.SMS
calls = user.Stats.Calls calls = user.Stats.Calls
} }
v := &visitor{ v := &visitor{
@ -134,13 +129,12 @@ func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Mana
requestLimiter: nil, // Set in resetLimiters requestLimiter: nil, // Set in resetLimiters
messagesLimiter: nil, // Set in resetLimiters, may be nil messagesLimiter: nil, // Set in resetLimiters, may be nil
emailsLimiter: nil, // Set in resetLimiters emailsLimiter: nil, // Set in resetLimiters
smsLimiter: nil, // Set in resetLimiters, may be nil
callsLimiter: nil, // Set in resetLimiters, may be nil callsLimiter: nil, // Set in resetLimiters, may be nil
bandwidthLimiter: nil, // Set in resetLimiters bandwidthLimiter: nil, // Set in resetLimiters
accountLimiter: nil, // Set in resetLimiters, may be nil accountLimiter: nil, // Set in resetLimiters, may be nil
authLimiter: 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 return v
} }
@ -168,9 +162,6 @@ func (v *visitor) contextNoLock() log.Context {
fields["visitor_emails_remaining"] = info.Stats.EmailsRemaining fields["visitor_emails_remaining"] = info.Stats.EmailsRemaining
} }
if v.config.TwilioAccount != "" { 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"] = info.Stats.Calls
fields["visitor_calls_limit"] = info.Limits.CallLimit fields["visitor_calls_limit"] = info.Limits.CallLimit
fields["visitor_calls_remaining"] = info.Stats.CallsRemaining fields["visitor_calls_remaining"] = info.Stats.CallsRemaining
@ -238,12 +229,6 @@ func (v *visitor) EmailAllowed() bool {
return v.emailsLimiter.Allow() 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 { func (v *visitor) CallAllowed() bool {
v.mu.RLock() // limiters could be replaced! v.mu.RLock() // limiters could be replaced!
defer v.mu.RUnlock() defer v.mu.RUnlock()
@ -330,7 +315,6 @@ func (v *visitor) Stats() *user.Stats {
return &user.Stats{ return &user.Stats{
Messages: v.messagesLimiter.Value(), Messages: v.messagesLimiter.Value(),
Emails: v.emailsLimiter.Value(), Emails: v.emailsLimiter.Value(),
SMS: v.smsLimiter.Value(),
Calls: v.callsLimiter.Value(), Calls: v.callsLimiter.Value(),
} }
} }
@ -340,7 +324,6 @@ func (v *visitor) ResetStats() {
defer v.mu.RUnlock() defer v.mu.RUnlock()
v.emailsLimiter.Reset() v.emailsLimiter.Reset()
v.messagesLimiter.Reset() v.messagesLimiter.Reset()
v.smsLimiter.Reset()
v.callsLimiter.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 shouldResetLimiters := v.user.TierID() != u.TierID() // TierID works with nil receiver
v.user = u // u may be nil! v.user = u // u may be nil!
if shouldResetLimiters { if shouldResetLimiters {
var messages, emails, sms, calls int64 var messages, emails, calls int64
if u != nil { 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 "" 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() limits := v.limitsNoLock()
v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst) v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst)
v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages) v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages)
v.emailsLimiter = util.NewRateLimiterWithValue(limits.EmailLimitReplenish, limits.EmailLimitBurst, emails) v.emailsLimiter = util.NewRateLimiterWithValue(limits.EmailLimitReplenish, limits.EmailLimitBurst, emails)
v.smsLimiter = util.NewFixedLimiterWithValue(limits.SMSLimit, sms)
v.callsLimiter = util.NewFixedLimiterWithValue(limits.CallLimit, calls) v.callsLimiter = util.NewFixedLimiterWithValue(limits.CallLimit, calls)
v.bandwidthLimiter = util.NewBytesLimiter(int(limits.AttachmentBandwidthLimit), oneDay) v.bandwidthLimiter = util.NewBytesLimiter(int(limits.AttachmentBandwidthLimit), oneDay)
if v.user == nil { 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{ go v.userManager.EnqueueUserStats(v.user.ID, &user.Stats{
Messages: messages, Messages: messages,
Emails: emails, Emails: emails,
SMS: sms,
Calls: calls, Calls: calls,
}) })
} }
@ -440,7 +421,6 @@ func tierBasedVisitorLimits(conf *Config, tier *user.Tier) *visitorLimits {
EmailLimit: tier.EmailLimit, EmailLimit: tier.EmailLimit,
EmailLimitBurst: util.MinMax(int(float64(tier.EmailLimit)*visitorEmailLimitBurstRate), conf.VisitorEmailLimitBurst, visitorEmailLimitBurstMax), EmailLimitBurst: util.MinMax(int(float64(tier.EmailLimit)*visitorEmailLimitBurstRate), conf.VisitorEmailLimitBurst, visitorEmailLimitBurstMax),
EmailLimitReplenish: dailyLimitToRate(tier.EmailLimit), EmailLimitReplenish: dailyLimitToRate(tier.EmailLimit),
SMSLimit: tier.SMSLimit,
CallLimit: tier.CallLimit, CallLimit: tier.CallLimit,
ReservationsLimit: tier.ReservationLimit, ReservationsLimit: tier.ReservationLimit,
AttachmentTotalSizeLimit: tier.AttachmentTotalSizeLimit, AttachmentTotalSizeLimit: tier.AttachmentTotalSizeLimit,
@ -464,7 +444,6 @@ func configBasedVisitorLimits(conf *Config) *visitorLimits {
EmailLimit: replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish), // Approximation! EmailLimit: replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish), // Approximation!
EmailLimitBurst: conf.VisitorEmailLimitBurst, EmailLimitBurst: conf.VisitorEmailLimitBurst,
EmailLimitReplenish: rate.Every(conf.VisitorEmailLimitReplenish), EmailLimitReplenish: rate.Every(conf.VisitorEmailLimitReplenish),
SMSLimit: int64(conf.VisitorSMSDailyLimit),
CallLimit: int64(conf.VisitorCallDailyLimit), CallLimit: int64(conf.VisitorCallDailyLimit),
ReservationsLimit: visitorDefaultReservationsLimit, ReservationsLimit: visitorDefaultReservationsLimit,
AttachmentTotalSizeLimit: conf.VisitorAttachmentTotalSizeLimit, AttachmentTotalSizeLimit: conf.VisitorAttachmentTotalSizeLimit,
@ -511,7 +490,6 @@ func (v *visitor) Info() (*visitorInfo, error) {
func (v *visitor) infoLightNoLock() *visitorInfo { func (v *visitor) infoLightNoLock() *visitorInfo {
messages := v.messagesLimiter.Value() messages := v.messagesLimiter.Value()
emails := v.emailsLimiter.Value() emails := v.emailsLimiter.Value()
sms := v.smsLimiter.Value()
calls := v.callsLimiter.Value() calls := v.callsLimiter.Value()
limits := v.limitsNoLock() limits := v.limitsNoLock()
stats := &visitorStats{ stats := &visitorStats{
@ -519,8 +497,6 @@ func (v *visitor) infoLightNoLock() *visitorInfo {
MessagesRemaining: zeroIfNegative(limits.MessageLimit - messages), MessagesRemaining: zeroIfNegative(limits.MessageLimit - messages),
Emails: emails, Emails: emails,
EmailsRemaining: zeroIfNegative(limits.EmailLimit - emails), EmailsRemaining: zeroIfNegative(limits.EmailLimit - emails),
SMS: sms,
SMSRemaining: zeroIfNegative(limits.SMSLimit - sms),
Calls: calls, Calls: calls,
CallsRemaining: zeroIfNegative(limits.CallLimit - calls), CallsRemaining: zeroIfNegative(limits.CallLimit - calls),
} }

View file

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/mattn/go-sqlite3"
_ "github.com/mattn/go-sqlite3" // SQLite driver _ "github.com/mattn/go-sqlite3" // SQLite driver
"github.com/stripe/stripe-go/v74" "github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -55,7 +56,6 @@ const (
messages_limit INT NOT NULL, messages_limit INT NOT NULL,
messages_expiry_duration INT NOT NULL, messages_expiry_duration INT NOT NULL,
emails_limit INT NOT NULL, emails_limit INT NOT NULL,
sms_limit INT NOT NULL,
calls_limit INT NOT NULL, calls_limit INT NOT NULL,
reservations_limit INT NOT NULL, reservations_limit INT NOT NULL,
attachment_file_size_limit INT NOT NULL, attachment_file_size_limit INT NOT NULL,
@ -78,7 +78,6 @@ const (
sync_topic TEXT NOT NULL, sync_topic TEXT NOT NULL,
stats_messages INT NOT NULL DEFAULT (0), stats_messages INT NOT NULL DEFAULT (0),
stats_emails 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), stats_calls INT NOT NULL DEFAULT (0),
stripe_customer_id TEXT, stripe_customer_id TEXT,
stripe_subscription_id TEXT, stripe_subscription_id TEXT,
@ -135,26 +134,26 @@ const (
` `
selectUserByIDQuery = ` 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 FROM user u
LEFT JOIN tier t on t.id = u.tier_id LEFT JOIN tier t on t.id = u.tier_id
WHERE u.id = ? WHERE u.id = ?
` `
selectUserByNameQuery = ` 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 FROM user u
LEFT JOIN tier t on t.id = u.tier_id LEFT JOIN tier t on t.id = u.tier_id
WHERE user = ? WHERE user = ?
` `
selectUserByTokenQuery = ` 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 FROM user u
JOIN user_token tk on u.id = tk.user_id JOIN user_token tk on u.id = tk.user_id
LEFT JOIN tier t on t.id = u.tier_id LEFT JOIN tier t on t.id = u.tier_id
WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?) WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?)
` `
selectUserByStripeCustomerIDQuery = ` 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 FROM user u
LEFT JOIN tier t on t.id = u.tier_id LEFT JOIN tier t on t.id = u.tier_id
WHERE u.stripe_customer_id = ? WHERE u.stripe_customer_id = ?
@ -185,8 +184,8 @@ const (
updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?` updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?`
updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?` updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?`
updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?` updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?`
updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_sms = ?, stats_calls = ? WHERE id = ?` updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_calls = ? WHERE id = ?`
updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_sms = 0, stats_calls = 0` updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_calls = 0`
updateUserDeletedQuery = `UPDATE user SET deleted = ? WHERE id = ?` updateUserDeletedQuery = `UPDATE user SET deleted = ? WHERE id = ?`
deleteUsersMarkedQuery = `DELETE FROM user WHERE deleted < ?` deleteUsersMarkedQuery = `DELETE FROM user WHERE deleted < ?`
deleteUserQuery = `DELETE FROM user WHERE user = ?` deleteUserQuery = `DELETE FROM user WHERE user = ?`
@ -274,25 +273,25 @@ const (
updatePhoneNumberVerifiedQuery = `UPDATE user_phone SET verified=1 WHERE user_id = ? AND phone_number = ?` updatePhoneNumberVerifiedQuery = `UPDATE user_phone SET verified=1 WHERE user_id = ? AND phone_number = ?`
insertTierQuery = ` 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) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
updateTierQuery = ` updateTierQuery = `
UPDATE tier 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 = ? WHERE code = ?
` `
selectTiersQuery = ` 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 FROM tier
` `
selectTierByCodeQuery = ` 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 FROM tier
WHERE code = ? WHERE code = ?
` `
selectTierByPriceIDQuery = ` 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 FROM tier
WHERE (stripe_monthly_price_id = ? OR stripe_yearly_price_id = ?) WHERE (stripe_monthly_price_id = ? OR stripe_yearly_price_id = ?)
` `
@ -410,9 +409,7 @@ const (
// 3 -> 4 // 3 -> 4
migrate3To4UpdateQueries = ` 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 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); ALTER TABLE user ADD COLUMN stats_calls INT NOT NULL DEFAULT (0);
CREATE TABLE IF NOT EXISTS user_phone ( CREATE TABLE IF NOT EXISTS user_phone (
user_id TEXT NOT NULL, 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 { func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error {
if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil { 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 err
} }
return nil return nil
@ -783,11 +783,10 @@ func (a *Manager) writeUserStatsQueue() error {
"user_id": userID, "user_id": userID,
"messages_count": update.Messages, "messages_count": update.Messages,
"emails_count": update.Emails, "emails_count": update.Emails,
"sms_count": update.SMS,
"calls_count": update.Calls, "calls_count": update.Calls,
}). }).
Trace("Updating stats for user %s", userID) 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 return err
} }
} }
@ -869,6 +868,9 @@ func (a *Manager) AddUser(username, password string, role Role) error {
userID := util.RandomStringPrefix(userIDPrefix, userIDLength) userID := util.RandomStringPrefix(userIDPrefix, userIDLength)
syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix()
if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { 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 err
} }
return nil return nil
@ -996,12 +998,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
defer rows.Close() defer rows.Close()
var id, username, hash, role, prefs, syncTopic string var id, username, hash, role, prefs, syncTopic string
var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString
var messages, emails, sms, calls int64 var messages, emails, calls int64
var messagesLimit, messagesExpiryDuration, emailsLimit, smsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64
if !rows.Next() { if !rows.Next() {
return nil, ErrUserNotFound 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 return nil, err
} else if err := rows.Err(); err != nil { } else if err := rows.Err(); err != nil {
return nil, err return nil, err
@ -1016,7 +1018,6 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
Stats: &Stats{ Stats: &Stats{
Messages: messages, Messages: messages,
Emails: emails, Emails: emails,
SMS: sms,
Calls: calls, Calls: calls,
}, },
Billing: &Billing{ Billing: &Billing{
@ -1041,7 +1042,6 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
MessageLimit: messagesLimit.Int64, MessageLimit: messagesLimit.Int64,
MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second,
EmailLimit: emailsLimit.Int64, EmailLimit: emailsLimit.Int64,
SMSLimit: smsLimit.Int64,
CallLimit: callsLimit.Int64, CallLimit: callsLimit.Int64,
ReservationLimit: reservationsLimit.Int64, ReservationLimit: reservationsLimit.Int64,
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
@ -1348,7 +1348,7 @@ func (a *Manager) AddTier(tier *Tier) error {
if tier.ID == "" { if tier.ID == "" {
tier.ID = util.RandomStringPrefix(tierIDPrefix, tierIDLength) 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 err
} }
return nil return nil
@ -1356,7 +1356,7 @@ func (a *Manager) AddTier(tier *Tier) error {
// UpdateTier updates a tier's properties in the database // UpdateTier updates a tier's properties in the database
func (a *Manager) UpdateTier(tier *Tier) error { 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 err
} }
return nil return nil
@ -1425,11 +1425,11 @@ func (a *Manager) TierByStripePrice(priceID string) (*Tier, error) {
func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
var id, code, name string var id, code, name string
var stripeMonthlyPriceID, stripeYearlyPriceID sql.NullString 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() { if !rows.Next() {
return nil, ErrTierNotFound 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 return nil, err
} else if err := rows.Err(); err != nil { } else if err := rows.Err(); err != nil {
return nil, err return nil, err
@ -1442,7 +1442,6 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
MessageLimit: messagesLimit.Int64, MessageLimit: messagesLimit.Int64,
MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second,
EmailLimit: emailsLimit.Int64, EmailLimit: emailsLimit.Int64,
SMSLimit: smsLimit.Int64,
CallLimit: callsLimit.Int64, CallLimit: callsLimit.Int64,
ReservationLimit: reservationsLimit.Int64, ReservationLimit: reservationsLimit.Int64,
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,

View file

@ -91,7 +91,6 @@ type Tier struct {
MessageLimit int64 // Daily message limit MessageLimit int64 // Daily message limit
MessageExpiryDuration time.Duration // Cache duration for messages MessageExpiryDuration time.Duration // Cache duration for messages
EmailLimit int64 // Daily email limit EmailLimit int64 // Daily email limit
SMSLimit int64 // Daily SMS limit
CallLimit int64 // Daily phone call limit CallLimit int64 // Daily phone call limit
ReservationLimit int64 // Number of topic reservations allowed by user ReservationLimit int64 // Number of topic reservations allowed by user
AttachmentFileSizeLimit int64 // Max file size per file (bytes) AttachmentFileSizeLimit int64 // Max file size per file (bytes)
@ -138,7 +137,6 @@ type NotificationPrefs struct {
type Stats struct { type Stats struct {
Messages int64 Messages int64
Emails int64 Emails int64
SMS int64
Calls int64 Calls int64
} }
@ -285,8 +283,10 @@ var (
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument") ErrInvalidArgument = errors.New("invalid argument")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrTierNotFound = errors.New("tier not found") ErrTierNotFound = errors.New("tier not found")
ErrTokenNotFound = errors.New("token not found") ErrTokenNotFound = errors.New("token not found")
ErrPhoneNumberNotFound = errors.New("phone number not found") ErrPhoneNumberNotFound = errors.New("phone number not found")
ErrTooManyReservations = errors.New("new tier has lower reservation limit") ErrTooManyReservations = errors.New("new tier has lower reservation limit")
ErrPhoneNumberExists = errors.New("phone number already exists")
) )

View file

@ -12,7 +12,6 @@ var config = {
enable_signup: true, enable_signup: true,
enable_payments: true, enable_payments: true,
enable_reservations: true, enable_reservations: true,
enable_sms: true,
enable_calls: true, enable_calls: true,
billing_contact: "", billing_contact: "",
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"]

View file

@ -127,9 +127,6 @@
"publish_dialog_email_label": "Email", "publish_dialog_email_label": "Email",
"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_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_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",
"publish_dialog_call_reset": "Remove phone call", "publish_dialog_call_reset": "Remove phone call",
@ -144,7 +141,6 @@
"publish_dialog_other_features": "Other features:", "publish_dialog_other_features": "Other features:",
"publish_dialog_chip_click_label": "Click URL", "publish_dialog_chip_click_label": "Click URL",
"publish_dialog_chip_email_label": "Forward to email", "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_call_label": "Phone call",
"publish_dialog_chip_attach_url_label": "Attach file by URL", "publish_dialog_chip_attach_url_label": "Attach file by URL",
"publish_dialog_chip_attach_file_label": "Attach local file", "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_confirm_password_label": "Confirm password",
"account_basics_password_dialog_button_submit": "Change password", "account_basics_password_dialog_button_submit": "Change password",
"account_basics_password_dialog_current_password_incorrect": "Password incorrect", "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_title": "Usage",
"account_usage_of_limit": "of {{limit}}", "account_usage_of_limit": "of {{limit}}",
"account_usage_unlimited": "Unlimited", "account_usage_unlimited": "Unlimited",
@ -211,8 +209,6 @@
"account_basics_tier_manage_billing_button": "Manage billing", "account_basics_tier_manage_billing_button": "Manage billing",
"account_usage_messages_title": "Published messages", "account_usage_messages_title": "Published messages",
"account_usage_emails_title": "Emails sent", "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_title": "Phone calls made",
"account_usage_calls_none": "No phone calls can be made with this account", "account_usage_calls_none": "No phone calls can be made with this account",
"account_usage_reservations_title": "Reserved topics", "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_messages_other": "{{messages}} daily messages",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email", "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_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_one": "{{calls}} daily phone calls",
"account_upgrade_dialog_tier_features_calls_other": "{{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", "account_upgrade_dialog_tier_features_no_calls": "No daily phone calls",

View file

@ -3,7 +3,7 @@ import {useContext, useState} from 'react';
import { import {
Alert, Alert,
CardActions, CardActions,
CardContent, CardContent, Chip,
FormControl, FormControl,
LinearProgress, LinearProgress,
Link, Link,
@ -52,6 +52,7 @@ import MenuItem from "@mui/material/MenuItem";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
import {ProChip} from "./SubscriptionPopup"; import {ProChip} from "./SubscriptionPopup";
import AddIcon from "@mui/icons-material/Add";
const Account = () => { const Account = () => {
if (!session.exists()) { if (!session.exists()) {
@ -80,6 +81,7 @@ const Basics = () => {
<PrefGroup> <PrefGroup>
<Username/> <Username/>
<ChangePassword/> <ChangePassword/>
<PhoneNumbers/>
<AccountType/> <AccountType/>
</PrefGroup> </PrefGroup>
</Card> </Card>
@ -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 (
<Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}>
<div aria-labelledby={labelId}>
{account?.phone_numbers.map(p =>
<Chip
label={p.number}
variant="outlined"
onClick={() => navigator.clipboard.writeText(p.number)}
onDelete={() => handleDelete(p.number)}
/>
)}
<IconButton onClick={() => handleAdd()}><AddIcon/></IconButton>
</div>
</Pref>
)
};
const Stats = () => { const Stats = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { account } = useContext(AccountContext); const { account } = useContext(AccountContext);
@ -380,23 +416,6 @@ const Stats = () => {
value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100} value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100}
/> />
</Pref> </Pref>
{(account.role === Role.ADMIN || account.limits.sms > 0) &&
<Pref title={
<>
{t("account_usage_sms_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
</>
}>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.sms.toLocaleString()}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.sms.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress
variant="determinate"
value={account.role === Role.USER && account.limits.sms > 0 ? normalize(account.stats.sms, account.limits.sms) : 100}
/>
</Pref>
}
{(account.role === Role.ADMIN || account.limits.calls > 0) && {(account.role === Role.ADMIN || account.limits.calls > 0) &&
<Pref title={ <Pref title={
<> <>
@ -410,7 +429,7 @@ const Stats = () => {
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.role === Role.USER && account.limits.sms > 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}
/> />
</Pref> </Pref>
} }
@ -439,11 +458,6 @@ const Stats = () => {
<em>{t("account_usage_reservations_none")}</em> <em>{t("account_usage_reservations_none")}</em>
</Pref> </Pref>
} }
{config.enable_sms && account.role === Role.USER && account.limits.sms === 0 &&
<Pref title={<>{t("account_usage_sms_title")}{config.enable_payments && <ProChip/>}</>}>
<em>{t("account_usage_sms_none")}</em>
</Pref>
}
{config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && {config.enable_calls && account.role === Role.USER && account.limits.calls === 0 &&
<Pref title={<>{t("account_usage_calls_title")}{config.enable_payments && <ProChip/>}</>}> <Pref title={<>{t("account_usage_calls_title")}{config.enable_payments && <ProChip/>}</>}>
<em>{t("account_usage_calls_none")}</em> <em>{t("account_usage_calls_none")}</em>