WIP calls, remove SMS
This commit is contained in:
parent
d4767caf30
commit
f99159ee5b
16 changed files with 132 additions and 301 deletions
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
150
docs/publish.md
150
docs/publish.md
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue