Add call verification

This commit is contained in:
binwiederhier 2023-05-16 22:27:48 -04:00
parent 496d6e74b0
commit 2c81773d01
11 changed files with 93 additions and 74 deletions

View file

@ -868,8 +868,8 @@ are the easiest), and then configure the following options:
* `twilio-from-number` is the outgoing phone number you purchased, e.g. +18775132586 * `twilio-from-number` is the outgoing phone number you purchased, e.g. +18775132586
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 * `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
After you have configured phone calls, create a [tier](#tiers) with a call limit, and then assign it to a user. After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
Users may then use the `X-Call` header to receive a phone call when publishing a message. and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
## Rate limiting ## Rate limiting
!!! info !!! info

View file

@ -2702,16 +2702,26 @@ You can use ntfy to call a phone and **read the message out loud using text-to-s
Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have
the ntfy app installed on their phone. the ntfy app installed on their phone.
**Phone numbers have to be previously verified** (via the web app), so this feature is **only available to authenticated users**. **Phone numbers have to be previously verified** (via the [web app](https://ntfy.sh/account)), so this feature is
To forward a message as a voice call, pass a phone number in the `X-Call` header (or its alias: `Call`), prefixed with a **only available to authenticated users** (no anonymous phone calls). To forward a message as a voice call, pass a phone
plus sign and the country code, e.g. `+12223334444`. You may also simply pass `yes` as a value to pick the first of your number in the `X-Call` header (or its alias: `Call`), prefixed with a plus sign and the country code, e.g. `+12223334444`.
verified phone numbers. You may also simply pass `yes` as a value to pick the first of your verified phone numbers.
On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans.
<figure markdown>
![e-mail publishing](static/img/web-phone-verify.png)
<figcaption>Phone number verification in the <a href="https://ntfy.sh/account">web app</a></figcaption>
</figure>
As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll
be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues).
!!! info !!! info
As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll You are responsible for the message content, and **you must abide by the [Twilio Acceptable Use Policy](https://www.twilio.com/en-us/legal/aup)**.
be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). This particularly means that you must not use this feature to send unsolicited messages, or messages that are illegal or
violate the rights of others. Please read the policy for details. Failure to do so may result in your account being suspended or terminated.
On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. Here's how you use it:
=== "Command line (curl)" === "Command line (curl)"
``` ```
@ -3431,17 +3441,18 @@ There are a few limitations to the API to prevent abuse and to keep the server h
are configurable via the server side [rate limiting settings](config.md#rate-limiting). Most of these limits you won't run into, are configurable via the server side [rate limiting settings](config.md#rate-limiting). Most of these limits you won't run into,
but just in case, let's list them all: but just in case, let's list them all:
| Limit | Description | | Limit | Description |
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Message length** | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments). | | **Message length** | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments). |
| **Requests** | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds. | | **Requests** | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds. |
| **Daily messages** | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250. | | **Daily messages** | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250. |
| **E-mails** | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5. | | **E-mails** | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5. |
| **Subscription limit** | By default, the server allows each visitor to keep 30 connections to the server open. | | **Phone calls** | By default, the server does not allow any phone calls, except for users with a tier that has a call limit. |
| **Attachment size limit** | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. | | **Subscription limit** | By default, the server allows each visitor to keep 30 connections to the server open. |
| **Attachment expiry** | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit. | | **Attachment size limit** | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. |
| **Attachment bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB. | | **Attachment expiry** | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit. |
| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. | | **Attachment bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB. |
| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. |
These limits can be changed on a per-user basis using [tiers](config.md#tiers). If [payments](config.md#payments) are enabled, a user tier can be changed by purchasing These limits can be changed on a per-user basis using [tiers](config.md#tiers). If [payments](config.md#payments) are enabled, a user tier can be changed by purchasing
a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above. a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above.
@ -3470,6 +3481,7 @@ table in their canonical form.
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) | | `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client | | `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
| `X-Call` | `Call` | Phone number for [phone calls](#phone-calls) |
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) | | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps | | `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |

BIN
docs/static/img/web-phone-verify.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

18
go.sum
View file

@ -1,23 +1,15 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.1 h1:oDJ19Fu9TX9Xs06iyCw4yifSqZ7JQ8BeuVHcTmWQlOA=
cloud.google.com/go v0.110.1/go.mod h1:uc+V/WjzxQ7vpkxfJhgW4Q4axWXyfAerpQOuSNDZyFw=
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY= cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY=
cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08= cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/iam v1.0.0 h1:hlQJMovyJJwYjZcTohUH4o1L8Z8kYz+E+W/zktiLCBc=
cloud.google.com/go/iam v1.0.0/go.mod h1:ikbQ4f1r91wTmBmmOtBCOtuEOei6taatNXytzB7Cxew=
cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU=
cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE=
cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ=
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
@ -147,8 +139,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stripe/stripe-go/v74 v74.17.0 h1:qVWSzmADr6gudznuAcPjB9ewzgxfyIhBCkyTbkxJcCw=
github.com/stripe/stripe-go/v74 v74.17.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw= github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw=
github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
@ -163,8 +153,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -186,14 +174,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -241,8 +225,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.121.0 h1:8Oopoo8Vavxx6gt+sgs8s8/X60WBAtKQq6JqnkF+xow=
google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=

View file

@ -112,6 +112,7 @@ var (
errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "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}

View file

@ -523,12 +523,13 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi
func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User() u := v.User()
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil { if err != nil {
return err return err
} } else if !phoneNumberRegex.MatchString(req.Number) {
if !phoneNumberRegex.MatchString(req.Number) {
return errHTTPBadRequestPhoneNumberInvalid return errHTTPBadRequestPhoneNumberInvalid
} else if req.Channel != "sms" && req.Channel != "call" {
return errHTTPBadRequestPhoneNumberVerifyChannelInvalid
} }
// 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) {
@ -545,7 +546,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
} }
// Actually add the unverified number, and send verification // Actually add the unverified number, and send verification
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification") logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification")
if err := s.verifyPhoneNumber(v, r, req.Number); err != nil { if err := s.verifyPhoneNumber(v, r, req.Number, req.Channel); err != nil {
return err return err
} }
return s.writeJSON(w, newSuccessResponse()) return s.writeJSON(w, newSuccessResponse())
@ -553,7 +554,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User() u := v.User()
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil { if err != nil {
return err return err
} }
@ -572,7 +573,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ
func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User() u := v.User()
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil { if err != nil {
return err return err
} }

View file

@ -18,13 +18,14 @@ const (
<Response> <Response>
<Pause length="1"/> <Pause length="1"/>
<Say loop="3"> <Say loop="3">
You have a notification from notify on topic %s. Message: You have a message from notify on topic %s. Message:
<break time="1s"/> <break time="1s"/>
%s %s
<break time="1s"/> <break time="1s"/>
End message. End of message.
<break time="1s"/> <break time="1s"/>
This message was sent by user %s. It will be repeated up to three times. This message was sent by user %s. It will be repeated three times.
To unsubscribe from calls like this, remove your phone number in the notify web app.
<break time="3s"/> <break time="3s"/>
</Say> </Say>
<Say>Goodbye.</Say> <Say>Goodbye.</Say>
@ -97,11 +98,11 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) {
return string(response), nil return string(response), nil
} }
func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber string) error { func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error {
ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification") ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification")
data := url.Values{} data := url.Values{}
data.Set("To", phoneNumber) data.Set("To", phoneNumber)
data.Set("Channel", "sms") data.Set("Channel", channel)
requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", 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 {

View file

@ -311,9 +311,14 @@ type apiAccountTokenResponse struct {
Expires int64 `json:"expires,omitempty"` // Unix timestamp Expires int64 `json:"expires,omitempty"` // Unix timestamp
} }
type apiAccountPhoneNumberRequest struct { type apiAccountPhoneNumberVerifyRequest struct {
Number string `json:"number"`
Channel string `json:"channel"`
}
type apiAccountPhoneNumberAddRequest struct {
Number string `json:"number"` Number string `json:"number"`
Code string `json:"code,omitempty"` // Only supplied in "verify" call Code string `json:"code,omitempty"`
} }
type apiAccountTier struct { type apiAccountTier struct {

View file

@ -188,17 +188,20 @@
"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_title": "Phone numbers",
"account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Adding it will send a verification SMS to your phone.", "account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Verification can be done via SMS or a phone call.",
"account_basics_phone_numbers_description": "For phone call notifications", "account_basics_phone_numbers_description": "For phone call notifications",
"account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet", "account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet",
"account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard", "account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard",
"account_basics_phone_numbers_dialog_title": "Add phone number", "account_basics_phone_numbers_dialog_title": "Add phone number",
"account_basics_phone_numbers_dialog_number_label": "Phone number", "account_basics_phone_numbers_dialog_number_label": "Phone number",
"account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444", "account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444",
"account_basics_phone_numbers_dialog_send_verification_button": "Send verification", "account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS",
"account_basics_phone_numbers_dialog_verify_button_call": "Call me",
"account_basics_phone_numbers_dialog_code_label": "Verification code", "account_basics_phone_numbers_dialog_code_label": "Verification code",
"account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456", "account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456",
"account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "Call",
"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",

View file

@ -299,14 +299,15 @@ class AccountApi {
return await response.json(); // May throw SyntaxError return await response.json(); // May throw SyntaxError
} }
async verifyPhoneNumber(phoneNumber) { async verifyPhoneNumber(phoneNumber, channel) {
const url = accountPhoneVerifyUrl(config.base_url); const url = accountPhoneVerifyUrl(config.base_url);
console.log(`[AccountApi] Sending phone verification ${url}`); console.log(`[AccountApi] Sending phone verification ${url}`);
await fetchOrThrow(url, { await fetchOrThrow(url, {
method: "PUT", method: "PUT",
headers: withBearerAuth({}, session.token()), headers: withBearerAuth({}, session.token()),
body: JSON.stringify({ body: JSON.stringify({
number: phoneNumber number: phoneNumber,
channel: channel
}) })
}); });
} }

View file

@ -1,13 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import {useContext, useState} from 'react'; import {useContext, useState} from 'react';
import { import {
Alert, Alert, ButtonGroup,
CardActions, CardActions,
CardContent, Chip, CardContent, Chip,
FormControl, FormControl, FormControlLabel, InputLabel,
LinearProgress, LinearProgress,
Link, Link,
Portal, Portal, Radio, RadioGroup,
Select, Select,
Snackbar, Snackbar,
Stack, Stack,
@ -47,12 +47,14 @@ import {AccountContext} from "./App";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import {Paragraph} from "./styles"; import {Paragraph} from "./styles";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import {ContentCopy, Public} from "@mui/icons-material"; import {Check, ContentCopy, DeleteForever, Public} from "@mui/icons-material";
import MenuItem from "@mui/material/MenuItem"; 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"; import AddIcon from "@mui/icons-material/Add";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
const Account = () => { const Account = () => {
if (!session.exists()) { if (!session.exists()) {
@ -408,6 +410,7 @@ const AddPhoneNumberDialog = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [error, setError] = useState(""); const [error, setError] = useState("");
const [phoneNumber, setPhoneNumber] = useState(""); const [phoneNumber, setPhoneNumber] = useState("");
const [channel, setChannel] = useState("sms");
const [code, setCode] = useState(""); const [code, setCode] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [verificationCodeSent, setVerificationCodeSent] = useState(false); const [verificationCodeSent, setVerificationCodeSent] = useState(false);
@ -432,7 +435,7 @@ const AddPhoneNumberDialog = (props) => {
const verifyPhone = async () => { const verifyPhone = async () => {
try { try {
setSending(true); setSending(true);
await accountApi.verifyPhoneNumber(phoneNumber); await accountApi.verifyPhoneNumber(phoneNumber, channel);
setVerificationCodeSent(true); setVerificationCodeSent(true);
} catch (e) { } catch (e) {
console.log(`[Account] Error sending verification`, e); console.log(`[Account] Error sending verification`, e);
@ -471,18 +474,26 @@ const AddPhoneNumberDialog = (props) => {
{t("account_basics_phone_numbers_dialog_description")} {t("account_basics_phone_numbers_dialog_description")}
</DialogContentText> </DialogContentText>
{!verificationCodeSent && {!verificationCodeSent &&
<TextField <div style={{display: "flex"}}>
margin="dense" <TextField
label={t("account_basics_phone_numbers_dialog_number_label")} margin="dense"
aria-label={t("account_basics_phone_numbers_dialog_number_label")} label={t("account_basics_phone_numbers_dialog_number_label")}
placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")} aria-label={t("account_basics_phone_numbers_dialog_number_label")}
type="tel" placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")}
value={phoneNumber} type="tel"
onChange={ev => setPhoneNumber(ev.target.value)} value={phoneNumber}
fullWidth onChange={ev => setPhoneNumber(ev.target.value)}
inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }}
variant="standard" variant="standard"
/> sx={{ flexGrow: 1 }}
/>
<FormControl sx={{ flexWrap: "nowrap" }}>
<RadioGroup row sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}>
<FormControlLabel value="sms" control={<Radio checked={channel === "sms"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_sms")} />
<FormControlLabel value="call" control={<Radio checked={channel === "call"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_call")} sx={{ marginRight: 0 }} />
</RadioGroup>
</FormControl>
</div>
} }
{verificationCodeSent && {verificationCodeSent &&
<TextField <TextField
@ -502,7 +513,9 @@ const AddPhoneNumberDialog = (props) => {
<DialogFooter status={error}> <DialogFooter status={error}>
<Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button>
<Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}> <Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}>
{verificationCodeSent ?t("account_basics_phone_numbers_dialog_check_verification_button") : t("account_basics_phone_numbers_dialog_send_verification_button")} {!verificationCodeSent && channel === "sms" && t("account_basics_phone_numbers_dialog_verify_button_sms")}
{!verificationCodeSent && channel === "call" && t("account_basics_phone_numbers_dialog_verify_button_call")}
{verificationCodeSent && t("account_basics_phone_numbers_dialog_check_verification_button")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>