mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-18 16:17:26 +01:00
Merge pull request #1536 from binwiederhier/303-update-notifications
Update/delete/clear notifications
This commit is contained in:
@@ -88,6 +88,11 @@ func WithFilename(filename string) PublishOption {
|
||||
return WithHeader("X-Filename", filename)
|
||||
}
|
||||
|
||||
// WithSequenceID sets a sequence ID for the message, allowing updates to existing notifications
|
||||
func WithSequenceID(sequenceID string) PublishOption {
|
||||
return WithHeader("X-Sequence-ID", sequenceID)
|
||||
}
|
||||
|
||||
// WithEmail instructs the server to also send the message to the given e-mail address
|
||||
func WithEmail(email string) PublishOption {
|
||||
return WithHeader("X-Email", email)
|
||||
|
||||
@@ -34,6 +34,7 @@ var flagsPublish = append(
|
||||
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
|
||||
&cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"},
|
||||
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
||||
&cli.StringFlag{Name: "sequence-id", Aliases: []string{"sequence_id", "sid", "S"}, EnvVars: []string{"NTFY_SEQUENCE_ID"}, Usage: "sequence ID for updating notifications"},
|
||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||
@@ -70,6 +71,7 @@ Examples:
|
||||
ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon
|
||||
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
|
||||
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
|
||||
ntfy pub -S my-id mytopic 'Update me' # Send with sequence ID for updates
|
||||
echo 'message' | ntfy publish mytopic # Send message from stdin
|
||||
ntfy pub -u phil:mypass secret Psst # Publish with username/password
|
||||
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
|
||||
@@ -101,6 +103,7 @@ func execPublish(c *cli.Context) error {
|
||||
markdown := c.Bool("markdown")
|
||||
template := c.String("template")
|
||||
filename := c.String("filename")
|
||||
sequenceID := c.String("sequence-id")
|
||||
file := c.String("file")
|
||||
email := c.String("email")
|
||||
user := c.String("user")
|
||||
@@ -154,6 +157,9 @@ func execPublish(c *cli.Context) error {
|
||||
if filename != "" {
|
||||
options = append(options, client.WithFilename(filename))
|
||||
}
|
||||
if sequenceID != "" {
|
||||
options = append(options, client.WithSequenceID(sequenceID))
|
||||
}
|
||||
if email != "" {
|
||||
options = append(options, client.WithEmail(email))
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ appreciated.
|
||||
## Can I email you? Can I DM you on Discord/Matrix?
|
||||
For community support, please use the public channels listed on the [contact page](contact.md). I generally
|
||||
**do not respond to direct messages** about ntfy, unless you are paying for a [ntfy Pro](https://ntfy.sh/#pricing)
|
||||
plan (see [paid support](contact.md#paid-support-ntfy-pro-subscribers)), or you are inquiring about business
|
||||
opportunities (see [general inquiries](contact.md#general-inquiries)).
|
||||
plan (see [paid support](contact.md#paid-support)), or you are inquiring about business
|
||||
opportunities (see [other inquiries](contact.md#other-inquiries)).
|
||||
|
||||
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
|
||||
in public forums benefits others, since I can link to the discussion at a later point in time, or other users
|
||||
|
||||
473
docs/publish.md
473
docs/publish.md
@@ -937,6 +937,445 @@ Here's an example with a custom message, tags and a priority:
|
||||
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
||||
```
|
||||
|
||||
## Updating + deleting notifications
|
||||
_Supported on:_ :material-android: :material-firefox:
|
||||
|
||||
!!! info
|
||||
**This feature is not yet released.** It will be available in ntfy v2.16.x and later and ntfy Android v1.22.x and later.
|
||||
|
||||
You can **update, clear (mark as read and dismiss), or delete notifications** that have already been delivered. This is useful for scenarios
|
||||
like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant.
|
||||
|
||||
* [Updating notifications](#updating-notifications) will alter the content of an existing notification.
|
||||
* [Clearing notifications](#clearing-notifications) will mark them as read and dismiss them from the notification drawer.
|
||||
* [Deleting notifications](#deleting-notifications) will remove them from the notification drawer and remove them in the clients as well (if supported).
|
||||
|
||||
Here's an example of a download progress notification being updated over time on Android:
|
||||
|
||||
<div id="updating-notifications-screenshots" class="screenshots">
|
||||
<a href="../../static/img/android-screenshot-notification-update-1.png"><img src="../../static/img/android-screenshot-notification-update-1.png"/></a>
|
||||
<a href="../../static/img/android-screenshot-notification-update-2.png"><img src="../../static/img/android-screenshot-notification-update-2.png"/></a>
|
||||
</div>
|
||||
|
||||
To facilitate updating notifications and altering existing notifications, ntfy messages are linked together in a sequence,
|
||||
using a **sequence ID**. When a notification is meant to be updated, cleared, or deleted, you publish a new message with the
|
||||
same sequence ID and the clients will perform the appropriate action on the existing notification.
|
||||
|
||||
Existing ntfy messages will not be updated on the server or in the message cache. Instead, a new message is created that indicates
|
||||
the update, clear, or delete action. This append-only behavior ensures that message history remains intact.
|
||||
|
||||
### Updating notifications
|
||||
To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous
|
||||
notification with the new one. You can either:
|
||||
|
||||
1. **Use the message ID**: First publish like normal to `POST /<topic>` without a sequence ID, then use the returned message `id` as the sequence ID for updates
|
||||
2. **Use a custom sequence ID**: Publish directly to `POST /<topic>/<sequence_id>` with your own identifier, or use `POST /<topic>` with the
|
||||
`X-Sequence-ID` header (or any of its aliases: `Sequence-ID` or`SID`)
|
||||
|
||||
If you don't know the sequence ID ahead of time, you can publish a message first and then use the returned
|
||||
message `id` to update it. Here's an example:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```bash
|
||||
# First, publish a message and capture the message ID
|
||||
curl -d "Downloading file..." ntfy.sh/mytopic
|
||||
# Returns: {"id":"xE73Iyuabi","time":1673542291,...}
|
||||
|
||||
# Then use the message ID to update it (via URL path)
|
||||
curl -d "Download 50% ..." ntfy.sh/mytopic/xE73Iyuabi
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
curl -H "X-Sequence-ID: xE73Iyuabi" -d "Download complete" ntfy.sh/mytopic
|
||||
```
|
||||
|
||||
=== "ntfy CLI"
|
||||
```bash
|
||||
# First, publish a message and capture the message ID
|
||||
ntfy pub mytopic "Downloading file..."
|
||||
# Returns: {"id":"xE73Iyuabi","time":1673542291,...}
|
||||
|
||||
# Then use the message ID to update it
|
||||
ntfy pub --sequence-id=xE73Iyuabi mytopic "Download 50% ..."
|
||||
|
||||
# Update again with the same sequence ID
|
||||
ntfy pub -S xE73Iyuabi mytopic "Download complete"
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
# First, publish a message and capture the message ID
|
||||
POST /mytopic HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
|
||||
Downloading file...
|
||||
|
||||
# Returns: {"id":"xE73Iyuabi","time":1673542291,...}
|
||||
|
||||
# Then use the message ID to update it
|
||||
POST /mytopic/xE73Iyuabi HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
|
||||
Download 50% ...
|
||||
|
||||
# Update again with the same sequence ID, this time using the header
|
||||
POST /mytopic HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
X-Sequence-ID: xE73Iyuabi
|
||||
|
||||
Download complete
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
// First, publish and get the message ID
|
||||
const response = await fetch('https://ntfy.sh/mytopic', {
|
||||
method: 'POST',
|
||||
body: 'Downloading file...'
|
||||
});
|
||||
const { id } = await response.json();
|
||||
|
||||
// Update via URL path
|
||||
await fetch(`https://ntfy.sh/mytopic/${id}`, {
|
||||
method: 'POST',
|
||||
body: 'Download 50% ...'
|
||||
});
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
await fetch('https://ntfy.sh/mytopic', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Sequence-ID': id },
|
||||
body: 'Download complete'
|
||||
});
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
// Publish and parse the response to get the message ID
|
||||
resp, _ := http.Post("https://ntfy.sh/mytopic", "text/plain",
|
||||
strings.NewReader("Downloading file..."))
|
||||
var msg struct { ID string `json:"id"` }
|
||||
json.NewDecoder(resp.Body).Decode(&msg)
|
||||
|
||||
// Update via URL path
|
||||
http.Post("https://ntfy.sh/mytopic/"+msg.ID, "text/plain",
|
||||
strings.NewReader("Download 50% ..."))
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic",
|
||||
strings.NewReader("Download complete"))
|
||||
req.Header.Set("X-Sequence-ID", msg.ID)
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
# Publish and get the message ID
|
||||
$response = Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" -Body "Downloading file..."
|
||||
$messageId = $response.id
|
||||
|
||||
# Update via URL path
|
||||
Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/$messageId" -Body "Download 50% ..."
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" `
|
||||
-Headers @{"X-Sequence-ID"=$messageId} -Body "Download complete"
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
import requests
|
||||
|
||||
# Publish and get the message ID
|
||||
response = requests.post("https://ntfy.sh/mytopic", data="Downloading file...")
|
||||
message_id = response.json()["id"]
|
||||
|
||||
# Update via URL path
|
||||
requests.post(f"https://ntfy.sh/mytopic/{message_id}", data="Download 50% ...")
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
requests.post("https://ntfy.sh/mytopic",
|
||||
headers={"X-Sequence-ID": message_id}, data="Download complete")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
// Publish and get the message ID
|
||||
$response = file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
'http' => ['method' => 'POST', 'content' => 'Downloading file...']
|
||||
]));
|
||||
$messageId = json_decode($response)->id;
|
||||
|
||||
// Update via URL path
|
||||
file_get_contents("https://ntfy.sh/mytopic/$messageId", false, stream_context_create([
|
||||
'http' => ['method' => 'POST', 'content' => 'Download 50% ...']
|
||||
]));
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "X-Sequence-ID: $messageId",
|
||||
'content' => 'Download complete'
|
||||
]
|
||||
]));
|
||||
```
|
||||
|
||||
You can also use a **custom sequence ID** (e.g., a download ID, job ID, etc.) when publishing the first message.
|
||||
**This is less cumbersome**, since you don't need to capture the message ID first. Just publish directly to
|
||||
`/<topic>/<sequence_id>`:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```bash
|
||||
# Publish with a custom sequence ID
|
||||
curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123
|
||||
|
||||
# Update using the same sequence ID (via URL path)
|
||||
curl -d "Download 50% ..." ntfy.sh/mytopic/my-download-123
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
curl -H "X-Sequence-ID: my-download-123" -d "Download complete" ntfy.sh/mytopic
|
||||
```
|
||||
|
||||
=== "ntfy CLI"
|
||||
```bash
|
||||
# Publish with a sequence ID
|
||||
ntfy pub --sequence-id=my-download-123 mytopic "Downloading file..."
|
||||
|
||||
# Update using the same sequence ID
|
||||
ntfy pub --sequence-id=my-download-123 mytopic "Download 50% ..."
|
||||
|
||||
# Update again
|
||||
ntfy pub -S my-download-123 mytopic "Download complete"
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
# Publish a message with a custom sequence ID
|
||||
POST /mytopic/my-download-123 HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
|
||||
Downloading file...
|
||||
|
||||
# Update again using the X-Sequence-ID header
|
||||
POST /mytopic HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
X-Sequence-ID: my-download-123
|
||||
|
||||
Download complete
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
// First message
|
||||
await fetch('https://ntfy.sh/mytopic/my-download-123', {
|
||||
method: 'POST',
|
||||
body: 'Downloading file...'
|
||||
});
|
||||
|
||||
// Update via URL path
|
||||
await fetch('https://ntfy.sh/mytopic/my-download-123', {
|
||||
method: 'POST',
|
||||
body: 'Download 50% ...'
|
||||
});
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
await fetch('https://ntfy.sh/mytopic', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Sequence-ID': 'my-download-123' },
|
||||
body: 'Download complete'
|
||||
});
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
// Publish with sequence ID in URL path
|
||||
http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain",
|
||||
strings.NewReader("Downloading file..."))
|
||||
|
||||
// Update via URL path
|
||||
http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain",
|
||||
strings.NewReader("Download 50% ..."))
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic",
|
||||
strings.NewReader("Download complete"))
|
||||
req.Header.Set("X-Sequence-ID", "my-download-123")
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
# Publish with sequence ID
|
||||
Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Downloading file..."
|
||||
|
||||
# Update via URL path
|
||||
Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Download 50% ..."
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" `
|
||||
-Headers @{"X-Sequence-ID"="my-download-123"} -Body "Download complete"
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
import requests
|
||||
|
||||
# Publish with sequence ID
|
||||
requests.post("https://ntfy.sh/mytopic/my-download-123", data="Downloading file...")
|
||||
|
||||
# Update via URL path
|
||||
requests.post("https://ntfy.sh/mytopic/my-download-123", data="Download 50% ...")
|
||||
|
||||
# Or update using the X-Sequence-ID header
|
||||
requests.post("https://ntfy.sh/mytopic",
|
||||
headers={"X-Sequence-ID": "my-download-123"}, data="Download complete")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
// Publish with sequence ID
|
||||
file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([
|
||||
'http' => ['method' => 'POST', 'content' => 'Downloading file...']
|
||||
]));
|
||||
|
||||
// Update via URL path
|
||||
file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([
|
||||
'http' => ['method' => 'POST', 'content' => 'Download 50% ...']
|
||||
]));
|
||||
|
||||
// Or update using the X-Sequence-ID header
|
||||
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => 'X-Sequence-ID: my-download-123',
|
||||
'content' => 'Download complete'
|
||||
]
|
||||
]));
|
||||
```
|
||||
|
||||
You can also set the sequence ID via the `sequence-id` [query parameter](#list-of-all-parameters), or when
|
||||
[publishing as JSON](#publish-as-json) using the `sequence_id` field.
|
||||
|
||||
If the message ID (`id`) and the sequence ID (`sequence_id`) are different, the ntfy server will include the `sequence_id`
|
||||
field the response. A sequence of updates may look like this (first example from above):
|
||||
|
||||
```json
|
||||
{"id":"xE73Iyuabi","time":1673542291,"event":"message","topic":"mytopic","message":"Downloading file..."}
|
||||
{"id":"yF84Jzvbcj","time":1673542295,"event":"message","topic":"mytopic","sequence_id":"xE73Iyuabi","message":"Download 50% ..."}
|
||||
{"id":"zG95Kawdde","time":1673542300,"event":"message","topic":"mytopic","sequence_id":"xE73Iyuabi","message":"Download complete"}
|
||||
```
|
||||
|
||||
### Clearing notifications
|
||||
Clearing a notification means **marking it as read and dismissing it from the notification drawer**.
|
||||
|
||||
To do this, send a PUT request to the `/<topic>/<sequence_id>/clear` endpoint (or `/<topic>/<sequence_id>/read` as an alias).
|
||||
This will then emit a `message_clear` event that is used by the clients (web app and Android app) to update the read status
|
||||
and dismiss the notification.
|
||||
|
||||
=== "Command line (curl)"
|
||||
```bash
|
||||
curl -X PUT ntfy.sh/mytopic/my-download-123/clear
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
PUT /mytopic/my-download-123/clear HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
await fetch('https://ntfy.sh/mytopic/my-download-123/clear', {
|
||||
method: 'PUT'
|
||||
});
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
req, _ := http.NewRequest("PUT", "https://ntfy.sh/mytopic/my-download-123/clear", nil)
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
Invoke-RestMethod -Method PUT -Uri "https://ntfy.sh/mytopic/my-download-123/clear"
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.put("https://ntfy.sh/mytopic/my-download-123/clear")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mytopic/my-download-123/clear', false, stream_context_create([
|
||||
'http' => ['method' => 'PUT']
|
||||
]));
|
||||
```
|
||||
|
||||
An example response from the server with the `message_clear` event may look like this:
|
||||
|
||||
```json
|
||||
{"id":"jkl012","time":1673542305,"event":"message_clear","topic":"mytopic","sequence_id":"my-download-123"}
|
||||
```
|
||||
|
||||
### Deleting notifications
|
||||
Deleting a notification means **removing it from the notification drawer and from the client's database**.
|
||||
|
||||
To do this, send a DELETE request to the `/<topic>/<sequence_id>` endpoint. This will emit a `message_delete` event
|
||||
that is used by the clients (web app and Android app) to remove the notification entirely.
|
||||
|
||||
=== "Command line (curl)"
|
||||
```bash
|
||||
curl -X DELETE ntfy.sh/mytopic/my-download-123
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
DELETE /mytopic/my-download-123 HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
await fetch('https://ntfy.sh/mytopic/my-download-123', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
req, _ := http.NewRequest("DELETE", "https://ntfy.sh/mytopic/my-download-123", nil)
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
Invoke-RestMethod -Method DELETE -Uri "https://ntfy.sh/mytopic/my-download-123"
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.delete("https://ntfy.sh/mytopic/my-download-123")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([
|
||||
'http' => ['method' => 'DELETE']
|
||||
]));
|
||||
```
|
||||
|
||||
An example response from the server with the `message_delete` event may look like this:
|
||||
|
||||
```json
|
||||
{"id":"mno345","time":1673542400,"event":"message_delete","topic":"mytopic","sequence_id":"my-download-123"}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Deleted sequences can be revived by publishing a new message with the same sequence ID. The notification will
|
||||
reappear as a new message.
|
||||
|
||||
## Message templating
|
||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||
|
||||
@@ -1418,22 +1857,23 @@ The JSON message format closely mirrors the format of the message you can consum
|
||||
(see [JSON message format](subscribe/api.md#json-message-format) for details), but is not exactly identical. Here's an overview of
|
||||
all the supported fields:
|
||||
|
||||
| Field | Required | Type | Example | Description |
|
||||
|------------|----------|----------------------------------|-------------------------------------------|-----------------------------------------------------------------------|
|
||||
| `topic` | ✔️ | *string* | `topic1` | Target topic name |
|
||||
| `message` | - | *string* | `Some message` | Message body; set to `triggered` if empty or not passed |
|
||||
| `title` | - | *string* | `Some title` | Message [title](#message-title) |
|
||||
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](#tags-emojis) that may or not map to emojis |
|
||||
| `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max |
|
||||
| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications |
|
||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) |
|
||||
| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) |
|
||||
| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted |
|
||||
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
|
||||
| `filename` | - | *string* | `file.jpg` | File name of the attachment |
|
||||
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
|
||||
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
|
||||
| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) |
|
||||
| Field | Required | Type | Example | Description |
|
||||
|---------------|----------|----------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------|
|
||||
| `topic` | ✔️ | *string* | `topic1` | Target topic name |
|
||||
| `message` | - | *string* | `Some message` | Message body; set to `triggered` if empty or not passed |
|
||||
| `title` | - | *string* | `Some title` | Message [title](#message-title) |
|
||||
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](#tags-emojis) that may or not map to emojis |
|
||||
| `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max |
|
||||
| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications |
|
||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) |
|
||||
| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) |
|
||||
| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted |
|
||||
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
|
||||
| `filename` | - | *string* | `file.jpg` | File name of the attachment |
|
||||
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
|
||||
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
|
||||
| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) |
|
||||
| `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](#updating-deleting-notifications) |
|
||||
|
||||
## Action buttons
|
||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||
@@ -3931,6 +4371,7 @@ table in their canonical form.
|
||||
|-----------------|--------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||
| `X-Message` | `Message`, `m` | Main body of the message as shown in the notification |
|
||||
| `X-Title` | `Title`, `t` | [Message title](#message-title) |
|
||||
| `X-Sequence-ID` | `Sequence-ID`, `SID` | [Sequence ID](#updating-deleting-notifications) for updating/clearing/deleting notifications |
|
||||
| `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
|
||||
| `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
|
||||
| `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
|
||||
|
||||
@@ -1599,15 +1599,32 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
## Not released yet
|
||||
|
||||
### ntfy server v2.16.x (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications)
|
||||
([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8)
|
||||
for the initial implementation)
|
||||
|
||||
### ntfy Android app v1.22.x (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for self-signed certs and client certs for mTLS (#215, #530, ntfy-android#149)
|
||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications)
|
||||
([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8)
|
||||
for the initial implementation)
|
||||
* Support for self-signed certs and client certs for mTLS ([#215](https://github.com/binwiederhier/ntfy/issues/215),
|
||||
[#530](https://github.com/binwiederhier/ntfy/issues/530), [ntfy-android#149](https://github.com/binwiederhier/ntfy-android/pull/149),
|
||||
thanks to [@cyb3rko](https://github.com/cyb3rko) for reviewing)
|
||||
* Connection error dialog to help diagnose connection issues
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Use server-specific user for attachment downloads (#1529, thanks to @ManInDark for reporting)
|
||||
* Fix crash in sharing dialog (thanks to @rogeliodh)
|
||||
* Use server-specific user for attachment downloads ([#1529](https://github.com/binwiederhier/ntfy/issues/1529),
|
||||
thanks to [@ManInDark](https://github.com/ManInDark) for reporting and testing)
|
||||
* Fix crash in sharing dialog (thanks to [@rogeliodh](https://github.com/rogeliodh))
|
||||
* Fix crash when exiting multi-delete in detail view
|
||||
* Fix potential crashes with icon downloader and backuper
|
||||
|
||||
18
docs/static/css/extra.css
vendored
18
docs/static/css/extra.css
vendored
@@ -1,10 +1,10 @@
|
||||
:root > * {
|
||||
--md-primary-fg-color: #338574;
|
||||
--md-primary-fg-color: #338574;
|
||||
--md-primary-fg-color--light: #338574;
|
||||
--md-primary-fg-color--dark: #338574;
|
||||
--md-footer-bg-color: #353744;
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Roboto Mono";
|
||||
--md-primary-fg-color--dark: #338574;
|
||||
--md-footer-bg-color: #353744;
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Roboto Mono";
|
||||
}
|
||||
|
||||
.md-header__button.md-logo :is(img, svg) {
|
||||
@@ -34,7 +34,7 @@ figure img, figure video {
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%);
|
||||
background: linear-gradient(150deg, rgba(51, 133, 116, 1) 0%, rgba(86, 189, 168, 1) 100%);
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="default"] header {
|
||||
@@ -93,7 +93,7 @@ figure video {
|
||||
|
||||
.screenshots img {
|
||||
max-height: 230px;
|
||||
max-width: 300px;
|
||||
max-width: 350px;
|
||||
margin: 3px;
|
||||
border-radius: 5px;
|
||||
filter: drop-shadow(2px 2px 2px #ddd);
|
||||
@@ -107,7 +107,7 @@ figure video {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
left:0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -119,7 +119,7 @@ figure video {
|
||||
}
|
||||
|
||||
.lightbox.show {
|
||||
background-color: rgba(0,0,0, 0.75);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 1000;
|
||||
|
||||
BIN
docs/static/img/android-screenshot-notification-update-1.png
vendored
Normal file
BIN
docs/static/img/android-screenshot-notification-update-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/static/img/android-screenshot-notification-update-2.png
vendored
Normal file
BIN
docs/static/img/android-screenshot-notification-update-2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -324,20 +324,21 @@ format of the message. It's very straight forward:
|
||||
|
||||
**Message**:
|
||||
|
||||
| Field | Required | Type | Example | Description |
|
||||
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
|
||||
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
|
||||
| `expires` | (✔)️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent |
|
||||
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
|
||||
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
|
||||
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
|
||||
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
|
||||
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
|
||||
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
|
||||
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
|
||||
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
|
||||
| Field | Required | Type | Example | Description |
|
||||
|---------------|----------|---------------------------------------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
|
||||
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
|
||||
| `expires` | (✔)️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent |
|
||||
| `event` | ✔️ | `open`, `keepalive`, `message`, `message_delete`, `message_clear`, `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
|
||||
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
|
||||
| `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](../publish.md#updating-deleting-notifications) |
|
||||
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
|
||||
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
|
||||
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
|
||||
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
|
||||
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
|
||||
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
|
||||
|
||||
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
|
||||
|
||||
|
||||
10
go.mod
10
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/olebedev/when v1.1.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.39.0
|
||||
@@ -92,12 +92,12 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
47
go.sum
47
go.sum
@@ -14,14 +14,10 @@ cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
|
||||
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8=
|
||||
cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
@@ -100,8 +96,6 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
@@ -118,8 +112,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -139,8 +131,6 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
@@ -194,8 +184,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -210,8 +200,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -235,8 +225,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
@@ -248,8 +236,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -263,8 +249,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
@@ -279,27 +263,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 h1:stRtB2UVzFOWnorVuwF0BVVEjQ3AN6SjHWdg811UIQM=
|
||||
google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b h1:kqShdsddZrS6q+DGBCA73CzHsKDu5vW4qw78tFnbVvY=
|
||||
google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:gw1DtiPCt5uh/HV9STVEeaO00S5ATsJiJ2LsZV8lcDI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 h1:wFALHMUiWKkK/x6rSxm79KpSnUyh7ks2E+mel670Dc4=
|
||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:wE6SUYr3iNtF/D0GxVAjT+0CbDFktQNssYs9PVptCt4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 h1:4DKBrmaqeptdEzp21EfrOEh8LE7PJ5ywH6wydSbOfGY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 h1:IY6/YYRrFUk0JPp0xOVctvFIVuRnjccihY5kxf5g0TE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
|
||||
@@ -3,8 +3,9 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"net/http"
|
||||
|
||||
"heckel.io/ntfy/v2/log"
|
||||
)
|
||||
|
||||
// errHTTP is a generic HTTP error for any non-200 HTTP error
|
||||
@@ -125,6 +126,7 @@ var (
|
||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
||||
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
|
||||
@@ -29,7 +29,9 @@ const (
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mid TEXT NOT NULL,
|
||||
sequence_id TEXT NOT NULL,
|
||||
time INT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
expires INT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
@@ -52,6 +54,7 @@ const (
|
||||
published INT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
|
||||
CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
|
||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||
CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
|
||||
@@ -66,50 +69,50 @@ const (
|
||||
COMMIT;
|
||||
`
|
||||
insertMessageQuery = `
|
||||
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO messages (mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
|
||||
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
|
||||
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
|
||||
selectMessagesByIDQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE mid = ?
|
||||
`
|
||||
selectMessagesSinceTimeQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND time >= ? AND published = 1
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceTimeIncludeScheduledQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND time >= ?
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceIDQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND id > ? AND published = 1
|
||||
WHERE topic = ? AND id > ? AND published = 1
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceIDIncludeScheduledQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND (id > ? OR published = 0)
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesLatestQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND published = 1
|
||||
ORDER BY time DESC, id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
selectMessagesDueQuery = `
|
||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||
FROM messages
|
||||
WHERE time <= ? AND published = 0
|
||||
ORDER BY time, id
|
||||
@@ -131,7 +134,7 @@ const (
|
||||
|
||||
// Schema management queries
|
||||
const (
|
||||
currentSchemaVersion = 13
|
||||
currentSchemaVersion = 14
|
||||
createSchemaVersionTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||
id INT PRIMARY KEY,
|
||||
@@ -260,6 +263,13 @@ const (
|
||||
migrate12To13AlterMessagesTableQuery = `
|
||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||
`
|
||||
|
||||
//13 -> 14
|
||||
migrate13To14AlterMessagesTableQuery = `
|
||||
ALTER TABLE messages ADD COLUMN sequence_id TEXT NOT NULL DEFAULT('');
|
||||
ALTER TABLE messages ADD COLUMN event TEXT NOT NULL DEFAULT('message');
|
||||
CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id);
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -277,6 +287,7 @@ var (
|
||||
10: migrateFrom10,
|
||||
11: migrateFrom11,
|
||||
12: migrateFrom12,
|
||||
13: migrateFrom13,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -369,7 +380,7 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
}
|
||||
defer stmt.Close()
|
||||
for _, m := range ms {
|
||||
if m.Event != messageEvent {
|
||||
if m.Event != messageEvent && m.Event != messageDeleteEvent && m.Event != messageClearEvent {
|
||||
return errUnexpectedMessageType
|
||||
}
|
||||
published := m.Time <= time.Now().Unix()
|
||||
@@ -397,7 +408,9 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
}
|
||||
_, err := stmt.Exec(
|
||||
m.ID,
|
||||
m.SequenceID,
|
||||
m.Time,
|
||||
m.Event,
|
||||
m.Expires,
|
||||
m.Topic,
|
||||
m.Message,
|
||||
@@ -706,10 +719,12 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
func readMessage(rows *sql.Rows) (*message, error) {
|
||||
var timestamp, expires, attachmentSize, attachmentExpires int64
|
||||
var priority int
|
||||
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
|
||||
var id, sequenceID, event, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
|
||||
err := rows.Scan(
|
||||
&id,
|
||||
&sequenceID,
|
||||
×tamp,
|
||||
&event,
|
||||
&expires,
|
||||
&topic,
|
||||
&msg,
|
||||
@@ -758,9 +773,10 @@ func readMessage(rows *sql.Rows) (*message, error) {
|
||||
}
|
||||
return &message{
|
||||
ID: id,
|
||||
SequenceID: sequenceID,
|
||||
Time: timestamp,
|
||||
Expires: expires,
|
||||
Event: messageEvent,
|
||||
Event: event,
|
||||
Topic: topic,
|
||||
Message: msg,
|
||||
Title: title,
|
||||
@@ -1030,3 +1046,19 @@ func migrateFrom12(db *sql.DB, _ time.Duration) error {
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func migrateFrom13(db *sql.DB, _ time.Duration) error {
|
||||
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 13 to 14")
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(migrate13To14AlterMessagesTableQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(updateSchemaVersion, 14); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires1 := time.Now().Add(-4 * time.Hour).Unix() // Expired
|
||||
m := newDefaultMessage("mytopic", "flower for you")
|
||||
m.ID = "m1"
|
||||
m.SequenceID = "m1"
|
||||
m.Sender = netip.MustParseAddr("1.2.3.4")
|
||||
m.Attachment = &attachment{
|
||||
Name: "flower.jpg",
|
||||
@@ -332,6 +333,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires2 := time.Now().Add(2 * time.Hour).Unix() // Future
|
||||
m = newDefaultMessage("mytopic", "sending you a car")
|
||||
m.ID = "m2"
|
||||
m.SequenceID = "m2"
|
||||
m.Sender = netip.MustParseAddr("1.2.3.4")
|
||||
m.Attachment = &attachment{
|
||||
Name: "car.jpg",
|
||||
@@ -345,6 +347,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires3 := time.Now().Add(1 * time.Hour).Unix() // Future
|
||||
m = newDefaultMessage("another-topic", "sending you another car")
|
||||
m.ID = "m3"
|
||||
m.SequenceID = "m3"
|
||||
m.User = "u_BAsbaAa"
|
||||
m.Sender = netip.MustParseAddr("5.6.7.8")
|
||||
m.Attachment = &attachment{
|
||||
@@ -400,11 +403,13 @@ func TestMemCache_Attachments_Expired(t *testing.T) {
|
||||
func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
|
||||
m := newDefaultMessage("mytopic", "flower for you")
|
||||
m.ID = "m1"
|
||||
m.SequenceID = "m1"
|
||||
m.Expires = time.Now().Add(time.Hour).Unix()
|
||||
require.Nil(t, c.AddMessage(m))
|
||||
|
||||
m = newDefaultMessage("mytopic", "message with attachment")
|
||||
m.ID = "m2"
|
||||
m.SequenceID = "m2"
|
||||
m.Expires = time.Now().Add(2 * time.Hour).Unix()
|
||||
m.Attachment = &attachment{
|
||||
Name: "car.jpg",
|
||||
@@ -417,6 +422,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
|
||||
|
||||
m = newDefaultMessage("mytopic", "message with external attachment")
|
||||
m.ID = "m3"
|
||||
m.SequenceID = "m3"
|
||||
m.Expires = time.Now().Add(2 * time.Hour).Unix()
|
||||
m.Attachment = &attachment{
|
||||
Name: "car.jpg",
|
||||
@@ -428,6 +434,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) {
|
||||
|
||||
m = newDefaultMessage("mytopic2", "message with expired attachment")
|
||||
m.ID = "m4"
|
||||
m.SequenceID = "m4"
|
||||
m.Expires = time.Now().Add(2 * time.Hour).Unix()
|
||||
m.Attachment = &attachment{
|
||||
Name: "expired-car.jpg",
|
||||
|
||||
107
server/server.go
107
server/server.go
@@ -80,11 +80,12 @@ var (
|
||||
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
|
||||
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
|
||||
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
||||
updatePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}$`)
|
||||
clearPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}/(read|clear)$`)
|
||||
sequenceIDRegex = topicRegex
|
||||
|
||||
webConfigPath = "/config.js"
|
||||
webManifestPath = "/manifest.webmanifest"
|
||||
webRootHTMLPath = "/app.html"
|
||||
webServiceWorkerPath = "/sw.js"
|
||||
accountPath = "/account"
|
||||
matrixPushPath = "/_matrix/push/v1/notify"
|
||||
metricsPath = "/metrics"
|
||||
@@ -108,7 +109,7 @@ var (
|
||||
apiAccountBillingSubscriptionCheckoutSuccessTemplate = "/v1/account/billing/subscription/success/{CHECKOUT_SESSION_ID}"
|
||||
apiAccountBillingSubscriptionCheckoutSuccessRegex = regexp.MustCompile(`/v1/account/billing/subscription/success/(.+)$`)
|
||||
apiAccountReservationSingleRegex = regexp.MustCompile(`/v1/account/reservation/([-_A-Za-z0-9]{1,64})$`)
|
||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||
staticRegex = regexp.MustCompile(`^/(static/.+|app.html|sw.js|sw.js.map)$`)
|
||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
||||
urlRegex = regexp.MustCompile(`^https?://`)
|
||||
@@ -137,7 +138,7 @@ var (
|
||||
const (
|
||||
firebaseControlTopic = "~control" // See Android if changed
|
||||
firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now)
|
||||
emptyMessageBody = "triggered" // Used if message body is empty
|
||||
emptyMessageBody = "triggered" // Used when a message body is empty
|
||||
newMessageBody = "New message" // Used in poll requests as generic message
|
||||
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
||||
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
|
||||
@@ -531,7 +532,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||
return s.handleMatrixDiscovery(w)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
||||
return s.handleMetrics(w, r, v)
|
||||
} else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) {
|
||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
||||
@@ -543,8 +544,12 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||
return s.transformBodyJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish)))(w, r, v)
|
||||
} else if r.Method == http.MethodPost && r.URL.Path == matrixPushPath {
|
||||
return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v)
|
||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updatePathRegex.MatchString(r.URL.Path)) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
|
||||
} else if r.Method == http.MethodDelete && updatePathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleDelete))(w, r, v)
|
||||
} else if r.Method == http.MethodPut && clearPathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleClear))(w, r, v)
|
||||
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
|
||||
} else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) {
|
||||
@@ -872,7 +877,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
||||
return err
|
||||
}
|
||||
minc(metricMessagesPublishedSuccess)
|
||||
return s.writeJSON(w, m)
|
||||
return s.writeJSON(w, m.forJSON())
|
||||
}
|
||||
|
||||
func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
@@ -900,6 +905,58 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *
|
||||
return writeMatrixSuccess(w)
|
||||
}
|
||||
|
||||
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
return s.handleActionMessage(w, r, v, messageDeleteEvent)
|
||||
}
|
||||
|
||||
func (s *Server) handleClear(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
return s.handleActionMessage(w, r, v, messageClearEvent)
|
||||
}
|
||||
|
||||
func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *visitor, event string) error {
|
||||
t, err := fromContext[*topic](r, contextTopic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vrate, err := fromContext[*visitor](r, contextRateVisitor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
|
||||
return errHTTPTooManyRequestsLimitMessages.With(t)
|
||||
}
|
||||
sequenceID, e := s.sequenceIDFromPath(r.URL.Path)
|
||||
if e != nil {
|
||||
return e.With(t)
|
||||
}
|
||||
// Create an action message with the given event type
|
||||
m := newActionMessage(event, t.ID, sequenceID)
|
||||
m.Sender = v.IP()
|
||||
m.User = v.MaybeUserID()
|
||||
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
||||
// Publish to subscribers
|
||||
if err := t.Publish(v, m); err != nil {
|
||||
return err
|
||||
}
|
||||
// Send to Firebase for Android clients
|
||||
if s.firebaseClient != nil {
|
||||
go s.sendToFirebase(v, m)
|
||||
}
|
||||
// Send to web push endpoints
|
||||
if s.config.WebPushPublicKey != "" {
|
||||
go s.publishToWebPushEndpoints(v, m)
|
||||
}
|
||||
// Add to message cache
|
||||
if err := s.messageCache.AddMessage(m); err != nil {
|
||||
return err
|
||||
}
|
||||
logvrm(v, r, m).Tag(tagPublish).Debug("Published %s for sequence ID %s", event, sequenceID)
|
||||
s.mu.Lock()
|
||||
s.messages++
|
||||
s.mu.Unlock()
|
||||
return s.writeJSON(w, m.forJSON())
|
||||
}
|
||||
|
||||
func (s *Server) sendToFirebase(v *visitor, m *message) {
|
||||
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
|
||||
if err := s.firebaseClient.Send(v, m); err != nil {
|
||||
@@ -957,6 +1014,24 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||
}
|
||||
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) {
|
||||
if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) {
|
||||
pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path)
|
||||
if err != nil {
|
||||
return false, false, "", "", "", false, err
|
||||
}
|
||||
m.SequenceID = pathSequenceID
|
||||
} else {
|
||||
sequenceID := readParam(r, "x-sequence-id", "sequence-id", "sid")
|
||||
if sequenceID != "" {
|
||||
if sequenceIDRegex.MatchString(sequenceID) {
|
||||
m.SequenceID = sequenceID
|
||||
} else {
|
||||
return false, false, "", "", "", false, errHTTPBadRequestSequenceIDInvalid
|
||||
}
|
||||
} else {
|
||||
m.SequenceID = m.ID
|
||||
}
|
||||
}
|
||||
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||
m.Title = readParam(r, "x-title", "title", "t")
|
||||
@@ -1271,7 +1346,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||
func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
encoder := func(msg *message) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
@@ -1282,10 +1357,10 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *
|
||||
func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
encoder := func(msg *message) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if msg.Event != messageEvent {
|
||||
if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageClearEvent {
|
||||
return fmt.Sprintf("event: %s\ndata: %s\n", msg.Event, buf.String()), nil // Browser's .onmessage() does not fire on this!
|
||||
}
|
||||
return fmt.Sprintf("data: %s\n", buf.String()), nil
|
||||
@@ -1695,6 +1770,15 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {
|
||||
return topics, parts[1], nil
|
||||
}
|
||||
|
||||
// sequenceIDFromPath returns the sequence ID from a path like /mytopic/sequenceIdHere
|
||||
func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 3 {
|
||||
return "", errHTTPBadRequestSequenceIDInvalid
|
||||
}
|
||||
return parts[2], nil
|
||||
}
|
||||
|
||||
// topicsFromIDs returns the topics with the given IDs, creating them if they don't exist.
|
||||
func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
|
||||
s.mu.Lock()
|
||||
@@ -1949,6 +2033,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||
if m.Firebase != "" {
|
||||
r.Header.Set("X-Firebase", m.Firebase)
|
||||
}
|
||||
if m.SequenceID != "" {
|
||||
r.Header.Set("X-Sequence-ID", m.SequenceID)
|
||||
}
|
||||
return next(w, r, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,15 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
|
||||
"poll_id": m.PollID,
|
||||
}
|
||||
apnsConfig = createAPNSAlertConfig(m, data)
|
||||
case messageDeleteEvent, messageClearEvent:
|
||||
data = map[string]string{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
"topic": m.Topic,
|
||||
"sequence_id": m.SequenceID,
|
||||
}
|
||||
apnsConfig = createAPNSBackgroundConfig(data)
|
||||
case messageEvent:
|
||||
if auther != nil {
|
||||
// If "anonymous read" for a topic is not allowed, we cannot send the message along
|
||||
@@ -161,6 +170,7 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
"topic": m.Topic,
|
||||
"sequence_id": m.SequenceID,
|
||||
"priority": fmt.Sprintf("%d", m.Priority),
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": m.Click,
|
||||
|
||||
@@ -177,6 +177,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": "message",
|
||||
"topic": "mytopic",
|
||||
"sequence_id": "",
|
||||
"priority": "4",
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": "https://google.com",
|
||||
@@ -199,6 +200,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": "message",
|
||||
"topic": "mytopic",
|
||||
"sequence_id": "",
|
||||
"priority": "4",
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": "https://google.com",
|
||||
@@ -232,6 +234,7 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": "poll_request",
|
||||
"topic": "mytopic",
|
||||
"sequence_id": "",
|
||||
"message": "New message",
|
||||
"title": "",
|
||||
"tags": "",
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -24,7 +22,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
@@ -678,6 +678,86 @@ func TestServer_PublishInvalidTopic(t *testing.T) {
|
||||
require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithSIDInPath(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic/sid", "message", nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "sid", msg.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithSIDInHeader(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic", "message", map[string]string{
|
||||
"sid": "sid",
|
||||
})
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "sid", msg.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{
|
||||
"sid": "sid2",
|
||||
})
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header
|
||||
}
|
||||
|
||||
func TestServer_PublishWithSIDInQuery(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "sid1", msg.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithSIDViaGet(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "sid1", msg.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_PublishAsJSON_WithSequenceID(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
body := `{"topic":"mytopic","message":"A message","sequence_id":"my-sequence-123"}`
|
||||
response := request(t, s, "PUT", "/", body, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "my-sequence-123", msg.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithInvalidSIDInPath(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic/.", "message", nil)
|
||||
|
||||
require.Equal(t, 404, response.Code)
|
||||
}
|
||||
|
||||
func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic", "message", map[string]string{
|
||||
"X-Sequence-ID": "*&?",
|
||||
})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_PollWithQueryFilters(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
@@ -3209,6 +3289,212 @@ func TestServer_MessageTemplate_Until100_000(t *testing.T) {
|
||||
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
|
||||
}
|
||||
|
||||
func TestServer_DeleteMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Publish a message with a sequence ID
|
||||
response := request(t, s, "PUT", "/mytopic/seq123", "original message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "seq123", msg.SequenceID)
|
||||
require.Equal(t, "message", msg.Event)
|
||||
|
||||
// Delete the message using DELETE method
|
||||
response = request(t, s, "DELETE", "/mytopic/seq123", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
deleteMsg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "seq123", deleteMsg.SequenceID)
|
||||
require.Equal(t, "message_delete", deleteMsg.Event)
|
||||
|
||||
// Poll and verify both messages are returned
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
|
||||
require.Equal(t, 2, len(lines))
|
||||
|
||||
msg1 := toMessage(t, lines[0])
|
||||
msg2 := toMessage(t, lines[1])
|
||||
require.Equal(t, "message", msg1.Event)
|
||||
require.Equal(t, "message_delete", msg2.Event)
|
||||
require.Equal(t, "seq123", msg1.SequenceID)
|
||||
require.Equal(t, "seq123", msg2.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_ClearMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Publish a message with a sequence ID
|
||||
response := request(t, s, "PUT", "/mytopic/seq456", "original message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "seq456", msg.SequenceID)
|
||||
require.Equal(t, "message", msg.Event)
|
||||
|
||||
// Clear the message using PUT /topic/seq/clear
|
||||
response = request(t, s, "PUT", "/mytopic/seq456/clear", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
clearMsg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "seq456", clearMsg.SequenceID)
|
||||
require.Equal(t, "message_clear", clearMsg.Event)
|
||||
|
||||
// Poll and verify both messages are returned
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
|
||||
require.Equal(t, 2, len(lines))
|
||||
|
||||
msg1 := toMessage(t, lines[0])
|
||||
msg2 := toMessage(t, lines[1])
|
||||
require.Equal(t, "message", msg1.Event)
|
||||
require.Equal(t, "message_clear", msg2.Event)
|
||||
require.Equal(t, "seq456", msg1.SequenceID)
|
||||
require.Equal(t, "seq456", msg2.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_ClearMessage_ReadEndpoint(t *testing.T) {
|
||||
// Test that /topic/seq/read also works
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Publish a message
|
||||
response := request(t, s, "PUT", "/mytopic/seq789", "original message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Clear using /read endpoint
|
||||
response = request(t, s, "PUT", "/mytopic/seq789/read", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
clearMsg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "seq789", clearMsg.SequenceID)
|
||||
require.Equal(t, "message_clear", clearMsg.Event)
|
||||
}
|
||||
|
||||
func TestServer_UpdateMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Publish original message
|
||||
response := request(t, s, "PUT", "/mytopic/update-seq", "original message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg1 := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "update-seq", msg1.SequenceID)
|
||||
require.Equal(t, "original message", msg1.Message)
|
||||
|
||||
// Update the message (same sequence ID, new content)
|
||||
response = request(t, s, "PUT", "/mytopic/update-seq", "updated message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg2 := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "update-seq", msg2.SequenceID)
|
||||
require.Equal(t, "updated message", msg2.Message)
|
||||
require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs
|
||||
|
||||
// Poll and verify both versions are returned
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
|
||||
require.Equal(t, 2, len(lines))
|
||||
|
||||
polledMsg1 := toMessage(t, lines[0])
|
||||
polledMsg2 := toMessage(t, lines[1])
|
||||
require.Equal(t, "original message", polledMsg1.Message)
|
||||
require.Equal(t, "updated message", polledMsg2.Message)
|
||||
require.Equal(t, "update-seq", polledMsg1.SequenceID)
|
||||
require.Equal(t, "update-seq", polledMsg2.SequenceID)
|
||||
}
|
||||
|
||||
func TestServer_UpdateMessage_UsingMessageID(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Publish original message without a sequence ID
|
||||
response := request(t, s, "PUT", "/mytopic", "original message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg1 := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg1.ID)
|
||||
require.Empty(t, msg1.SequenceID) // No sequence ID provided
|
||||
require.Equal(t, "original message", msg1.Message)
|
||||
|
||||
// Update the message using the message ID as the sequence ID
|
||||
response = request(t, s, "PUT", "/mytopic/"+msg1.ID, "updated message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
msg2 := toMessage(t, response.Body.String())
|
||||
require.Equal(t, msg1.ID, msg2.SequenceID) // Message ID is now used as sequence ID
|
||||
require.Equal(t, "updated message", msg2.Message)
|
||||
require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs
|
||||
|
||||
// Poll and verify both versions are returned
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
|
||||
require.Equal(t, 2, len(lines))
|
||||
|
||||
polledMsg1 := toMessage(t, lines[0])
|
||||
polledMsg2 := toMessage(t, lines[1])
|
||||
require.Equal(t, "original message", polledMsg1.Message)
|
||||
require.Equal(t, "updated message", polledMsg2.Message)
|
||||
require.Empty(t, polledMsg1.SequenceID) // Original has no sequence ID
|
||||
require.Equal(t, msg1.ID, polledMsg2.SequenceID) // Update uses original message ID as sequence ID
|
||||
}
|
||||
|
||||
func TestServer_DeleteAndClear_InvalidSequenceID(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Test invalid sequence ID for delete (returns 404 because route doesn't match)
|
||||
response := request(t, s, "DELETE", "/mytopic/invalid*seq", "", nil)
|
||||
require.Equal(t, 404, response.Code)
|
||||
|
||||
// Test invalid sequence ID for clear (returns 404 because route doesn't match)
|
||||
response = request(t, s, "PUT", "/mytopic/invalid*seq/clear", "", nil)
|
||||
require.Equal(t, 404, response.Code)
|
||||
}
|
||||
|
||||
func TestServer_DeleteMessage_WithFirebase(t *testing.T) {
|
||||
sender := newTestFirebaseSender(10)
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
|
||||
|
||||
// Publish a message
|
||||
response := request(t, s, "PUT", "/mytopic/firebase-seq", "test message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Firebase publishing happens
|
||||
require.Equal(t, 1, len(sender.Messages()))
|
||||
require.Equal(t, "message", sender.Messages()[0].Data["event"])
|
||||
|
||||
// Delete the message
|
||||
response = request(t, s, "DELETE", "/mytopic/firebase-seq", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Firebase publishing happens
|
||||
require.Equal(t, 2, len(sender.Messages()))
|
||||
require.Equal(t, "message_delete", sender.Messages()[1].Data["event"])
|
||||
require.Equal(t, "firebase-seq", sender.Messages()[1].Data["sequence_id"])
|
||||
}
|
||||
|
||||
func TestServer_ClearMessage_WithFirebase(t *testing.T) {
|
||||
sender := newTestFirebaseSender(10)
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
|
||||
|
||||
// Publish a message
|
||||
response := request(t, s, "PUT", "/mytopic/firebase-clear-seq", "test message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.Equal(t, 1, len(sender.Messages()))
|
||||
|
||||
// Clear the message
|
||||
response = request(t, s, "PUT", "/mytopic/firebase-clear-seq/clear", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.Equal(t, 2, len(sender.Messages()))
|
||||
require.Equal(t, "message_clear", sender.Messages()[1].Data["event"])
|
||||
require.Equal(t, "firebase-clear-seq", sender.Messages()[1].Data["sequence_id"])
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *Config {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
|
||||
@@ -89,7 +89,7 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||
return
|
||||
}
|
||||
log.Tag(tagWebPush).With(v, m).Debug("Publishing web push message to %d subscribers", len(subscriptions))
|
||||
payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m))
|
||||
payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m.forJSON()))
|
||||
if err != nil {
|
||||
log.Tag(tagWebPush).Err(err).With(v, m).Warn("Unable to marshal expiring payload")
|
||||
return
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
|
||||
// List of possible events
|
||||
const (
|
||||
openEvent = "open"
|
||||
keepaliveEvent = "keepalive"
|
||||
messageEvent = "message"
|
||||
pollRequestEvent = "poll_request"
|
||||
openEvent = "open"
|
||||
keepaliveEvent = "keepalive"
|
||||
messageEvent = "message"
|
||||
messageDeleteEvent = "message_delete"
|
||||
messageClearEvent = "message_clear"
|
||||
pollRequestEvent = "poll_request"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,10 +26,11 @@ const (
|
||||
|
||||
// message represents a message published to a topic
|
||||
type message struct {
|
||||
ID string `json:"id"` // Random message ID
|
||||
Time int64 `json:"time"` // Unix time in seconds
|
||||
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
||||
Event string `json:"event"` // One of the above
|
||||
ID string `json:"id"` // Random message ID
|
||||
SequenceID string `json:"sequence_id,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID)
|
||||
Time int64 `json:"time"` // Unix time in seconds
|
||||
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
||||
Event string `json:"event"` // One of the above
|
||||
Topic string `json:"topic"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
@@ -39,18 +42,19 @@ type message struct {
|
||||
Attachment *attachment `json:"attachment,omitempty"`
|
||||
PollID string `json:"poll_id,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
|
||||
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
||||
Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes
|
||||
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
||||
User string `json:"-"` // UserID of the uploader, used to associated attachments
|
||||
}
|
||||
|
||||
func (m *message) Context() log.Context {
|
||||
fields := map[string]any{
|
||||
"topic": m.Topic,
|
||||
"message_id": m.ID,
|
||||
"message_time": m.Time,
|
||||
"message_event": m.Event,
|
||||
"message_body_size": len(m.Message),
|
||||
"topic": m.Topic,
|
||||
"message_id": m.ID,
|
||||
"message_sequence_id": m.SequenceID,
|
||||
"message_time": m.Time,
|
||||
"message_event": m.Event,
|
||||
"message_body_size": len(m.Message),
|
||||
}
|
||||
if m.Sender.IsValid() {
|
||||
fields["message_sender"] = m.Sender.String()
|
||||
@@ -61,6 +65,17 @@ func (m *message) Context() log.Context {
|
||||
return fields
|
||||
}
|
||||
|
||||
// forJSON returns a copy of the message suitable for JSON output.
|
||||
// It clears the SequenceID if it equals the ID to reduce redundancy.
|
||||
func (m *message) forJSON() *message {
|
||||
if m.SequenceID == m.ID {
|
||||
clone := *m
|
||||
clone.SequenceID = ""
|
||||
return &clone
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type attachment struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type,omitempty"`
|
||||
@@ -91,22 +106,23 @@ func newAction() *action {
|
||||
|
||||
// publishMessage is used as input when publishing as JSON
|
||||
type publishMessage struct {
|
||||
Topic string `json:"topic"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Priority int `json:"priority"`
|
||||
Tags []string `json:"tags"`
|
||||
Click string `json:"click"`
|
||||
Icon string `json:"icon"`
|
||||
Actions []action `json:"actions"`
|
||||
Attach string `json:"attach"`
|
||||
Markdown bool `json:"markdown"`
|
||||
Filename string `json:"filename"`
|
||||
Email string `json:"email"`
|
||||
Call string `json:"call"`
|
||||
Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead)
|
||||
Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead)
|
||||
Delay string `json:"delay"`
|
||||
Topic string `json:"topic"`
|
||||
SequenceID string `json:"sequence_id"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Priority int `json:"priority"`
|
||||
Tags []string `json:"tags"`
|
||||
Click string `json:"click"`
|
||||
Icon string `json:"icon"`
|
||||
Actions []action `json:"actions"`
|
||||
Attach string `json:"attach"`
|
||||
Markdown bool `json:"markdown"`
|
||||
Filename string `json:"filename"`
|
||||
Email string `json:"email"`
|
||||
Call string `json:"call"`
|
||||
Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead)
|
||||
Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead)
|
||||
Delay string `json:"delay"`
|
||||
}
|
||||
|
||||
// messageEncoder is a function that knows how to encode a message
|
||||
@@ -145,6 +161,13 @@ func newPollRequestMessage(topic, pollID string) *message {
|
||||
return m
|
||||
}
|
||||
|
||||
// newActionMessage creates a new action message (message_delete or message_clear)
|
||||
func newActionMessage(event, topic, sequenceID string) *message {
|
||||
m := newMessage(event, topic, "")
|
||||
m.SequenceID = sequenceID
|
||||
return m
|
||||
}
|
||||
|
||||
func validMessageID(s string) bool {
|
||||
return util.ValidRandomString(s, messageIDLength)
|
||||
}
|
||||
@@ -223,7 +246,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
||||
}
|
||||
|
||||
func (q *queryFilter) Pass(msg *message) bool {
|
||||
if msg.Event != messageEvent {
|
||||
if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageClearEvent {
|
||||
return true // filters only apply to messages
|
||||
} else if q.ID != "" && msg.ID != q.ID {
|
||||
return false
|
||||
|
||||
526
web/package-lock.json
generated
526
web/package-lock.json
generated
@@ -46,12 +46,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
|
||||
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -60,9 +60,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
|
||||
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
|
||||
"integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -70,21 +70,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.4",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -108,13 +108,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
|
||||
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
|
||||
"integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -137,13 +137,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
||||
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
@@ -154,18 +154,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz",
|
||||
"integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
|
||||
"integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||
"@babel/helper-member-expression-to-functions": "^7.28.5",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.28.6",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.5",
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -234,28 +234,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
||||
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
||||
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
||||
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.3"
|
||||
"@babel/helper-module-imports": "^7.28.6",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -278,9 +278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
||||
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -306,15 +306,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-replace-supers": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
|
||||
"integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
|
||||
"integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.27.1",
|
||||
"@babel/helper-member-expression-to-functions": "^7.28.5",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -366,41 +366,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-wrap-function": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz",
|
||||
"integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz",
|
||||
"integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2"
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
|
||||
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
||||
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.4"
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
|
||||
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
"@babel/types": "^7.28.6"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -477,14 +477,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz",
|
||||
"integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
|
||||
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.3"
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -507,13 +507,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-assertions": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz",
|
||||
"integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
|
||||
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -523,13 +523,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
|
||||
"integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
|
||||
"integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -572,15 +572,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
|
||||
"integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz",
|
||||
"integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.0"
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -590,14 +590,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-async-to-generator": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz",
|
||||
"integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz",
|
||||
"integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-module-imports": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-remap-async-to-generator": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -624,13 +624,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-block-scoping": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz",
|
||||
"integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz",
|
||||
"integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -640,14 +640,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-properties": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
|
||||
"integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz",
|
||||
"integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -657,14 +657,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-static-block": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz",
|
||||
"integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
|
||||
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.3",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -674,18 +674,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-classes": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
|
||||
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz",
|
||||
"integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.4"
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-replace-supers": "^7.28.6",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -695,14 +695,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-computed-properties": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
|
||||
"integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz",
|
||||
"integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/template": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/template": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -729,14 +729,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dotall-regex": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz",
|
||||
"integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -762,14 +762,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz",
|
||||
"integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -795,14 +795,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-explicit-resource-management": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
|
||||
"integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
|
||||
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.0"
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -812,13 +812,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz",
|
||||
"integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
|
||||
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -879,13 +879,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-json-strings": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz",
|
||||
"integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
|
||||
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -911,13 +911,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz",
|
||||
"integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz",
|
||||
"integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -960,14 +960,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-commonjs": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
|
||||
"integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz",
|
||||
"integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1046,13 +1046,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
|
||||
"integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz",
|
||||
"integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1062,13 +1062,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-numeric-separator": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz",
|
||||
"integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz",
|
||||
"integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1078,17 +1078,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
|
||||
"integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz",
|
||||
"integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.0",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.5",
|
||||
"@babel/plugin-transform-parameters": "^7.27.7",
|
||||
"@babel/traverse": "^7.28.4"
|
||||
"@babel/traverse": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1115,13 +1115,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-catch-binding": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz",
|
||||
"integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz",
|
||||
"integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1131,13 +1131,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-chaining": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz",
|
||||
"integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz",
|
||||
"integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1164,14 +1164,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-private-methods": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz",
|
||||
"integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz",
|
||||
"integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1181,15 +1181,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-private-property-in-object": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz",
|
||||
"integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz",
|
||||
"integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1247,13 +1247,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regenerator": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
|
||||
"integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz",
|
||||
"integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1263,14 +1263,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regexp-modifiers": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz",
|
||||
"integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
|
||||
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1312,13 +1312,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-spread": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz",
|
||||
"integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz",
|
||||
"integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1393,14 +1393,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-property-regex": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz",
|
||||
"integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1427,14 +1427,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz",
|
||||
"integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
|
||||
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1444,76 +1444,76 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-env": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz",
|
||||
"integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz",
|
||||
"integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.28.5",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
|
||||
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
|
||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
|
||||
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3",
|
||||
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.27.1",
|
||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.28.6",
|
||||
"@babel/plugin-syntax-import-attributes": "^7.28.6",
|
||||
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||
"@babel/plugin-transform-async-generator-functions": "^7.28.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.27.1",
|
||||
"@babel/plugin-transform-async-generator-functions": "^7.28.6",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.28.6",
|
||||
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
|
||||
"@babel/plugin-transform-block-scoping": "^7.28.5",
|
||||
"@babel/plugin-transform-class-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-class-static-block": "^7.28.3",
|
||||
"@babel/plugin-transform-classes": "^7.28.4",
|
||||
"@babel/plugin-transform-computed-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-block-scoping": "^7.28.6",
|
||||
"@babel/plugin-transform-class-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-class-static-block": "^7.28.6",
|
||||
"@babel/plugin-transform-classes": "^7.28.6",
|
||||
"@babel/plugin-transform-computed-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.5",
|
||||
"@babel/plugin-transform-dotall-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-dotall-regex": "^7.28.6",
|
||||
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6",
|
||||
"@babel/plugin-transform-dynamic-import": "^7.27.1",
|
||||
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
|
||||
"@babel/plugin-transform-exponentiation-operator": "^7.28.5",
|
||||
"@babel/plugin-transform-explicit-resource-management": "^7.28.6",
|
||||
"@babel/plugin-transform-exponentiation-operator": "^7.28.6",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-for-of": "^7.27.1",
|
||||
"@babel/plugin-transform-function-name": "^7.27.1",
|
||||
"@babel/plugin-transform-json-strings": "^7.27.1",
|
||||
"@babel/plugin-transform-json-strings": "^7.28.6",
|
||||
"@babel/plugin-transform-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.28.5",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.28.6",
|
||||
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-amd": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/plugin-transform-modules-systemjs": "^7.28.5",
|
||||
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-new-target": "^7.27.1",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.27.1",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.28.4",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.28.6",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.28.6",
|
||||
"@babel/plugin-transform-object-super": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.28.5",
|
||||
"@babel/plugin-transform-optional-catch-binding": "^7.28.6",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.28.6",
|
||||
"@babel/plugin-transform-parameters": "^7.27.7",
|
||||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.28.6",
|
||||
"@babel/plugin-transform-property-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-regenerator": "^7.28.4",
|
||||
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
|
||||
"@babel/plugin-transform-regenerator": "^7.28.6",
|
||||
"@babel/plugin-transform-regexp-modifiers": "^7.28.6",
|
||||
"@babel/plugin-transform-reserved-words": "^7.27.1",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-spread": "^7.27.1",
|
||||
"@babel/plugin-transform-spread": "^7.28.6",
|
||||
"@babel/plugin-transform-sticky-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-template-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-typeof-symbol": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-escapes": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-property-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-property-regex": "^7.28.6",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
|
||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.14",
|
||||
"babel-plugin-polyfill-corejs3": "^0.13.0",
|
||||
@@ -1544,40 +1544,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
|
||||
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
|
||||
"integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.28.6",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1585,9 +1585,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
|
||||
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@@ -3823,9 +3823,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001763",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz",
|
||||
"integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==",
|
||||
"version": "1.0.30001764",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
|
||||
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
147
web/public/sw.js
147
web/public/sw.js
@@ -3,11 +3,16 @@ import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from
|
||||
import { NavigationRoute, registerRoute } from "workbox-routing";
|
||||
import { NetworkFirst } from "workbox-strategies";
|
||||
import { clientsClaim } from "workbox-core";
|
||||
|
||||
import { dbAsync } from "../src/app/db";
|
||||
|
||||
import { toNotificationParams, icon, badge } from "../src/app/notificationUtils";
|
||||
import { badge, icon, messageWithSequenceId, toNotificationParams } from "../src/app/notificationUtils";
|
||||
import initI18n from "../src/app/i18n";
|
||||
import {
|
||||
EVENT_MESSAGE,
|
||||
EVENT_MESSAGE_CLEAR,
|
||||
EVENT_MESSAGE_DELETE,
|
||||
WEBPUSH_EVENT_MESSAGE,
|
||||
WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING,
|
||||
} from "../src/app/events";
|
||||
|
||||
/**
|
||||
* General docs for service workers and PWAs:
|
||||
@@ -21,25 +26,6 @@ import initI18n from "../src/app/i18n";
|
||||
|
||||
const broadcastChannel = new BroadcastChannel("web-push-broadcast");
|
||||
|
||||
const addNotification = async ({ subscriptionId, message }) => {
|
||||
const db = await dbAsync();
|
||||
|
||||
await db.notifications.add({
|
||||
...message,
|
||||
subscriptionId,
|
||||
// New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
new: 1,
|
||||
});
|
||||
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
});
|
||||
|
||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||
console.log("[ServiceWorker] Setting new app badge count", { badgeCount });
|
||||
self.navigator.setAppBadge?.(badgeCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a received web push message and show notification.
|
||||
*
|
||||
@@ -48,10 +34,35 @@ const addNotification = async ({ subscriptionId, message }) => {
|
||||
*/
|
||||
const handlePushMessage = async (data) => {
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
const db = await dbAsync();
|
||||
|
||||
broadcastChannel.postMessage(message); // To potentially play sound
|
||||
console.log("[ServiceWorker] Message received", data);
|
||||
|
||||
// Delete existing notification with same sequence ID (if any)
|
||||
const sequenceId = message.sequence_id || message.id;
|
||||
if (sequenceId) {
|
||||
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||
}
|
||||
|
||||
// Add notification to database
|
||||
await db.notifications.add({
|
||||
...messageWithSequenceId(message),
|
||||
subscriptionId,
|
||||
new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
});
|
||||
|
||||
// Update subscription last message id (for ?since=... queries)
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
});
|
||||
|
||||
// Update badge in PWA
|
||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||
self.navigator.setAppBadge?.(badgeCount);
|
||||
|
||||
// Broadcast the message to potentially play a sound
|
||||
broadcastChannel.postMessage(message);
|
||||
|
||||
await addNotification({ subscriptionId, message });
|
||||
await self.registration.showNotification(
|
||||
...toNotificationParams({
|
||||
subscriptionId,
|
||||
@@ -62,11 +73,70 @@ const handlePushMessage = async (data) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a message_delete event: delete the notification from the database.
|
||||
*/
|
||||
const handlePushMessageDelete = async (data) => {
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
const db = await dbAsync();
|
||||
console.log("[ServiceWorker] Deleting notification sequence", data);
|
||||
|
||||
// Delete notification with the same sequence_id
|
||||
const sequenceId = message.sequence_id;
|
||||
if (sequenceId) {
|
||||
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||
}
|
||||
|
||||
// Close browser notification with matching tag
|
||||
const tag = message.sequence_id || message.id;
|
||||
if (tag) {
|
||||
const notifications = await self.registration.getNotifications({ tag });
|
||||
notifications.forEach((notification) => notification.close());
|
||||
}
|
||||
|
||||
// Update subscription last message id (for ?since=... queries)
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a message_clear event: clear/dismiss the notification.
|
||||
*/
|
||||
const handlePushMessageClear = async (data) => {
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
const db = await dbAsync();
|
||||
console.log("[ServiceWorker] Marking notification as read", data);
|
||||
|
||||
// Mark notification as read (set new = 0)
|
||||
const sequenceId = message.sequence_id;
|
||||
if (sequenceId) {
|
||||
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
||||
}
|
||||
|
||||
// Close browser notification with matching tag
|
||||
const tag = message.sequence_id || message.id;
|
||||
if (tag) {
|
||||
const notifications = await self.registration.getNotifications({ tag });
|
||||
notifications.forEach((notification) => notification.close());
|
||||
}
|
||||
|
||||
// Update subscription last message id (for ?since=... queries)
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
last: message.id,
|
||||
});
|
||||
|
||||
// Update badge count
|
||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||
self.navigator.setAppBadge?.(badgeCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a received web push subscription expiring.
|
||||
*/
|
||||
const handlePushSubscriptionExpiring = async (data) => {
|
||||
const t = await initI18n();
|
||||
console.log("[ServiceWorker] Handling incoming subscription expiring event", data);
|
||||
|
||||
await self.registration.showNotification(t("web_push_subscription_expiring_title"), {
|
||||
body: t("web_push_subscription_expiring_body"),
|
||||
@@ -82,6 +152,7 @@ const handlePushSubscriptionExpiring = async (data) => {
|
||||
*/
|
||||
const handlePushUnknown = async (data) => {
|
||||
const t = await initI18n();
|
||||
console.log("[ServiceWorker] Unknown event received", data);
|
||||
|
||||
await self.registration.showNotification(t("web_push_unknown_notification_title"), {
|
||||
body: t("web_push_unknown_notification_body"),
|
||||
@@ -96,13 +167,26 @@ const handlePushUnknown = async (data) => {
|
||||
* @param {object} data see server/types.go, type webPushPayload
|
||||
*/
|
||||
const handlePush = async (data) => {
|
||||
if (data.event === "message") {
|
||||
await handlePushMessage(data);
|
||||
} else if (data.event === "subscription_expiring") {
|
||||
await handlePushSubscriptionExpiring(data);
|
||||
} else {
|
||||
await handlePushUnknown(data);
|
||||
// This logic is (partially) duplicated in
|
||||
// - Android: SubscriberService::onNotificationReceived()
|
||||
// - Android: FirebaseService::onMessageReceived()
|
||||
// - Web app: hooks.js:handleNotification()
|
||||
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
||||
|
||||
if (data.event === WEBPUSH_EVENT_MESSAGE) {
|
||||
const { message } = data;
|
||||
if (message.event === EVENT_MESSAGE) {
|
||||
return await handlePushMessage(data);
|
||||
} else if (message.event === EVENT_MESSAGE_DELETE) {
|
||||
return await handlePushMessageDelete(data);
|
||||
} else if (message.event === EVENT_MESSAGE_CLEAR) {
|
||||
return await handlePushMessageClear(data);
|
||||
}
|
||||
} else if (data.event === WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING) {
|
||||
return await handlePushSubscriptionExpiring(data);
|
||||
}
|
||||
|
||||
return await handlePushUnknown(data);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -113,10 +197,8 @@ const handleClick = async (event) => {
|
||||
const t = await initI18n();
|
||||
|
||||
const clients = await self.clients.matchAll({ type: "window" });
|
||||
|
||||
const rootUrl = new URL(self.location.origin);
|
||||
const rootClient = clients.find((client) => client.url === rootUrl.toString());
|
||||
// perhaps open on another topic
|
||||
const fallbackClient = clients[0];
|
||||
|
||||
if (!event.notification.data?.message) {
|
||||
@@ -232,6 +314,7 @@ precacheAndRoute(
|
||||
|
||||
// Claim all open windows
|
||||
clientsClaim();
|
||||
|
||||
// Delete any cached old dist files from previous service worker versions
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
|
||||
import { EVENT_OPEN, isNotificationEvent } from "./events";
|
||||
|
||||
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
||||
|
||||
@@ -48,10 +49,11 @@ class Connection {
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event === "open") {
|
||||
if (data.event === EVENT_OPEN) {
|
||||
return;
|
||||
}
|
||||
const relevantAndValid = data.event === "message" && "id" in data && "time" in data && "message" in data;
|
||||
// Accept message, message_delete, and message_clear events
|
||||
const relevantAndValid = isNotificationEvent(data.event) && "id" in data && "time" in data;
|
||||
if (!relevantAndValid) {
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`);
|
||||
return;
|
||||
|
||||
@@ -31,6 +31,21 @@ class Notifier {
|
||||
);
|
||||
}
|
||||
|
||||
async cancel(notification) {
|
||||
if (!this.supported()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tag = notification.sequence_id || notification.id;
|
||||
console.log(`[Notifier] Cancelling notification with ${tag}`);
|
||||
const registration = await this.serviceWorkerRegistration();
|
||||
const notifications = await registration.getNotifications({ tag });
|
||||
notifications.forEach((n) => n.close());
|
||||
} catch (e) {
|
||||
console.log(`[Notifier] Error cancelling notification`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async playSound() {
|
||||
// Play sound
|
||||
const sound = await prefs.sound();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import api from "./Api";
|
||||
import prefs from "./Prefs";
|
||||
import subscriptionManager from "./SubscriptionManager";
|
||||
import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE } from "./events";
|
||||
|
||||
const delayMillis = 2000; // 2 seconds
|
||||
const intervalMillis = 300000; // 5 minutes
|
||||
@@ -42,12 +44,35 @@ class Poller {
|
||||
|
||||
const since = subscription.last;
|
||||
const notifications = await api.poll(subscription.baseUrl, subscription.topic, since);
|
||||
if (!notifications || notifications.length === 0) {
|
||||
console.log(`[Poller] No new notifications found for ${subscription.id}`);
|
||||
return;
|
||||
|
||||
// Filter out notifications older than the prune threshold
|
||||
const deleteAfterSeconds = await prefs.deleteAfter();
|
||||
const pruneThresholdTimestamp = deleteAfterSeconds > 0 ? Math.round(Date.now() / 1000) - deleteAfterSeconds : 0;
|
||||
const recentNotifications =
|
||||
pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications;
|
||||
|
||||
// Find the latest notification for each sequence ID
|
||||
const latestBySequenceId = this.latestNotificationsBySequenceId(recentNotifications);
|
||||
|
||||
// Delete all existing notifications for which the latest notification is marked as deleted
|
||||
const deletedSequenceIds = Object.entries(latestBySequenceId)
|
||||
.filter(([, notification]) => notification.event === EVENT_MESSAGE_DELETE)
|
||||
.map(([sequenceId]) => sequenceId);
|
||||
if (deletedSequenceIds.length > 0) {
|
||||
console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds);
|
||||
await Promise.all(
|
||||
deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId))
|
||||
);
|
||||
}
|
||||
|
||||
// Add only the latest notification for each non-deleted sequence
|
||||
const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => n.event === EVENT_MESSAGE);
|
||||
if (notificationsToAdd.length > 0) {
|
||||
console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`);
|
||||
await subscriptionManager.addNotifications(subscription.id, notificationsToAdd);
|
||||
} else {
|
||||
console.log(`[Poller] No new notifications found for ${subscription.id}`);
|
||||
}
|
||||
console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`);
|
||||
await subscriptionManager.addNotifications(subscription.id, notifications);
|
||||
}
|
||||
|
||||
pollInBackground(subscription) {
|
||||
@@ -59,6 +84,21 @@ class Poller {
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups notifications by sequenceId and returns only the latest (highest time) for each sequence.
|
||||
* Returns an object mapping sequenceId -> latest notification.
|
||||
*/
|
||||
latestNotificationsBySequenceId(notifications) {
|
||||
const latestBySequenceId = {};
|
||||
notifications.forEach((notification) => {
|
||||
const sequenceId = notification.sequence_id || notification.id;
|
||||
if (!(sequenceId in latestBySequenceId) || notification.time >= latestBySequenceId[sequenceId].time) {
|
||||
latestBySequenceId[sequenceId] = notification;
|
||||
}
|
||||
});
|
||||
return latestBySequenceId;
|
||||
}
|
||||
}
|
||||
|
||||
const poller = new Poller();
|
||||
|
||||
@@ -3,6 +3,8 @@ import notifier from "./Notifier";
|
||||
import prefs from "./Prefs";
|
||||
import db from "./db";
|
||||
import { topicUrl } from "./utils";
|
||||
import { messageWithSequenceId } from "./notificationUtils";
|
||||
import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE } from "./events";
|
||||
|
||||
class SubscriptionManager {
|
||||
constructor(dbImpl) {
|
||||
@@ -48,16 +50,17 @@ class SubscriptionManager {
|
||||
}
|
||||
|
||||
async notify(subscriptionId, notification) {
|
||||
if (notification.event !== EVENT_MESSAGE) {
|
||||
return;
|
||||
}
|
||||
const subscription = await this.get(subscriptionId);
|
||||
if (subscription.mutedUntil > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const priority = notification.priority ?? 3;
|
||||
if (priority < (await prefs.minPriority())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await notifier.notify(subscription, notification);
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ class SubscriptionManager {
|
||||
// killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach
|
||||
|
||||
return this.db.notifications
|
||||
.orderBy("time") // Sort by time first
|
||||
.orderBy("time") // Sort by time
|
||||
.filter((n) => n.subscriptionId === subscriptionId)
|
||||
.reverse()
|
||||
.toArray();
|
||||
@@ -173,17 +176,22 @@ class SubscriptionManager {
|
||||
/** Adds notification, or returns false if it already exists */
|
||||
async addNotification(subscriptionId, notification) {
|
||||
const exists = await this.db.notifications.get(notification.id);
|
||||
if (exists) {
|
||||
if (exists || notification.event === EVENT_MESSAGE_DELETE || notification.event === EVENT_MESSAGE_CLEAR) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// sw.js duplicates this logic, so if you change it here, change it there too
|
||||
// Note: Service worker (sw.js) and addNotifications() duplicates this logic,
|
||||
// so if you change it here, change it there too.
|
||||
|
||||
// Add notification to database
|
||||
await this.db.notifications.add({
|
||||
...notification,
|
||||
...messageWithSequenceId(notification),
|
||||
subscriptionId,
|
||||
// New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
new: 1,
|
||||
}); // FIXME consider put() for double tab
|
||||
new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
});
|
||||
|
||||
// FIXME consider put() for double tab
|
||||
// Update subscription last message id (for ?since=... queries)
|
||||
await this.db.subscriptions.update(subscriptionId, {
|
||||
last: notification.id,
|
||||
});
|
||||
@@ -195,7 +203,10 @@ class SubscriptionManager {
|
||||
|
||||
/** Adds/replaces notifications, will not throw if they exist */
|
||||
async addNotifications(subscriptionId, notifications) {
|
||||
const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId }));
|
||||
const notificationsWithSubscriptionId = notifications.map((notification) => ({
|
||||
...messageWithSequenceId(notification),
|
||||
subscriptionId,
|
||||
}));
|
||||
const lastNotificationId = notifications.at(-1).id;
|
||||
await this.db.notifications.bulkPut(notificationsWithSubscriptionId);
|
||||
await this.db.subscriptions.update(subscriptionId, {
|
||||
@@ -220,6 +231,10 @@ class SubscriptionManager {
|
||||
await this.db.notifications.delete(notificationId);
|
||||
}
|
||||
|
||||
async deleteNotificationBySequenceId(subscriptionId, sequenceId) {
|
||||
await this.db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||
}
|
||||
|
||||
async deleteNotifications(subscriptionId) {
|
||||
await this.db.notifications.where({ subscriptionId }).delete();
|
||||
}
|
||||
@@ -228,6 +243,10 @@ class SubscriptionManager {
|
||||
await this.db.notifications.where({ id: notificationId }).modify({ new: 0 });
|
||||
}
|
||||
|
||||
async markNotificationReadBySequenceId(subscriptionId, sequenceId) {
|
||||
await this.db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
||||
}
|
||||
|
||||
async markNotificationsRead(subscriptionId) {
|
||||
await this.db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 });
|
||||
}
|
||||
|
||||
@@ -11,13 +11,20 @@ const createDatabase = (username) => {
|
||||
const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user
|
||||
const db = new Dexie(dbName);
|
||||
|
||||
db.version(2).stores({
|
||||
db.version(3).stores({
|
||||
subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]",
|
||||
notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance
|
||||
notifications: "&id,sequenceId,subscriptionId,time,new,[subscriptionId+new],[subscriptionId+sequenceId]",
|
||||
users: "&baseUrl,username",
|
||||
prefs: "&key",
|
||||
});
|
||||
|
||||
// When another connection (e.g., service worker or another tab) wants to upgrade,
|
||||
// close this connection gracefully to allow the upgrade to proceed
|
||||
db.on("versionchange", () => {
|
||||
console.log("[db] versionchange event: closing database");
|
||||
db.close();
|
||||
});
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
|
||||
15
web/src/app/events.js
Normal file
15
web/src/app/events.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Event types for ntfy messages
|
||||
// These correspond to the server event types in server/types.go
|
||||
|
||||
export const EVENT_OPEN = "open";
|
||||
export const EVENT_KEEPALIVE = "keepalive";
|
||||
export const EVENT_MESSAGE = "message";
|
||||
export const EVENT_MESSAGE_DELETE = "message_delete";
|
||||
export const EVENT_MESSAGE_CLEAR = "message_clear";
|
||||
export const EVENT_POLL_REQUEST = "poll_request";
|
||||
|
||||
export const WEBPUSH_EVENT_MESSAGE = "message";
|
||||
export const WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING = "subscription_expiring";
|
||||
|
||||
// Check if an event is a notification event (message, delete, or read)
|
||||
export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR;
|
||||
@@ -25,13 +25,13 @@ const formatTitleWithDefault = (m, fallback) => {
|
||||
|
||||
export const formatMessage = (m) => {
|
||||
if (m.title) {
|
||||
return m.message;
|
||||
return m.message || "";
|
||||
}
|
||||
const emojiList = toEmojis(m.tags);
|
||||
if (emojiList.length > 0) {
|
||||
return `${emojiList.join(" ")} ${m.message}`;
|
||||
return `${emojiList.join(" ")} ${m.message || ""}`;
|
||||
}
|
||||
return m.message;
|
||||
return m.message || "";
|
||||
};
|
||||
|
||||
const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
|
||||
@@ -50,7 +50,7 @@ export const isImage = (attachment) => {
|
||||
export const icon = "/static/images/ntfy.png";
|
||||
export const badge = "/static/images/mask-icon.svg";
|
||||
|
||||
export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => {
|
||||
export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
||||
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
||||
@@ -61,8 +61,8 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to
|
||||
badge,
|
||||
icon,
|
||||
image,
|
||||
timestamp: message.time * 1_000,
|
||||
tag: subscriptionId,
|
||||
timestamp: message.time * 1000,
|
||||
tag: message.sequence_id || message.id, // Update notification if there is a sequence ID
|
||||
renotify: true,
|
||||
silent: false,
|
||||
// This is used by the notification onclick event
|
||||
@@ -79,3 +79,10 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const messageWithSequenceId = (message) => {
|
||||
if (message.sequenceId) {
|
||||
return message;
|
||||
}
|
||||
return { ...message, sequenceId: message.sequence_id || message.id };
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import accountApi from "../app/AccountApi";
|
||||
import { UnauthorizedError } from "../app/errors";
|
||||
import notifier from "../app/Notifier";
|
||||
import prefs from "../app/Prefs";
|
||||
import { EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../app/events";
|
||||
|
||||
/**
|
||||
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
|
||||
@@ -50,9 +51,28 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
||||
};
|
||||
|
||||
const handleNotification = async (subscriptionId, notification) => {
|
||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
||||
if (added) {
|
||||
await subscriptionManager.notify(subscriptionId, notification);
|
||||
// This logic is (partially) duplicated in
|
||||
// - Android: SubscriberService::onNotificationReceived()
|
||||
// - Android: FirebaseService::onMessageReceived()
|
||||
// - Web app: hooks.js:handleNotification()
|
||||
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
||||
|
||||
if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) {
|
||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id);
|
||||
await notifier.cancel(notification);
|
||||
} else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) {
|
||||
await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id);
|
||||
await notifier.cancel(notification);
|
||||
} else {
|
||||
// Regular message: delete existing and add new
|
||||
const sequenceId = notification.sequence_id || notification.id;
|
||||
if (sequenceId) {
|
||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId);
|
||||
}
|
||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
||||
if (added) {
|
||||
await subscriptionManager.notify(subscriptionId, notification);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,7 +251,9 @@ export const useIsLaunchedPWA = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isIOSStandalone) {
|
||||
return () => {}; // No need to listen for events on iOS
|
||||
return () => {
|
||||
// No need to listen for events on iOS
|
||||
};
|
||||
}
|
||||
const handler = (evt) => {
|
||||
console.log(`[useIsLaunchedPWA] App is now running ${evt.matches ? "standalone" : "in the browser"}`);
|
||||
|
||||
@@ -5,10 +5,19 @@ import { registerSW as viteRegisterSW } from "virtual:pwa-register";
|
||||
const intervalMS = 60 * 60 * 1000;
|
||||
|
||||
// https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html
|
||||
const registerSW = () =>
|
||||
const registerSW = () => {
|
||||
console.log("[ServiceWorker] Registering service worker");
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
console.warn("[ServiceWorker] Service workers not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
viteRegisterSW({
|
||||
onRegisteredSW(swUrl, registration) {
|
||||
console.log("[ServiceWorker] Registered:", { swUrl, registration });
|
||||
|
||||
if (!registration) {
|
||||
console.warn("[ServiceWorker] No registration returned");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,9 +32,16 @@ const registerSW = () =>
|
||||
},
|
||||
});
|
||||
|
||||
if (resp?.status === 200) await registration.update();
|
||||
if (resp?.status === 200) {
|
||||
console.log("[ServiceWorker] Updating service worker");
|
||||
await registration.update();
|
||||
}
|
||||
}, intervalMS);
|
||||
},
|
||||
onRegisterError(error) {
|
||||
console.error("[ServiceWorker] Registration error:", error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default registerSW;
|
||||
|
||||
Reference in New Issue
Block a user