Compare commits
6 Commits
v2.17.0
...
scalar-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3619c80544 | ||
|
|
27bce6f4d1 | ||
|
|
a06550a90f | ||
|
|
d2e0588037 | ||
|
|
854aa1f783 | ||
|
|
0ae2ca43bd |
@@ -40,7 +40,6 @@ ADD ./log ./log
|
|||||||
ADD ./server ./server
|
ADD ./server ./server
|
||||||
ADD ./user ./user
|
ADD ./user ./user
|
||||||
ADD ./util ./util
|
ADD ./util ./util
|
||||||
ADD ./payments ./payments
|
|
||||||
RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
|
RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|||||||
@@ -34,12 +34,6 @@ You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There
|
|||||||
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
|
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
|
||||||
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img height="50" src="docs/static/img/badge-googleplay.png"></a>
|
|
||||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="docs/static/img/badge-fdroid.svg"></a>
|
|
||||||
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img height="50" src="docs/static/img/badge-appstore.png"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<img src=".github/images/screenshot-curl.png" height="180">
|
<img src=".github/images/screenshot-curl.png" height="180">
|
||||||
<img src=".github/images/screenshot-web-detail.png" height="180">
|
<img src=".github/images/screenshot-web-detail.png" height="180">
|
||||||
|
|||||||
192
docs/api/index.html
Normal file
192
docs/api/index.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ntfy API Reference</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/img/favicon.png">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header matching docs.ntfy.sh */
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(to right, #317f6f, #14b8a6);
|
||||||
|
padding: 16px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text p {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scalar container */
|
||||||
|
#scalar-api-reference {
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Share and Generate SDKs menu items */
|
||||||
|
[data-scalar-menu-item="share"],
|
||||||
|
[data-scalar-menu-item="sdk"],
|
||||||
|
.scalar-card-header-actions button[title="Share"],
|
||||||
|
.scalar-card-header-actions button[title="Generate SDK"],
|
||||||
|
button:has(> span:contains("Share")),
|
||||||
|
button:has(> span:contains("SDK")) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="/" style="display: flex; align-items: center; text-decoration: none;">
|
||||||
|
<img src="/static/img/ntfy.png" width="35" height="35" alt="ntfy logo" style="margin-right: 10px;">
|
||||||
|
</a>
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>ntfy</h1>
|
||||||
|
<p>API Reference</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-links">
|
||||||
|
<a href="/">Documentation Home</a>
|
||||||
|
<a href="https://ntfy.sh">ntfy.sh</a>
|
||||||
|
<a href="https://github.com/binwiederhier/ntfy">GitHub</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Scalar API Reference -->
|
||||||
|
<div id="scalar-api-reference"></div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="./scalar-style.css" />
|
||||||
|
<script src="./scalar-standalone.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initScalar() {
|
||||||
|
const targetElement = document.getElementById('scalar-api-reference');
|
||||||
|
|
||||||
|
if (!targetElement) {
|
||||||
|
setTimeout(initScalar, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Scalar === 'undefined' || typeof Scalar.createApiReference !== 'function') {
|
||||||
|
setTimeout(initScalar, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: './openapi.yaml',
|
||||||
|
layout: 'modern',
|
||||||
|
theme: 'default',
|
||||||
|
customCss: `
|
||||||
|
/* Hide Share and Generate SDKs menu items in dropdowns */
|
||||||
|
[data-testid="share-button"],
|
||||||
|
[data-testid="sdk-button"],
|
||||||
|
.context-menu-item:has(svg[data-icon="share"]),
|
||||||
|
.context-menu-item:has(svg[data-icon="sdk"]),
|
||||||
|
.dropdown-item:has(span:contains("Share")),
|
||||||
|
.dropdown-item:has(span:contains("SDK")),
|
||||||
|
.scalar-dropdown-item[data-action="share"],
|
||||||
|
.scalar-dropdown-item[data-action="generate-sdk"],
|
||||||
|
button[aria-label="Share"],
|
||||||
|
button[aria-label="Generate SDK"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
configuration: {
|
||||||
|
hideSidebar: false,
|
||||||
|
hideSearch: false,
|
||||||
|
hideModels: false,
|
||||||
|
hideDownloadButton: false,
|
||||||
|
hideTabs: false,
|
||||||
|
hideServerUrl: false,
|
||||||
|
hideInfo: false,
|
||||||
|
darkMode: false,
|
||||||
|
withDefaultFonts: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Scalar.createApiReference('#scalar-api-reference', config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing Scalar:', error);
|
||||||
|
targetElement.innerHTML = '<div style="padding: 20px; color: red;">Error loading API reference: ' + error.message + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initScalar);
|
||||||
|
} else {
|
||||||
|
setTimeout(initScalar, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(initScalar, 200);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2370
docs/api/openapi.yaml
Normal file
2370
docs/api/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
35987
docs/api/scalar-standalone.js
Normal file
35987
docs/api/scalar-standalone.js
Normal file
File diff suppressed because one or more lines are too long
11911
docs/api/scalar-style.css
Normal file
11911
docs/api/scalar-style.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ or POST requests. I use it to notify myself when scripts fail, or long-running c
|
|||||||
|
|
||||||
## Step 1: Get the app
|
## Step 1: Get the app
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a>
|
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a>
|
||||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.svg"></a>
|
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.png"></a>
|
||||||
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="static/img/badge-appstore.png"></a>
|
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="static/img/badge-appstore.png"></a>
|
||||||
|
|
||||||
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid.
|
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid.
|
||||||
|
|||||||
@@ -30,37 +30,37 @@ deb/rpm packages.
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_amd64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.tar.gz
|
||||||
tar zxvf ntfy_2.17.0_linux_amd64.tar.gz
|
tar zxvf ntfy_2.16.0_linux_amd64.tar.gz
|
||||||
sudo cp -a ntfy_2.17.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.17.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_2.17.0_linux_armv6.tar.gz
|
tar zxvf ntfy_2.16.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_2.17.0_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.17.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_2.17.0_linux_armv7.tar.gz
|
tar zxvf ntfy_2.16.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_2.17.0_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.17.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_2.17.0_linux_arm64.tar.gz
|
tar zxvf ntfy_2.16.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_2.17.0_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.17.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ deb/rpm packages.
|
|||||||
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely
|
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely
|
||||||
go away soon. I suspect I will phase it out some time in early 2026.
|
go away soon. I suspect I will phase it out some time in early 2026.
|
||||||
|
|
||||||
Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 B6B7 CFDB 962D 4F1E C4AF`):
|
Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`):
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
@@ -116,7 +116,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -124,7 +124,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -132,7 +132,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -140,7 +140,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -150,28 +150,28 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
@@ -213,18 +213,18 @@ pkg install go-ntfy
|
|||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
||||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_darwin_all.tar.gz),
|
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_darwin_all.tar.gz),
|
||||||
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||||
|
|
||||||
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
||||||
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_darwin_all.tar.gz > ntfy_2.17.0_darwin_all.tar.gz
|
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_darwin_all.tar.gz > ntfy_2.16.0_darwin_all.tar.gz
|
||||||
tar zxvf ntfy_2.17.0_darwin_all.tar.gz
|
tar zxvf ntfy_2.16.0_darwin_all.tar.gz
|
||||||
sudo cp -a ntfy_2.17.0_darwin_all/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.16.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||||
mkdir ~/Library/Application\ Support/ntfy
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
cp ntfy_2.17.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
cp ntfy_2.16.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
ntfy --help
|
ntfy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ brew install ntfy
|
|||||||
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
||||||
To install, you can either
|
To install, you can either
|
||||||
|
|
||||||
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.17.0/ntfy_2.17.0_windows_amd64.zip),
|
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_windows_amd64.zip),
|
||||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
||||||
|
|
||||||
@@ -567,18 +567,18 @@ kubectl apply -k /ntfy
|
|||||||
cpu: 150m
|
cpu: 150m
|
||||||
memory: 150Mi
|
memory: 150Mi
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /etc/ntfy/server.yml
|
- mountPath: /etc/ntfy
|
||||||
subPath: server.yml
|
subPath: server.yml
|
||||||
name: config-volume # generated via configMapGenerator from kustomization file
|
name: config-volume # generated vie configMapGenerator from kustomization file
|
||||||
- mountPath: /var/cache/ntfy
|
- mountPath: /var/cache/ntfy
|
||||||
name: cache-volume # cache volume mounted to persistent volume
|
name: cache-volume #cache volume mounted to persistent volume
|
||||||
volumes:
|
volumes:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
configMap: # uses configmap generator to parse server.yml to configmap
|
configMap: # uses configmap generator to parse server.yml to configmap
|
||||||
name: server-config
|
name: server-config
|
||||||
- name: cache-volume
|
- name: cache-volume
|
||||||
persistentVolumeClaim: # stores /cache/ntfy in defined pv
|
persistentVolumeClaim: # stores /cache/ntfy in defined pv
|
||||||
claimName: ntfy-pvc
|
claimName: ntfy-pvc
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "ntfy-pvc.yaml"
|
=== "ntfy-pvc.yaml"
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||||||
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy.
|
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy.
|
||||||
- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go)
|
- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go)
|
||||||
- [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go)
|
- [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go)
|
||||||
- [Uptime Monitor](https://uptime-monitor.org) - Self-hosted, enterprise-grade uptime monitoring and alerting system (TS)
|
|
||||||
- [send_to_ntfy_extension](https://github.com/TheDuffman85/send_to_ntfy_extension/) ⭐ - A browser extension to send the notifications to ntfy (JS)
|
|
||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
|
|||||||
307
docs/publish.md
307
docs/publish.md
@@ -1134,7 +1134,6 @@ As of today, the following actions are supported:
|
|||||||
* [`broadcast`](#send-android-broadcast): Sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
* [`broadcast`](#send-android-broadcast): Sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
||||||
when the action button is tapped (only supported on Android)
|
when the action button is tapped (only supported on Android)
|
||||||
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
|
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
|
||||||
* [`copy`](#copy-to-clipboard): Copies a given value to the clipboard when the action button is tapped
|
|
||||||
|
|
||||||
Here's an example of what a notification with actions can look like:
|
Here's an example of what a notification with actions can look like:
|
||||||
|
|
||||||
@@ -1165,12 +1164,9 @@ To define actions using the `X-Actions` header (or any of its aliases: `Actions`
|
|||||||
Multiple actions are separated by a semicolon (`;`), and key/value pairs are separated by commas (`,`). Values may be
|
Multiple actions are separated by a semicolon (`;`), and key/value pairs are separated by commas (`,`). Values may be
|
||||||
quoted with double quotes (`"`) or single quotes (`'`) if the value itself contains commas or semicolons.
|
quoted with double quotes (`"`) or single quotes (`'`) if the value itself contains commas or semicolons.
|
||||||
|
|
||||||
Each action type has a short format where some key prefixes can be omitted:
|
The `action=` and `label=` prefix are optional in all actions, and the `url=` prefix is optional in the `view` and
|
||||||
|
`http` action. The only limitation of this format is that depending on your language/library, UTF-8 characters may not
|
||||||
* [`view`](#open-websiteapp): `view, <label>, <url>[, clear=true]`
|
work. If they don't, use the [JSON array format](#using-a-json-array) instead.
|
||||||
* [`broadcast`](#send-android-broadcast):`broadcast, <label>[, extras.<param>=<value>][, intent=<intent>][, clear=true]`
|
|
||||||
* [`http`](#send-http-request): `http, <label>, <url>[, method=<method>][, headers.<header>=<value>][, body=<body>][, clear=true]`
|
|
||||||
* [`copy`](#copy-to-clipboard): `copy, <label>, <value>[, clear=true]`
|
|
||||||
|
|
||||||
As an example, here's how you can create the above notification using this format. Refer to the [`view` action](#open-websiteapp) and
|
As an example, here's how you can create the above notification using this format. Refer to the [`view` action](#open-websiteapp) and
|
||||||
[`http` action](#send-http-request) section for details on the specific actions:
|
[`http` action](#send-http-request) section for details on the specific actions:
|
||||||
@@ -1470,8 +1466,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
|||||||
```
|
```
|
||||||
|
|
||||||
The required/optional fields for each action depend on the type of the action itself. Please refer to
|
The required/optional fields for each action depend on the type of the action itself. Please refer to
|
||||||
[`view` action](#open-websiteapp), [`broadcast` action](#send-android-broadcast), [`http` action](#send-http-request),
|
[`view` action](#open-websiteapp), [`broadcast` action](#send-android-broadcast), and [`http` action](#send-http-request)
|
||||||
and [`copy` action](#copy-to-clipboard) for details.
|
for details.
|
||||||
|
|
||||||
### Open website/app
|
### Open website/app
|
||||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
@@ -1714,9 +1710,6 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
|
|
||||||
The short format for the `view` action is `view, <label>, <url>` (e.g. `view, Open Google, https://google.com`),
|
|
||||||
but you can always just use the `<key>=<value>` notation as well (e.g. `action=view, url=https://google.com, label=Open Google`).
|
|
||||||
|
|
||||||
The `view` action supports the following fields:
|
The `view` action supports the following fields:
|
||||||
|
|
||||||
| Field | Required | Type | Default | Example | Description |
|
| Field | Required | Type | Default | Example | Description |
|
||||||
@@ -1993,9 +1986,6 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
|
|
||||||
The short format for the `broadcast` action is `broadcast, <label>, <url>` (e.g. `broadcast, Take picture, extras.cmd=pic`),
|
|
||||||
but you can always just use the `<key>=<value>` notation as well (e.g. `action=broadcast, label=Take picture, extras.cmd=pic`).
|
|
||||||
|
|
||||||
The `broadcast` action supports the following fields:
|
The `broadcast` action supports the following fields:
|
||||||
|
|
||||||
| Field | Required | Type | Default | Example | Description |
|
| Field | Required | Type | Default | Example | Description |
|
||||||
@@ -2283,9 +2273,6 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
|
|
||||||
The short format for the `http` action is `http, <label>, <url>` (e.g. `http, Close door, https://api.mygarage.lan/close`),
|
|
||||||
but you can always just use the `<key>=<value>` notation as well (e.g. `action=http, label=Close door, url=https://api.mygarage.lan/close`).
|
|
||||||
|
|
||||||
The `http` action supports the following fields:
|
The `http` action supports the following fields:
|
||||||
|
|
||||||
| Field | Required | Type | Default | Example | Description |
|
| Field | Required | Type | Default | Example | Description |
|
||||||
@@ -2298,254 +2285,6 @@ The `http` action supports the following fields:
|
|||||||
| `body` | -️ | *string* | *empty* | `some body, somebody?` | HTTP body |
|
| `body` | -️ | *string* | *empty* | `some body, somebody?` | HTTP body |
|
||||||
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared. |
|
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared. |
|
||||||
|
|
||||||
### Copy to clipboard
|
|
||||||
_Supported on:_ :material-android: :material-firefox:
|
|
||||||
|
|
||||||
The `copy` action **copies a given value to the clipboard when the action button is tapped**. This is useful for
|
|
||||||
one-time passcodes, tokens, or any other value you want to quickly copy without opening the full notification.
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
The copy action button is only shown in the web app and Android app notification list, **not** in browser desktop
|
|
||||||
notifications. This is because browsers do not allow clipboard access from notification actions without direct
|
|
||||||
user interaction with the page.
|
|
||||||
|
|
||||||
Here's an example using the [`X-Actions` header](#using-a-header):
|
|
||||||
|
|
||||||
=== "Command line (curl)"
|
|
||||||
```
|
|
||||||
curl \
|
|
||||||
-d "Your one-time passcode is 123456" \
|
|
||||||
-H "Actions: copy, Copy code, 123456" \
|
|
||||||
ntfy.sh/myhome
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "ntfy CLI"
|
|
||||||
```
|
|
||||||
ntfy publish \
|
|
||||||
--actions="copy, Copy code, 123456" \
|
|
||||||
myhome \
|
|
||||||
"Your one-time passcode is 123456"
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "HTTP"
|
|
||||||
``` http
|
|
||||||
POST /myhome HTTP/1.1
|
|
||||||
Host: ntfy.sh
|
|
||||||
Actions: copy, Copy code, 123456
|
|
||||||
|
|
||||||
Your one-time passcode is 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "JavaScript"
|
|
||||||
``` javascript
|
|
||||||
fetch('https://ntfy.sh/myhome', {
|
|
||||||
method: 'POST',
|
|
||||||
body: 'Your one-time passcode is 123456',
|
|
||||||
headers: {
|
|
||||||
'Actions': 'copy, Copy code, 123456'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Go"
|
|
||||||
``` go
|
|
||||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Your one-time passcode is 123456"))
|
|
||||||
req.Header.Set("Actions", "copy, Copy code, 123456")
|
|
||||||
http.DefaultClient.Do(req)
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "PowerShell"
|
|
||||||
``` powershell
|
|
||||||
$Request = @{
|
|
||||||
Method = "POST"
|
|
||||||
URI = "https://ntfy.sh/myhome"
|
|
||||||
Headers = @{
|
|
||||||
Actions = "copy, Copy code, 123456"
|
|
||||||
}
|
|
||||||
Body = "Your one-time passcode is 123456"
|
|
||||||
}
|
|
||||||
Invoke-RestMethod @Request
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Python"
|
|
||||||
``` python
|
|
||||||
requests.post("https://ntfy.sh/myhome",
|
|
||||||
data="Your one-time passcode is 123456",
|
|
||||||
headers={ "Actions": "copy, Copy code, 123456" })
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "PHP"
|
|
||||||
``` php-inline
|
|
||||||
file_get_contents('https://ntfy.sh/myhome', false, stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'header' =>
|
|
||||||
"Content-Type: text/plain\r\n" .
|
|
||||||
"Actions: copy, Copy code, 123456",
|
|
||||||
'content' => 'Your one-time passcode is 123456'
|
|
||||||
]
|
|
||||||
]));
|
|
||||||
```
|
|
||||||
|
|
||||||
And the same example using [JSON publishing](#publish-as-json):
|
|
||||||
|
|
||||||
=== "Command line (curl)"
|
|
||||||
```
|
|
||||||
curl ntfy.sh \
|
|
||||||
-d '{
|
|
||||||
"topic": "myhome",
|
|
||||||
"message": "Your one-time passcode is 123456",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "ntfy CLI"
|
|
||||||
```
|
|
||||||
ntfy publish \
|
|
||||||
--actions '[
|
|
||||||
{
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
]' \
|
|
||||||
myhome \
|
|
||||||
"Your one-time passcode is 123456"
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "HTTP"
|
|
||||||
``` http
|
|
||||||
POST / HTTP/1.1
|
|
||||||
Host: ntfy.sh
|
|
||||||
|
|
||||||
{
|
|
||||||
"topic": "myhome",
|
|
||||||
"message": "Your one-time passcode is 123456",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "JavaScript"
|
|
||||||
``` javascript
|
|
||||||
fetch('https://ntfy.sh', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
topic: "myhome",
|
|
||||||
message: "Your one-time passcode is 123456",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: "copy",
|
|
||||||
label: "Copy code",
|
|
||||||
value: "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Go"
|
|
||||||
``` go
|
|
||||||
// You should probably use json.Marshal() instead and make a proper struct,
|
|
||||||
// but for the sake of the example, this is easier.
|
|
||||||
|
|
||||||
body := `{
|
|
||||||
"topic": "myhome",
|
|
||||||
"message": "Your one-time passcode is 123456",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`
|
|
||||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
|
|
||||||
http.DefaultClient.Do(req)
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "PowerShell"
|
|
||||||
``` powershell
|
|
||||||
$Request = @{
|
|
||||||
Method = "POST"
|
|
||||||
URI = "https://ntfy.sh"
|
|
||||||
Body = ConvertTo-JSON @{
|
|
||||||
Topic = "myhome"
|
|
||||||
Message = "Your one-time passcode is 123456"
|
|
||||||
Actions = @(
|
|
||||||
@{
|
|
||||||
Action = "copy"
|
|
||||||
Label = "Copy code"
|
|
||||||
Value = "123456"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ContentType = "application/json"
|
|
||||||
}
|
|
||||||
Invoke-RestMethod @Request
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Python"
|
|
||||||
``` python
|
|
||||||
requests.post("https://ntfy.sh/",
|
|
||||||
data=json.dumps({
|
|
||||||
"topic": "myhome",
|
|
||||||
"message": "Your one-time passcode is 123456",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "PHP"
|
|
||||||
``` php-inline
|
|
||||||
file_get_contents('https://ntfy.sh/', false, stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'header' => "Content-Type: application/json",
|
|
||||||
'content' => json_encode([
|
|
||||||
"topic": "myhome",
|
|
||||||
"message": "Your one-time passcode is 123456",
|
|
||||||
"actions": [
|
|
||||||
[
|
|
||||||
"action": "copy",
|
|
||||||
"label": "Copy code",
|
|
||||||
"value": "123456"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
])
|
|
||||||
]
|
|
||||||
]));
|
|
||||||
```
|
|
||||||
|
|
||||||
The short format for the `copy` action is `copy, <label>, <value>` (e.g. `copy, Copy code, 123456`),
|
|
||||||
but you can always just use the `<key>=<value>` notation as well (e.g. `action=copy, label=Copy code, value=123456`).
|
|
||||||
|
|
||||||
The `copy` action supports the following fields:
|
|
||||||
|
|
||||||
| Field | Required | Type | Default | Example | Description |
|
|
||||||
|----------|----------|-----------|---------|-----------------|--------------------------------------------------|
|
|
||||||
| `action` | ✔️ | *string* | - | `copy` | Action type (**must be `copy`**) |
|
|
||||||
| `label` | ✔️ | *string* | - | `Copy code` | Label of the action button in the notification |
|
|
||||||
| `value` | ✔️ | *string* | - | `123456` | Value to copy to the clipboard |
|
|
||||||
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after action button is tapped |
|
|
||||||
|
|
||||||
## Scheduled delivery
|
## Scheduled delivery
|
||||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
@@ -2904,7 +2643,7 @@ You can enable templating by setting the `X-Template` header (or its aliases `Te
|
|||||||
will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`).
|
will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`).
|
||||||
See [custom templates](#custom-templates) for more details.
|
See [custom templates](#custom-templates) for more details.
|
||||||
* **Inline templating**: Setting the `X-Template` header or query parameter to `yes` or `1` (e.g. `?template=yes`)
|
* **Inline templating**: Setting the `X-Template` header or query parameter to `yes` or `1` (e.g. `?template=yes`)
|
||||||
will enable inline templating, which means that the `message`, `title`, and/or `priority` will be parsed as a Go template.
|
will enable inline templating, which means that the `message` and/or `title` will be parsed as a Go template.
|
||||||
See [inline templating](#inline-templating) for more details.
|
See [inline templating](#inline-templating) for more details.
|
||||||
|
|
||||||
To learn the basics of Go's templating language, please see [template syntax](#template-syntax).
|
To learn the basics of Go's templating language, please see [template syntax](#template-syntax).
|
||||||
@@ -2947,7 +2686,7 @@ and set the `X-Template` header or query parameter to the name of the template f
|
|||||||
For example, if you have a template file `/etc/ntfy/templates/myapp.yml`, you can set the header `X-Template: myapp` or
|
For example, if you have a template file `/etc/ntfy/templates/myapp.yml`, you can set the header `X-Template: myapp` or
|
||||||
the query parameter `?template=myapp` to use it.
|
the query parameter `?template=myapp` to use it.
|
||||||
|
|
||||||
Template files must have the `.yml` (not: `.yaml`!) extension and must be formatted as YAML. They may contain `title`, `message`, and `priority` keys,
|
Template files must have the `.yml` (not: `.yaml`!) extension and must be formatted as YAML. They may contain `title` and `message` keys,
|
||||||
which are interpreted as Go templates.
|
which are interpreted as Go templates.
|
||||||
|
|
||||||
Here's an **example custom template**:
|
Here's an **example custom template**:
|
||||||
@@ -2965,11 +2704,6 @@ Here's an **example custom template**:
|
|||||||
Status: {{ .status }}
|
Status: {{ .status }}
|
||||||
Type: {{ .type | upper }} ({{ .percent }}%)
|
Type: {{ .type | upper }} ({{ .percent }}%)
|
||||||
Server: {{ .server }}
|
Server: {{ .server }}
|
||||||
priority: |
|
|
||||||
{{ if gt .percent 90.0 }}5
|
|
||||||
{{ else if gt .percent 75.0 }}4
|
|
||||||
{{ else }}3
|
|
||||||
{{ end }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you have the template file in place, you can send the payload to your topic using the `X-Template`
|
Once you have the template file in place, you can send the payload to your topic using the `X-Template`
|
||||||
@@ -3051,7 +2785,7 @@ Which will result in a notification that looks like this:
|
|||||||
|
|
||||||
### Inline templating
|
### Inline templating
|
||||||
|
|
||||||
When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message`, `title`, and `priority` fields of your
|
When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your
|
||||||
webhook payload.
|
webhook payload.
|
||||||
|
|
||||||
Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh).
|
Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh).
|
||||||
@@ -3107,12 +2841,12 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
curl \
|
curl \
|
||||||
--globoff \
|
--globoff \
|
||||||
-d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \
|
-d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \
|
||||||
'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}'
|
'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "HTTP"
|
=== "HTTP"
|
||||||
``` http
|
``` http
|
||||||
POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}} HTTP/1.1
|
POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1
|
||||||
Host: ntfy.sh
|
Host: ntfy.sh
|
||||||
|
|
||||||
{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
|
{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
|
||||||
@@ -3120,7 +2854,7 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
|
|
||||||
=== "JavaScript"
|
=== "JavaScript"
|
||||||
``` javascript
|
``` javascript
|
||||||
fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', {
|
fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
||||||
})
|
})
|
||||||
@@ -3129,7 +2863,7 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
=== "Go"
|
=== "Go"
|
||||||
``` go
|
``` go
|
||||||
body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}`
|
body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}`
|
||||||
uri := `https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if eq .error.level "severe"}}5{{else}}3{{end}}`
|
uri := "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
|
||||||
req, _ := http.NewRequest("POST", uri, strings.NewReader(body))
|
req, _ := http.NewRequest("POST", uri, strings.NewReader(body))
|
||||||
http.DefaultClient.Do(req)
|
http.DefaultClient.Do(req)
|
||||||
```
|
```
|
||||||
@@ -3139,7 +2873,7 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
``` powershell
|
``` powershell
|
||||||
$Request = @{
|
$Request = @{
|
||||||
Method = "POST"
|
Method = "POST"
|
||||||
URI = 'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}'
|
URI = "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
|
||||||
Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
||||||
ContentType = "application/json"
|
ContentType = "application/json"
|
||||||
}
|
}
|
||||||
@@ -3149,14 +2883,14 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
=== "Python"
|
=== "Python"
|
||||||
``` python
|
``` python
|
||||||
requests.post(
|
requests.post(
|
||||||
'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}',
|
"https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}",
|
||||||
data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "PHP"
|
=== "PHP"
|
||||||
``` php-inline
|
``` php-inline
|
||||||
file_get_contents('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', false, stream_context_create([
|
file_get_contents("https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", false, stream_context_create([
|
||||||
'http' => [
|
'http' => [
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' => "Content-Type: application/json",
|
'header' => "Content-Type: application/json",
|
||||||
@@ -3165,9 +2899,9 @@ Here's an **easier example with a shorter JSON payload**:
|
|||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
|
|
||||||
This example uses the `message`/`m`, `title`/`t`, and `priority`/`p` query parameters, but obviously this also works with the
|
This example uses the `message`/`m` and `title`/`t` query parameters, but obviously this also works with the corresponding
|
||||||
corresponding headers. It will send a notification with a title `phil-pc: A severe error has occurred`, a message
|
`Message`/`Title` headers. It will send a notification with a title `phil-pc: A severe error has occurred` and a message
|
||||||
`Error message: Disk has run out of space`, and priority `5` (max) if the level is "severe", or `3` (default) otherwise.
|
`Error message: Disk has run out of space`.
|
||||||
|
|
||||||
### Template syntax
|
### Template syntax
|
||||||
ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful,
|
ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful,
|
||||||
@@ -3186,7 +2920,7 @@ your templates there first ([example for Grafana alert](https://repeatit.io/#/sh
|
|||||||
ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig),
|
ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig),
|
||||||
thank you to the Sprig developers 🙏). This is useful for advanced message templating and for transforming the data provided through the JSON payload.
|
thank you to the Sprig developers 🙏). This is useful for advanced message templating and for transforming the data provided through the JSON payload.
|
||||||
|
|
||||||
Below are the functions that are available to use inside your message, title, and priority templates.
|
Below are the functions that are available to use inside your message/title templates.
|
||||||
|
|
||||||
* [String Functions](publish/template-functions.md#string-functions): `trim`, `trunc`, `substr`, `plural`, etc.
|
* [String Functions](publish/template-functions.md#string-functions): `trim`, `trunc`, `substr`, `plural`, etc.
|
||||||
* [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc.
|
* [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc.
|
||||||
@@ -3769,6 +3503,9 @@ Here's an example with a custom message, tags and a priority:
|
|||||||
## Updating + deleting notifications
|
## Updating + deleting notifications
|
||||||
_Supported on:_ :material-android: :material-firefox:
|
_Supported on:_ :material-android: :material-firefox:
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
This feature is not fully released yet. The ntfy Android 1.22.x is being released right now. This may take a week or so.
|
||||||
|
|
||||||
You can **update, clear (mark as read and dismiss), or delete notifications** that have already been delivered. This is useful for scenarios
|
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.
|
like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant.
|
||||||
|
|
||||||
|
|||||||
@@ -6,49 +6,16 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
| Component | Version | Release date |
|
| Component | Version | Release date |
|
||||||
|------------------|---------|--------------|
|
|------------------|---------|--------------|
|
||||||
| ntfy server | v2.17.0 | Feb 8, 2026 |
|
| ntfy server | v2.16.0 | Jan 19, 2026 |
|
||||||
| ntfy Android app | v1.22.2 | Jan 25, 2026 |
|
| ntfy Android app | v1.22.2 | Jan 25, 2026 |
|
||||||
| ntfy iOS app | v1.3 | Nov 26, 2023 |
|
| ntfy iOS app | v1.3 | Nov 26, 2023 |
|
||||||
|
|
||||||
Please check out the release notes for [upcoming releases](#not-released-yet) below.
|
Please check out the release notes for [upcoming releases](#not-released-yet) below.
|
||||||
|
|
||||||
## ntfy server v2.17.0
|
### ntfy Android app v1.22.2
|
||||||
Released February 8, 2026
|
|
||||||
|
|
||||||
This release adds support for templating in the priority field, a new "copy" action button to copy values to the clipboard,
|
|
||||||
a red notification dot on the favicon for unread messages, and an admin-only version endpoint. It also includes several
|
|
||||||
crash fixes, web app improvements, and documentation updates.
|
|
||||||
|
|
||||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), [Liberapay](https://en.liberapay.com/ntfy/), Bitcoin (`1626wjrw3uWk9adyjCfYwafw4sQWujyjn8`),
|
|
||||||
or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy will always remain open source.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Server: Support templating in the priority field ([#1426](https://github.com/binwiederhier/ntfy/issues/1426), thanks to [@seantomburke](https://github.com/seantomburke) for reporting)
|
|
||||||
* Server: Add admin-only `GET /v1/version` endpoint returning server version, build commit, and date ([#1599](https://github.com/binwiederhier/ntfy/issues/1599), thanks to [@crivchri](https://github.com/crivchri) for reporting)
|
|
||||||
* Server/Web: [Support "copy" action](publish.md#copy-to-clipboard) button to copy a value to the clipboard ([#1364](https://github.com/binwiederhier/ntfy/issues/1364), thanks to [@SudoWatson](https://github.com/SudoWatson) for reporting)
|
|
||||||
* Web: Show red notification dot on favicon when there are unread messages ([#1017](https://github.com/binwiederhier/ntfy/issues/1017), thanks to [@ad-si](https://github.com/ad-si) for reporting)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Server: Fix crash when commit string is shorter than 7 characters in non-GitHub-Action builds ([#1493](https://github.com/binwiederhier/ntfy/issues/1493), thanks to [@cyrinux](https://github.com/cyrinux) for reporting)
|
|
||||||
* Server: Fix server crash (nil pointer panic) when subscriber disconnects during publish ([#1598](https://github.com/binwiederhier/ntfy/pull/1598))
|
|
||||||
* Server: Fix log spam from `http: response.WriteHeader on hijacked connection` for WebSocket errors ([#1362](https://github.com/binwiederhier/ntfy/issues/1362), thanks to [@bonfiresh](https://github.com/bonfiresh) for reporting)
|
|
||||||
* Server: Use `slices.Contains` from stdlib to simplify code ([#1406](https://github.com/binwiederhier/ntfy/pull/1406), thanks to [@tanhuaan](https://github.com/tanhuaan))
|
|
||||||
* Web: Fix `clear=true` on action buttons not clearing the notification ([#1029](https://github.com/binwiederhier/ntfy/issues/1029), thanks to [@ElFishi](https://github.com/ElFishi) for reporting)
|
|
||||||
* Web: Fix Markdown message line height to match plain text (1.5 instead of 1.2) ([#1139](https://github.com/binwiederhier/ntfy/issues/1139), thanks to [@etfz](https://github.com/etfz) for reporting)
|
|
||||||
* Web: Fix long lines (e.g. JSON) being truncated by adding horizontal scroll ([#1363](https://github.com/binwiederhier/ntfy/issues/1363), thanks to [@v3DJG6GL](https://github.com/v3DJG6GL) for reporting)
|
|
||||||
* Web: Fix Windows notification icon being cut off ([#884](https://github.com/binwiederhier/ntfy/issues/884), thanks to [@ZhangTianrong](https://github.com/ZhangTianrong) for reporting)
|
|
||||||
* Web: Use full URL in curl example on empty topic pages ([#1435](https://github.com/binwiederhier/ntfy/issues/1435), [#1535](https://github.com/binwiederhier/ntfy/pull/1535), thanks to [@elmatadoor](https://github.com/elmatadoor) for reporting and [@jjasghar](https://github.com/jjasghar) for the PR)
|
|
||||||
* Web: Add validation feedback for service URL when adding user ([#1566](https://github.com/binwiederhier/ntfy/issues/1566), thanks to [@jermanuts](https://github.com/jermanuts))
|
|
||||||
* Docs: Remove obsolete `version` field from docker-compose examples ([#1333](https://github.com/binwiederhier/ntfy/issues/1333), thanks to [@seals187](https://github.com/seals187) for reporting and [@cyb3rko](https://github.com/cyb3rko) for fixing)
|
|
||||||
* Docs: Fix Kustomize config in installation docs ([#1367](https://github.com/binwiederhier/ntfy/issues/1367), thanks to [@toby-griffiths](https://github.com/toby-griffiths))
|
|
||||||
* Docs: Use SVG F-Droid badge and add app store badges to README ([#1170](https://github.com/binwiederhier/ntfy/issues/1170), thanks to [@PanderMusubi](https://github.com/PanderMusubi) for reporting)
|
|
||||||
|
|
||||||
## ntfy Android app v1.22.2
|
|
||||||
Released January 20, 2026
|
Released January 20, 2026
|
||||||
|
|
||||||
This release adds support for [updating and deleting notifications](publish.md#updating-deleting-notifications) (requires server v2.16.0),
|
This release adds support for [updating and deleting notifications](publish.md#updating--deleting-notifications) (requires server v2.16.0),
|
||||||
as well as [certificate management for self-signed certs and mTLS client certificates](subscribe/phone.md#manage-certificates),
|
as well as [certificate management for self-signed certs and mTLS client certificates](subscribe/phone.md#manage-certificates),
|
||||||
and a new connection error dialog to help [troubleshoot connection issues](subscribe/phone.md#troubleshooting).
|
and a new connection error dialog to help [troubleshoot connection issues](subscribe/phone.md#troubleshooting).
|
||||||
|
|
||||||
@@ -1698,19 +1665,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
## Not released yet
|
## Not released yet
|
||||||
|
|
||||||
### ntfy Android v1.23.x (UNRELEASED)
|
_Nothing here_
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Search within a topic ([#141](https://github.com/binwiederhier/ntfy/issues/141), [ntfy-android#153](https://github.com/binwiederhier/ntfy-android/pull/153), thanks to [@Copephobia](https://github.com/Copephobia) and [@StoyanYonkov](https://github.com/StoyanYonkov) for reporting and sponsoring)
|
|
||||||
* Add "reconnecting to N topics ..." to foreground notification ([#1101](https://github.com/binwiederhier/ntfy/issues/1101), thanks to [@milosivanovic](https://github.com/milosivanovic) for reporting)
|
|
||||||
* Improved default server dialog with full-screen UI and stricter URL validation ([#1582](https://github.com/binwiederhier/ntfy/issues/1582))
|
|
||||||
* Show last notification time for UnifiedPush subscriptions ([#1230](https://github.com/binwiederhier/ntfy/issues/1230), [#1454](https://github.com/binwiederhier/ntfy/issues/1454), thanks to [@Tealk](https://github.com/Tealk) and [@user4andre](https://github.com/user4andre) for reporting)
|
|
||||||
* Support "copy" action button to copy a value to the clipboard ([#1364](https://github.com/binwiederhier/ntfy/issues/1364), thanks to [@SudoWatson](https://github.com/SudoWatson) for reporting)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Fix `clear=true` on action buttons not marking notification as read ([#1029](https://github.com/binwiederhier/ntfy/issues/1029), thanks to [@ElFishi](https://github.com/ElFishi) for reporting)
|
|
||||||
* Fix crash when default server URL is missing scheme by auto-prepending `https://` ([#1582](https://github.com/binwiederhier/ntfy/issues/1582), thanks to [@hard-zero1](https://github.com/hard-zero1))
|
|
||||||
* Fix notification timestamp to use original send time instead of receive time ([#1112](https://github.com/binwiederhier/ntfy/issues/1112), thanks to [@voruti](https://github.com/voruti) for reporting)
|
|
||||||
* Fix notifications being missed after service restart by using persisted lastNotificationId ([#1591](https://github.com/binwiederhier/ntfy/issues/1591), thanks to @Epifeny for reporting)
|
|
||||||
240
docs/static/img/badge-fdroid.svg
vendored
240
docs/static/img/badge-fdroid.svg
vendored
@@ -1,240 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
viewBox="43 43 560 164"
|
|
||||||
version="1.1"
|
|
||||||
id="svg78"
|
|
||||||
sodipodi:docname="get-it-on-en.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview80"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1" />
|
|
||||||
<defs
|
|
||||||
id="defs8">
|
|
||||||
<radialGradient
|
|
||||||
xlink:href="#a"
|
|
||||||
id="b"
|
|
||||||
cx="113"
|
|
||||||
cy="-12.89"
|
|
||||||
r="59.662"
|
|
||||||
fx="113"
|
|
||||||
fy="-12.89"
|
|
||||||
gradientTransform="matrix(0 1.96105 -1.97781 0 254.507 78.763)"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
id="a">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
style="stop-color:#fff;stop-opacity:.09803922"
|
|
||||||
id="stop3" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
style="stop-color:#fff;stop-opacity:0"
|
|
||||||
id="stop5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
transform="translate(-289,-312.362)"
|
|
||||||
id="g76">
|
|
||||||
<path
|
|
||||||
id="rect10"
|
|
||||||
style="display:inline;overflow:visible;stroke:#a6a6a6;stroke-width:4;marker:none"
|
|
||||||
d="m 352,355.362 h 520 c 11.08,0 20,8.92 20,20 v 124 c 0,11.08 -8.92,20 -20,20 H 352 c -11.08,0 -20,-8.92 -20,-20 v -124 c 0,-11.08 8.92,-20 20,-20 z" />
|
|
||||||
<g
|
|
||||||
aria-label="GET IT ON"
|
|
||||||
id="text14"
|
|
||||||
style="font-size:12.3952px;line-height:100%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0;word-spacing:0;display:inline;overflow:visible;fill:#ffffff;stroke-width:1px;marker:none">
|
|
||||||
<path
|
|
||||||
d="m 529.2627,398.81787 v -6.6817 h -5.49866 v -2.76599 h 8.83117 v 10.68072 q -1.94952,1.383 -4.29895,2.09949 -2.34942,0.69983 -5.01544,0.69983 -5.83191,0 -9.1311,-3.39917 -3.28253,-3.41583 -3.28253,-9.49768 0,-6.09851 3.28253,-9.49768 3.29919,-3.41583 9.1311,-3.41583 2.43274,0 4.61554,0.59985 2.19947,0.59985 4.04901,1.76623 v 3.58246 q -1.86621,-1.58294 -3.96569,-2.38275 -2.09949,-0.7998 -4.41559,-0.7998 -4.56555,0 -6.86499,2.54937 -2.28278,2.54938 -2.28278,7.59815 0,5.0321 2.28278,7.58148 2.29944,2.54938 6.86499,2.54938 1.78289,0 3.18255,-0.29993 1.39966,-0.31659 2.51606,-0.96643 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path83" />
|
|
||||||
<path
|
|
||||||
d="m 538.74371,377.48975 h 15.7295 v 2.83264 h -12.36365 v 7.36487 h 11.84711 v 2.83264 h -11.84711 v 9.01446 h 12.66357 v 2.83264 h -16.02942 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path85" />
|
|
||||||
<path
|
|
||||||
d="m 556.85596,377.48975 h 21.04486 v 2.83264 h -8.83118 V 402.367 h -3.38251 v -22.04461 h -8.83117 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path87" />
|
|
||||||
<path
|
|
||||||
d="m 591.99738,377.48975 h 3.36584 V 402.367 h -3.36584 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path89" />
|
|
||||||
<path
|
|
||||||
d="m 598.61243,377.48975 h 21.04486 v 2.83264 h -8.83118 V 402.367 h -3.38251 v -22.04461 h -8.83117 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path91" />
|
|
||||||
<path
|
|
||||||
d="m 643.85138,379.77252 q -3.66577,0 -5.83191,2.73267 -2.14947,2.73266 -2.14947,7.44818 0,4.69885 2.14947,7.43152 2.16614,2.73266 5.83191,2.73266 3.66577,0 5.79858,-2.73266 2.14948,-2.73267 2.14948,-7.43152 0,-4.71552 -2.14948,-7.44818 -2.13281,-2.73267 -5.79858,-2.73267 z m 0,-2.73266 q 5.23206,0 8.36462,3.5158 3.13257,3.49915 3.13257,9.39771 0,5.8819 -3.13257,9.3977 -3.13256,3.49915 -8.36462,3.49915 -5.24872,0 -8.39795,-3.49915 -3.13257,-3.49914 -3.13257,-9.3977 0,-5.89856 3.13257,-9.39771 3.14923,-3.5158 8.39795,-3.5158 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path93" />
|
|
||||||
<path
|
|
||||||
d="m 660.61395,377.48975 h 4.53223 l 11.03064,20.81158 v -20.81158 h 3.26587 V 402.367 h -4.53223 L 663.87982,381.55542 V 402.367 h -3.26587 z"
|
|
||||||
style="font-size:34.125px"
|
|
||||||
id="path95" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
aria-label="F-Droid"
|
|
||||||
id="text18"
|
|
||||||
style="font-weight:700;font-size:29.7088px;line-height:100%;font-family:Rokkitt;-inkscape-font-specification:'Rokkitt Bold';letter-spacing:0;word-spacing:0;display:inline;overflow:visible;fill:#ffffff;stroke-width:1px;marker:none">
|
|
||||||
<path
|
|
||||||
d="m 510.81067,481.24332 v 8.11767 h 27.97119 v -8.11767 l -7.23633,-1.3916 v -18.55469 h 23.65723 v -10.43701 h -23.65723 v -18.60108 h 22.03369 l 0.60303,8.07129 h 10.39063 v -18.5083 h -53.76221 v 8.16406 l 7.18994,1.3916 v 48.47413 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path98" />
|
|
||||||
<path
|
|
||||||
d="m 599.13098,465.70377 v -10.43702 h -26.16211 v 10.43702 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path100" />
|
|
||||||
<path
|
|
||||||
d="m 637.67834,421.82193 h -30.3833 v 8.16406 l 7.18995,1.3916 v 48.47413 l -7.18995,1.3916 v 8.11767 h 30.3833 c 16.51368,0 28.43506,-11.59668 28.43506,-28.15674 v -11.1792 c 0,-16.51367 -11.92138,-28.20312 -28.43506,-28.20312 z m -9.64843,10.43701 h 8.95263 c 9.69483,0 15.53955,7.23633 15.53955,17.67334 v 11.27197 c 0,10.57618 -5.84472,17.76612 -15.53955,17.76612 h -8.95263 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path102" />
|
|
||||||
<path
|
|
||||||
d="m 674.09192,481.24332 v 8.11767 h 26.5332 v -8.11767 l -6.49414,-1.3916 v -24.58497 c 1.48438,-2.82959 3.89649,-4.31396 7.88574,-4.12841 l 6.67969,0.3247 1.43799,-12.47802 c -1.29883,-0.46387 -3.43262,-0.74219 -5.33447,-0.74219 -4.87061,0 -8.62793,3.06152 -10.99366,8.25683 l -0.0928,-1.11328 -0.51025,-6.21582 h -19.80713 v 8.16407 l 7.18994,1.3916 v 31.12549 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path104" />
|
|
||||||
<path
|
|
||||||
d="m 713.24231,463.80191 v 0.97412 c 0,15.07569 8.85986,25.55908 23.75,25.55908 14.70459,0 23.61084,-10.48339 23.61084,-25.55908 v -0.97412 c 0,-15.0293 -8.85986,-25.55908 -23.70361,-25.55908 -14.79737,0 -23.65723,10.57617 -23.65723,25.55908 z m 13.54492,0.97412 v -0.97412 c 0,-8.90625 3.06152,-15.12207 10.11231,-15.12207 7.05078,0 10.20507,6.21582 10.20507,15.12207 v 0.97412 c 0,9.0918 -3.10791,15.16846 -10.1123,15.16846 -7.18994,0 -10.20508,-6.03027 -10.20508,-15.16846 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path106" />
|
|
||||||
<path
|
|
||||||
d="M 786.16223,428.548 V 416.99771 H 772.15344 V 428.548 Z m -20.08545,52.69532 v 8.11767 h 26.57959 v -8.11767 l -6.49414,-1.3916 v -40.68116 h -20.78125 v 8.16407 l 7.23633,1.3916 v 31.12549 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path108" />
|
|
||||||
<path
|
|
||||||
d="m 829.76575,483.23795 1.0205,6.12304 h 18.22999 v -8.11767 l -6.49415,-1.3916 v -62.85401 h -20.78125 v 8.16406 l 7.23633,1.39161 v 17.99804 c -3.01513,-4.03564 -7.05078,-6.30859 -12.06054,-6.30859 -12.43164,0 -19.62159,10.62256 -19.62159,26.44043 v 0.97412 c 0,14.84375 7.14356,24.67773 19.52881,24.67773 5.52002,0 9.7876,-2.45849 12.9419,-7.09716 z m -18.92578,-17.58057 v -0.97412 c 0,-9.46289 2.87597,-15.91065 9.50927,-15.91065 3.89649,0 6.77246,1.85547 8.62793,5.05616 v 21.2915 c -1.85547,3.01514 -4.77783,4.68506 -8.7207,4.68506 -6.67969,0 -9.4165,-5.38086 -9.4165,-14.14795 z"
|
|
||||||
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
|
|
||||||
id="path110" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
d="m 2.589,1006.862 4.25,5.5"
|
|
||||||
style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
|
|
||||||
id="path20" />
|
|
||||||
<path
|
|
||||||
d="m 2.611,1005.61 c -0.453,0.011 -0.761,0.188 -0.98,0.448 2.027,2.409 2.368,2.792 5.135,6.221 1.02,1.32 2.082,0.638 1.062,-0.681 l -4.25,-5.5 a 1.24,1.24 0 0 0 -0.967,-0.489"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.298039;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
|
|
||||||
id="path22" />
|
|
||||||
<path
|
|
||||||
d="m 1.622,1006.07 a 1.25,1.25 0 0 0 -0.022,1.557 l 4.25,5.5 c 1.02,1.319 1.15,-0.613 1.15,-0.613 0,0 -3.735,-4.51 -5.378,-6.443"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
|
|
||||||
id="path24" />
|
|
||||||
<path
|
|
||||||
d="m 2.338,1005.844 c -0.438,0 -0.96,0.142 -0.824,0.799 0.103,0.501 4.66,6.074 4.66,6.074 1.02,1.32 2.494,0.677 1.474,-0.642 l -4.234,-5.473 c -0.26,-0.29 -0.608,-0.744 -1.076,-0.758"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
|
|
||||||
id="path26" />
|
|
||||||
<path
|
|
||||||
d="m 2.589,1006.862 4.25,5.5"
|
|
||||||
style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
|
|
||||||
id="path28" />
|
|
||||||
<path
|
|
||||||
d="m 2.611,1005.61 c -0.453,0.011 -0.761,0.188 -0.98,0.448 2.027,2.409 2.368,2.792 5.135,6.221 1.02,1.32 2.082,0.638 1.062,-0.681 l -4.25,-5.5 a 1.24,1.24 0 0 0 -0.967,-0.489"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.298039;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
|
|
||||||
id="path30" />
|
|
||||||
<path
|
|
||||||
d="m 1.622,1006.07 a 1.25,1.25 0 0 0 -0.022,1.557 l 4.25,5.5 c 1.02,1.319 1.15,-0.613 1.15,-0.613 0,0 -3.735,-4.51 -5.378,-6.443"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
|
|
||||||
id="path32" />
|
|
||||||
<path
|
|
||||||
d="m 2.338,1005.844 c -0.438,0 -0.96,0.142 -0.824,0.799 0.103,0.501 4.66,6.074 4.66,6.074 1.02,1.32 2.494,0.677 1.474,-0.642 l -4.234,-5.473 c -0.26,-0.29 -0.608,-0.744 -1.076,-0.758"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
|
|
||||||
id="path34" />
|
|
||||||
<g
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,467.369,-2270.475)"
|
|
||||||
id="g44">
|
|
||||||
<path
|
|
||||||
id="rect36"
|
|
||||||
style="opacity:1;fill:#aeea00;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m -34,1010.36 h 32 c 1.662,0 3,1.338 3,3 v 6.92 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -6.92 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect38"
|
|
||||||
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m -34,1013.279 h 32 c 1.662,0 3,1.338 3,3 v 4 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -4 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect40"
|
|
||||||
style="opacity:1;fill:#ffffff;fill-opacity:0.298039;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m -34,1010.362 h 32 c 1.662,0 3,1.338 3,3 v 4 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -4 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect42"
|
|
||||||
style="opacity:1;fill:#aeea00;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m -34,1011.5 h 32 c 1.662,0 3,1.0954 3,2.456 v 5.729 c 0,1.3606 -1.338,2.456 -3,2.456 h -32 c -1.662,0 -3,-1.0954 -3,-2.456 v -5.729 c 0,-1.3606 1.338,-2.456 3,-2.456 z" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.745)"
|
|
||||||
id="g54">
|
|
||||||
<path
|
|
||||||
id="rect46"
|
|
||||||
style="opacity:1;fill:#1976d2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m 8,1024.522 h 32 c 1.662,0 3,1.338 3,3 v 19.84 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -19.84 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect48"
|
|
||||||
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m 8,1037.3621 h 32 c 1.662,0 3,1.338 3,3 v 7 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -7 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect50"
|
|
||||||
style="opacity:1;fill:#ffffff;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m 8,1024.442 h 32 c 1.662,0 3,1.338 3,3 v 7 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -7 c 0,-1.662 1.338,-3 3,-3 z" />
|
|
||||||
<path
|
|
||||||
id="rect52"
|
|
||||||
style="opacity:1;fill:#1976d2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
|
|
||||||
d="m 8,1025.662 h 32 c 1.662,0 3,1.2122 3,2.718 v 18.124 c 0,1.5058 -1.338,2.718 -3,2.718 H 8 c -1.662,0 -3,-1.2122 -3,-2.718 v -18.124 c 0,-1.5058 1.338,-2.718 3,-2.718 z" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,396.264)"
|
|
||||||
id="g60">
|
|
||||||
<path
|
|
||||||
d="m 24,17.75 c -2.88,0 -5.32,1.985 -6.033,4.65 H 21.18 A 3.22,3.22 0 0 1 24,20.75 3.23,3.23 0 0 1 27.25,24 3.23,3.23 0 0 1 24,27.25 3.22,3.22 0 0 1 21.07,25.4 h -3.154 c 0.642,2.766 3.132,4.85 6.084,4.85 3.434,0 6.25,-2.816 6.25,-6.25 0,-3.434 -2.816,-6.25 -6.25,-6.25"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0d47a1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
id="path56" />
|
|
||||||
<path
|
|
||||||
id="circle58"
|
|
||||||
style="opacity:1;fill:none;fill-opacity:0.403922;stroke:#0d47a1;stroke-width:1.9;stroke-linecap:round"
|
|
||||||
d="M 33.55,24 A 9.5500002,9.5500002 0 0 1 24,33.55 9.5500002,9.5500002 0 0 1 14.45,24 9.5500002,9.5500002 0 0 1 24,14.45 9.5500002,9.5500002 0 0 1 33.55,24 Z" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,356.842,-2269.159)"
|
|
||||||
id="g66">
|
|
||||||
<path
|
|
||||||
id="ellipse62"
|
|
||||||
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
|
|
||||||
d="m 17.75,1016.487 a 3.375,3.875 0 0 1 -3.375,3.875 3.375,3.875 0 0 1 -3.375,-3.875 3.375,3.875 0 0 1 3.375,-3.875 3.375,3.875 0 0 1 3.375,3.875 z" />
|
|
||||||
<path
|
|
||||||
id="circle64"
|
|
||||||
style="opacity:1;fill:#ffffff;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
|
|
||||||
d="m 17.75,1016.987 a 3.375,3.375 0 0 1 -3.375,3.375 3.375,3.375 0 0 1 -3.375,-3.375 3.375,3.375 0 0 1 3.375,-3.375 3.375,3.375 0 0 1 3.375,3.375 z" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(2.63159,0,0,2.63157,408.158,-2269.159)"
|
|
||||||
id="g72">
|
|
||||||
<path
|
|
||||||
id="ellipse68"
|
|
||||||
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
|
|
||||||
d="m 17.75,1016.487 a 3.375,3.875 0 0 1 -3.375,3.875 3.375,3.875 0 0 1 -3.375,-3.875 3.375,3.875 0 0 1 3.375,-3.875 3.375,3.875 0 0 1 3.375,3.875 z" />
|
|
||||||
<path
|
|
||||||
id="circle70"
|
|
||||||
style="opacity:1;fill:#ffffff;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
|
|
||||||
d="m 17.75,1016.987 a 3.375,3.375 0 0 1 -3.375,3.375 3.375,3.375 0 0 1 -3.375,-3.375 3.375,3.375 0 0 1 3.375,-3.375 3.375,3.375 0 0 1 3.375,3.375 z" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
d="m 282.715,299.835 a 3.29,3.29 0 0 0 -2.662,5.336 l 9.474,12.261 A 7.9,7.9 0 0 0 289,320.257 v 18.21 a 7.877,7.877 0 0 0 7.895,7.895 h 84.21 A 7.877,7.877 0 0 0 389,338.468 v -18.211 c 0,-0.999 -0.19,-1.949 -0.525,-2.826 l 9.472,-12.26 a 3.29,3.29 0 0 0 -2.433,-5.334 3.29,3.29 0 0 0 -2.772,1.31 l -9.013,11.666 a 7.9,7.9 0 0 0 -2.624,-0.45 h -84.21 c -0.922,0 -1.8,0.163 -2.622,0.45 l -9.015,-11.666 a 3.29,3.29 0 0 0 -2.543,-1.312 m 14.18,49.527 A 7.877,7.877 0 0 0 289,357.257 v 52.21 a 7.877,7.877 0 0 0 7.895,7.895 h 84.21 A 7.877,7.877 0 0 0 389,409.468 v -52.211 a 7.877,7.877 0 0 0 -7.895,-7.895 z"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#b);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:6.57895;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
|
|
||||||
transform="translate(81,76)"
|
|
||||||
id="path74" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 23 KiB |
@@ -5,7 +5,7 @@ on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https
|
|||||||
contribute, or [build your own](../develop.md).
|
contribute, or [build your own](../develop.md).
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="../../static/img/badge-googleplay.png"></a>
|
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="../../static/img/badge-googleplay.png"></a>
|
||||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.svg"></a>
|
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.png"></a>
|
||||||
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a>
|
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a>
|
||||||
|
|
||||||
You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy),
|
You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy),
|
||||||
@@ -82,8 +82,9 @@ you'll see as a permanent notification that looks like this:
|
|||||||
<figcaption>Instant delivery foreground notification</figcaption>
|
<figcaption>Instant delivery foreground notification</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
To turn off this notification, long-press on the foreground notification (screenshot above) and navigate to the
|
Android does not allow you to dismiss this notification, unless you turn off the notification channel in the settings.
|
||||||
settings. Then toggle the "Subscription Service" off:
|
To do so, long-press on the foreground notification (screenshot above) and navigate to the settings. Then toggle the
|
||||||
|
"Subscription Service" off:
|
||||||
|
|
||||||
<figure markdown>
|
<figure markdown>
|
||||||
{ width=500 }
|
{ width=500 }
|
||||||
@@ -101,11 +102,6 @@ notifications. Firebase is overall pretty bad at delivering messages in time, bu
|
|||||||
The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in the Google Play flavor of the app.
|
The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in the Google Play flavor of the app.
|
||||||
It won't use Firebase for any self-hosted servers, and not at all in the F-Droid flavor.
|
It won't use Firebase for any self-hosted servers, and not at all in the F-Droid flavor.
|
||||||
|
|
||||||
!!! info "F-Droid: Always instant delivery"
|
|
||||||
Since the F-Droid build does not include Firebase, **all subscriptions use instant delivery by default**, and
|
|
||||||
there is no option to disable it. The F-Droid app hides all mentions of "instant delivery" in the UI, since
|
|
||||||
showing options that can't be changed would only be confusing.
|
|
||||||
|
|
||||||
## Publishing messages
|
## Publishing messages
|
||||||
_Supported on:_ :material-android:
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
|
|||||||
38
go.mod
38
go.mod
@@ -1,14 +1,16 @@
|
|||||||
module heckel.io/ntfy/v2
|
module heckel.io/ntfy/v2
|
||||||
|
|
||||||
go 1.24.6
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.21.0 // indirect
|
cloud.google.com/go/firestore v1.21.0 // indirect
|
||||||
cloud.google.com/go/storage v1.59.2 // indirect
|
cloud.google.com/go/storage v1.59.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/emersion/go-smtp v0.18.0
|
github.com/emersion/go-smtp v0.18.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13
|
github.com/gabriel-vasile/mimetype v1.4.12
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/mattn/go-sqlite3 v1.14.33
|
github.com/mattn/go-sqlite3 v1.14.33
|
||||||
github.com/olebedev/when v1.1.0
|
github.com/olebedev/when v1.1.0
|
||||||
@@ -19,7 +21,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.39.0
|
golang.org/x/term v0.39.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
google.golang.org/api v0.265.0
|
google.golang.org/api v0.262.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ require (
|
|||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||||
@@ -64,12 +66,12 @@ require (
|
|||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
@@ -82,20 +84,20 @@ require (
|
|||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
|
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20 // indirect
|
google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
68
go.sum
68
go.sum
@@ -18,8 +18,8 @@ cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7
|
|||||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
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 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||||
cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw=
|
cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58=
|
||||||
cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||||
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
|
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
|
||||||
@@ -46,8 +46,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94 h1:kkHPnzHm5Ln7WA0XYjrr2ITA0l9Vs6H++Ni//P+SZso=
|
||||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -68,8 +68,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg
|
|||||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -81,8 +81,8 @@ github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
|||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
@@ -98,8 +98,8 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
@@ -156,24 +156,24 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
|
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=
|
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
@@ -263,16 +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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
|
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
||||||
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
|
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
||||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
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/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||||
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20 h1:/CU1zrxTpGylJJbe3Ru94yy6sZRbzALq2/oxl3pGB3U=
|
google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d h1:hUplc9kLwH374NIY3PreRUK3Unc0xLm/W7MDsm0gCNo=
|
||||||
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20/go.mod h1:Tt+08/KdKEt3l8x3Pby3HLQxMB3uk/MzaQ4ZIv0ORTs=
|
google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:SpjiK7gGN2j/djoQMxLl3QOe/J/XxNzC5M+YLecVVWU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
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/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ nav:
|
|||||||
- "Other things":
|
- "Other things":
|
||||||
- "FAQs": faq.md
|
- "FAQs": faq.md
|
||||||
- "Examples": examples.md
|
- "Examples": examples.md
|
||||||
|
- "API Reference": /api/
|
||||||
- "Integrations + projects": integrations.md
|
- "Integrations + projects": integrations.md
|
||||||
- "Release notes": releases.md
|
- "Release notes": releases.md
|
||||||
- "Emojis 🥳 🎉": emojis.md
|
- "Emojis 🥳 🎉": emojis.md
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"heckel.io/ntfy/v2/util"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"heckel.io/ntfy/v2/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -21,14 +20,12 @@ const (
|
|||||||
actionView = "view"
|
actionView = "view"
|
||||||
actionBroadcast = "broadcast"
|
actionBroadcast = "broadcast"
|
||||||
actionHTTP = "http"
|
actionHTTP = "http"
|
||||||
actionCopy = "copy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
actionsAll = []string{actionView, actionBroadcast, actionHTTP, actionCopy}
|
actionsAll = []string{actionView, actionBroadcast, actionHTTP}
|
||||||
actionsWithURL = []string{actionView, actionHTTP} // Must be distinct from actionsWithValue, see populateAction()
|
actionsWithURL = []string{actionView, actionHTTP}
|
||||||
actionsWithValue = []string{actionCopy} // Must be distinct from actionsWithURL, see populateAction()
|
actionsKeyRegex = regexp.MustCompile(`^([-.\w]+)\s*=\s*`)
|
||||||
actionsKeyRegex = regexp.MustCompile(`^([-.\w]+)\s*=\s*`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type actionParser struct {
|
type actionParser struct {
|
||||||
@@ -64,13 +61,11 @@ func parseActions(s string) (actions []*action, err error) {
|
|||||||
}
|
}
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
if !util.Contains(actionsAll, action.Action) {
|
if !util.Contains(actionsAll, action.Action) {
|
||||||
return nil, fmt.Errorf("parameter 'action' cannot be '%s', valid values are 'view', 'broadcast', 'http' and 'copy'", action.Action)
|
return nil, fmt.Errorf("parameter 'action' cannot be '%s', valid values are 'view', 'broadcast' and 'http'", action.Action)
|
||||||
} else if action.Label == "" {
|
} else if action.Label == "" {
|
||||||
return nil, fmt.Errorf("parameter 'label' is required")
|
return nil, fmt.Errorf("parameter 'label' is required")
|
||||||
} else if util.Contains(actionsWithURL, action.Action) && action.URL == "" {
|
} else if util.Contains(actionsWithURL, action.Action) && action.URL == "" {
|
||||||
return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action)
|
return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action)
|
||||||
} else if util.Contains(actionsWithValue, action.Action) && action.Value == "" {
|
|
||||||
return nil, fmt.Errorf("parameter 'value' is required for action '%s'", action.Action)
|
|
||||||
} else if action.Action == actionHTTP && util.Contains([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
|
} else if action.Action == actionHTTP && util.Contains([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
|
||||||
return nil, fmt.Errorf("parameter 'body' cannot be set if method is %s", action.Method)
|
return nil, fmt.Errorf("parameter 'body' cannot be set if method is %s", action.Method)
|
||||||
}
|
}
|
||||||
@@ -163,8 +158,6 @@ func populateAction(newAction *action, section int, key, value string) error {
|
|||||||
key = "label"
|
key = "label"
|
||||||
} else if key == "" && section == 2 && util.Contains(actionsWithURL, newAction.Action) {
|
} else if key == "" && section == 2 && util.Contains(actionsWithURL, newAction.Action) {
|
||||||
key = "url"
|
key = "url"
|
||||||
} else if key == "" && section == 2 && util.Contains(actionsWithValue, newAction.Action) {
|
|
||||||
key = "value"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
@@ -195,8 +188,6 @@ func populateAction(newAction *action, section int, key, value string) error {
|
|||||||
newAction.Method = value
|
newAction.Method = value
|
||||||
case "body":
|
case "body":
|
||||||
newAction.Body = value
|
newAction.Body = value
|
||||||
case "value":
|
|
||||||
newAction.Value = value
|
|
||||||
case "intent":
|
case "intent":
|
||||||
newAction.Intent = value
|
newAction.Intent = value
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseActions(t *testing.T) {
|
func TestParseActions(t *testing.T) {
|
||||||
@@ -133,44 +132,6 @@ func TestParseActions(t *testing.T) {
|
|||||||
require.Equal(t, `https://x.org`, actions[1].URL)
|
require.Equal(t, `https://x.org`, actions[1].URL)
|
||||||
require.Equal(t, true, actions[1].Clear)
|
require.Equal(t, true, actions[1].Clear)
|
||||||
|
|
||||||
// Copy action (simple format)
|
|
||||||
actions, err = parseActions("copy, Copy code, 1234")
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(actions))
|
|
||||||
require.Equal(t, "copy", actions[0].Action)
|
|
||||||
require.Equal(t, "Copy code", actions[0].Label)
|
|
||||||
require.Equal(t, "1234", actions[0].Value)
|
|
||||||
|
|
||||||
// Copy action (JSON)
|
|
||||||
actions, err = parseActions(`[{"action":"copy","label":"Copy OTP","value":"567890"}]`)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(actions))
|
|
||||||
require.Equal(t, "copy", actions[0].Action)
|
|
||||||
require.Equal(t, "Copy OTP", actions[0].Label)
|
|
||||||
require.Equal(t, "567890", actions[0].Value)
|
|
||||||
|
|
||||||
// Copy action with clear
|
|
||||||
actions, err = parseActions("copy, Copy code, 1234, clear=true")
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(actions))
|
|
||||||
require.Equal(t, "copy", actions[0].Action)
|
|
||||||
require.Equal(t, "Copy code", actions[0].Label)
|
|
||||||
require.Equal(t, "1234", actions[0].Value)
|
|
||||||
require.Equal(t, true, actions[0].Clear)
|
|
||||||
|
|
||||||
// Copy action with explicit value key
|
|
||||||
actions, err = parseActions("action=copy, label=Copy token, clear=true, value=abc-123-def")
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(actions))
|
|
||||||
require.Equal(t, "copy", actions[0].Action)
|
|
||||||
require.Equal(t, "Copy token", actions[0].Label)
|
|
||||||
require.Equal(t, "abc-123-def", actions[0].Value)
|
|
||||||
require.True(t, actions[0].Clear)
|
|
||||||
|
|
||||||
// Copy action without value (error)
|
|
||||||
_, err = parseActions("copy, Copy code")
|
|
||||||
require.EqualError(t, err, "parameter 'value' is required for action 'copy'")
|
|
||||||
|
|
||||||
// Invalid syntax
|
// Invalid syntax
|
||||||
_, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`)
|
_, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`)
|
||||||
require.EqualError(t, err, "unexpected character 'x' at position 22")
|
require.EqualError(t, err, "unexpected character 'x' at position 22")
|
||||||
@@ -185,7 +146,7 @@ func TestParseActions(t *testing.T) {
|
|||||||
require.EqualError(t, err, "term 'what is this anyway' unknown")
|
require.EqualError(t, err, "term 'what is this anyway' unknown")
|
||||||
|
|
||||||
_, err = parseActions(`fdsfdsf`)
|
_, err = parseActions(`fdsfdsf`)
|
||||||
require.EqualError(t, err, "parameter 'action' cannot be 'fdsfdsf', valid values are 'view', 'broadcast', 'http' and 'copy'")
|
require.EqualError(t, err, "parameter 'action' cannot be 'fdsfdsf', valid values are 'view', 'broadcast' and 'http'")
|
||||||
|
|
||||||
_, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`)
|
_, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`)
|
||||||
require.EqualError(t, err, "key 'aaa' unknown")
|
require.EqualError(t, err, "key 'aaa' unknown")
|
||||||
@@ -212,7 +173,7 @@ func TestParseActions(t *testing.T) {
|
|||||||
require.EqualError(t, err, "JSON error: invalid character 'i' looking for beginning of value")
|
require.EqualError(t, err, "JSON error: invalid character 'i' looking for beginning of value")
|
||||||
|
|
||||||
_, err = parseActions(`[ { "some": "object" } ]`)
|
_, err = parseActions(`[ { "some": "object" } ]`)
|
||||||
require.EqualError(t, err, "parameter 'action' cannot be '', valid values are 'view', 'broadcast', 'http' and 'copy'")
|
require.EqualError(t, err, "parameter 'action' cannot be '', valid values are 'view', 'broadcast' and 'http'")
|
||||||
|
|
||||||
_, err = parseActions("\x00\x01\xFFx\xFE")
|
_, err = parseActions("\x00\x01\xFFx\xFE")
|
||||||
require.EqualError(t, err, "invalid utf-8 string")
|
require.EqualError(t, err, "invalid utf-8 string")
|
||||||
|
|||||||
@@ -78,21 +78,6 @@ func (e errHTTP) clone() errHTTP {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// errWebSocketPostUpgrade is a wrapper error indicating an error occurred after the WebSocket
|
|
||||||
// upgrade completed (i.e., the connection was hijacked). This is used to avoid calling
|
|
||||||
// WriteHeader on hijacked connections, which causes log spam.
|
|
||||||
type errWebSocketPostUpgrade struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *errWebSocketPostUpgrade) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *errWebSocketPostUpgrade) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil}
|
errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil}
|
||||||
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil}
|
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"heckel.io/ntfy/v2/log"
|
"heckel.io/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/v2/util"
|
"heckel.io/ntfy/v2/util"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Log tags
|
// Log tags
|
||||||
@@ -85,8 +83,7 @@ func httpContext(r *http.Request) log.Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func websocketErrorContext(err error) log.Context {
|
func websocketErrorContext(err error) log.Context {
|
||||||
var c *websocket.CloseError
|
if c, ok := err.(*websocket.CloseError); ok {
|
||||||
if errors.As(err, &c) {
|
|
||||||
return log.Context{
|
return log.Context{
|
||||||
"error": c.Error(),
|
"error": c.Error(),
|
||||||
"error_code": c.Code,
|
"error_code": c.Code,
|
||||||
|
|||||||
123
server/server.go
123
server/server.go
@@ -90,7 +90,6 @@ var (
|
|||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
metricsPath = "/metrics"
|
metricsPath = "/metrics"
|
||||||
apiHealthPath = "/v1/health"
|
apiHealthPath = "/v1/health"
|
||||||
apiVersionPath = "/v1/version"
|
|
||||||
apiConfigPath = "/v1/config"
|
apiConfigPath = "/v1/config"
|
||||||
apiStatsPath = "/v1/stats"
|
apiStatsPath = "/v1/stats"
|
||||||
apiWebPushPath = "/v1/webpush"
|
apiWebPushPath = "/v1/webpush"
|
||||||
@@ -435,14 +434,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor,
|
|||||||
} else {
|
} else {
|
||||||
ev.Info("WebSocket error: %s", err.Error())
|
ev.Info("WebSocket error: %s", err.Error())
|
||||||
}
|
}
|
||||||
// Write error response only if the connection was not hijacked yet. Bytes written to hijacked
|
w.WriteHeader(httpErr.HTTPCode)
|
||||||
// connections are WebSocket frames, not HTTP, and will cause "http: response.WriteHeader on hijacked
|
return // Do not attempt to write any body to upgraded connection
|
||||||
// connection" log spam.
|
|
||||||
var postUpgradeErr *errWebSocketPostUpgrade
|
|
||||||
if !errors.As(err, &postUpgradeErr) {
|
|
||||||
w.WriteHeader(httpErr.HTTPCode)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if isNormalError {
|
if isNormalError {
|
||||||
ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code)
|
ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code)
|
||||||
@@ -468,8 +461,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
|
||||||
return s.handleHealth(w, r, v)
|
return s.handleHealth(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiVersionPath {
|
|
||||||
return s.ensureAdmin(s.handleVersion)(w, r, v)
|
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiConfigPath {
|
||||||
return s.handleConfig(w, r, v)
|
return s.handleConfig(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||||
@@ -796,7 +787,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := newDefaultMessage(t.ID, "")
|
m := newDefaultMessage(t.ID, "")
|
||||||
cache, firebase, email, call, template, unifiedpush, priorityStr, e := s.parsePublishParams(r, m)
|
cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, e.With(t)
|
return nil, e.With(t)
|
||||||
}
|
}
|
||||||
@@ -827,7 +818,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||||||
if cache {
|
if cache {
|
||||||
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
||||||
}
|
}
|
||||||
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush, priorityStr); err != nil {
|
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
@@ -1058,11 +1049,11 @@ 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, priorityStr string, err *errHTTP) {
|
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) {
|
if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) {
|
||||||
pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path)
|
pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, "", "", "", false, "", err
|
return false, false, "", "", "", false, err
|
||||||
}
|
}
|
||||||
m.SequenceID = pathSequenceID
|
m.SequenceID = pathSequenceID
|
||||||
} else {
|
} else {
|
||||||
@@ -1071,7 +1062,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
if sequenceIDRegex.MatchString(sequenceID) {
|
if sequenceIDRegex.MatchString(sequenceID) {
|
||||||
m.SequenceID = sequenceID
|
m.SequenceID = sequenceID
|
||||||
} else {
|
} else {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestSequenceIDInvalid
|
return false, false, "", "", "", false, errHTTPBadRequestSequenceIDInvalid
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.SequenceID = m.ID
|
m.SequenceID = m.ID
|
||||||
@@ -1092,7 +1083,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
}
|
}
|
||||||
if attach != "" {
|
if attach != "" {
|
||||||
if !urlRegex.MatchString(attach) {
|
if !urlRegex.MatchString(attach) {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestAttachmentURLInvalid
|
return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid
|
||||||
}
|
}
|
||||||
m.Attachment.URL = attach
|
m.Attachment.URL = attach
|
||||||
if m.Attachment.Name == "" {
|
if m.Attachment.Name == "" {
|
||||||
@@ -1110,19 +1101,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
}
|
}
|
||||||
if icon != "" {
|
if icon != "" {
|
||||||
if !urlRegex.MatchString(icon) {
|
if !urlRegex.MatchString(icon) {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestIconURLInvalid
|
return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid
|
||||||
}
|
}
|
||||||
m.Icon = icon
|
m.Icon = icon
|
||||||
}
|
}
|
||||||
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||||
if s.smtpSender == nil && email != "" {
|
if s.smtpSender == nil && email != "" {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestEmailDisabled
|
return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled
|
||||||
}
|
}
|
||||||
call = readParam(r, "x-call", "call")
|
call = readParam(r, "x-call", "call")
|
||||||
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestPhoneCallsDisabled
|
return false, false, "", "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
||||||
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestPhoneNumberInvalid
|
return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
||||||
}
|
}
|
||||||
template = templateMode(readParam(r, "x-template", "template", "tpl"))
|
template = templateMode(readParam(r, "x-template", "template", "tpl"))
|
||||||
messageStr := readParam(r, "x-message", "message", "m")
|
messageStr := readParam(r, "x-message", "message", "m")
|
||||||
@@ -1134,33 +1125,29 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
m.Message = messageStr
|
m.Message = messageStr
|
||||||
}
|
}
|
||||||
var e error
|
var e error
|
||||||
priorityStr = readParam(r, "x-priority", "priority", "prio", "p")
|
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
||||||
if !template.Enabled() {
|
if e != nil {
|
||||||
m.Priority, e = util.ParsePriority(priorityStr)
|
return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid
|
||||||
if e != nil {
|
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestPriorityInvalid
|
|
||||||
}
|
|
||||||
priorityStr = "" // Clear since it's already parsed
|
|
||||||
}
|
}
|
||||||
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
||||||
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
||||||
if delayStr != "" {
|
if delayStr != "" {
|
||||||
if !cache {
|
if !cache {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoCache
|
return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache
|
||||||
}
|
}
|
||||||
if email != "" {
|
if email != "" {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
return false, false, "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||||
}
|
}
|
||||||
if call != "" {
|
if call != "" {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
return false, false, "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||||
}
|
}
|
||||||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayCannotParse
|
return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse
|
||||||
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooSmall
|
return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall
|
||||||
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooLarge
|
return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge
|
||||||
}
|
}
|
||||||
m.Time = delay.Unix()
|
m.Time = delay.Unix()
|
||||||
}
|
}
|
||||||
@@ -1168,7 +1155,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
if actionsStr != "" {
|
if actionsStr != "" {
|
||||||
m.Actions, e = parseActions(actionsStr)
|
m.Actions, e = parseActions(actionsStr)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return false, false, "", "", "", false, "", errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
|
return false, false, "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
||||||
@@ -1187,7 +1174,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
cache = false
|
cache = false
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
return cache, firebase, email, call, template, unifiedpush, priorityStr, nil
|
return cache, firebase, email, call, template, unifiedpush, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
||||||
@@ -1206,7 +1193,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||||
// 7. curl -T file.txt ntfy.sh/mytopic
|
// 7. curl -T file.txt ntfy.sh/mytopic
|
||||||
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
|
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
|
||||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool, priorityStr string) error {
|
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool) error {
|
||||||
if m.Event == pollRequestEvent { // Case 1
|
if m.Event == pollRequestEvent { // Case 1
|
||||||
return s.handleBodyDiscard(body)
|
return s.handleBodyDiscard(body)
|
||||||
} else if unifiedpush {
|
} else if unifiedpush {
|
||||||
@@ -1216,7 +1203,7 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
|
|||||||
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
||||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
||||||
} else if template.Enabled() {
|
} else if template.Enabled() {
|
||||||
return s.handleBodyAsTemplatedTextMessage(m, template, body, priorityStr) // Case 5
|
return s.handleBodyAsTemplatedTextMessage(m, template, body) // Case 5
|
||||||
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
||||||
return s.handleBodyAsTextMessage(m, body) // Case 6
|
return s.handleBodyAsTextMessage(m, body) // Case 6
|
||||||
}
|
}
|
||||||
@@ -1252,7 +1239,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser, priorityStr string) error {
|
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser) error {
|
||||||
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
|
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1265,7 +1252,7 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := s.renderTemplateFromParams(m, peekedBody, priorityStr); err != nil {
|
if err := s.renderTemplateFromParams(m, peekedBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1296,51 +1283,33 @@ func (s *Server) renderTemplateFromFile(m *message, templateName, peekedBody str
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if tpl.Message != nil {
|
if tpl.Message != nil {
|
||||||
if m.Message, err = s.renderTemplate(templateName+" (message)", *tpl.Message, peekedBody); err != nil {
|
if m.Message, err = s.renderTemplate(*tpl.Message, peekedBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tpl.Title != nil {
|
if tpl.Title != nil {
|
||||||
if m.Title, err = s.renderTemplate(templateName+" (title)", *tpl.Title, peekedBody); err != nil {
|
if m.Title, err = s.renderTemplate(*tpl.Title, peekedBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tpl.Priority != nil {
|
|
||||||
renderedPriority, err := s.renderTemplate(templateName+" (priority)", *tpl.Priority, peekedBody)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if m.Priority, err = util.ParsePriority(renderedPriority); err != nil {
|
|
||||||
return errHTTPBadRequestPriorityInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderTemplateFromParams transforms the JSON message body according to the inline template in the
|
// renderTemplateFromParams transforms the JSON message body according to the inline template in the
|
||||||
// message, title, and priority parameters.
|
// message and title parameters.
|
||||||
func (s *Server) renderTemplateFromParams(m *message, peekedBody string, priorityStr string) error {
|
func (s *Server) renderTemplateFromParams(m *message, peekedBody string) error {
|
||||||
var err error
|
var err error
|
||||||
if m.Message, err = s.renderTemplate("priority query parameter", m.Message, peekedBody); err != nil {
|
if m.Message, err = s.renderTemplate(m.Message, peekedBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if m.Title, err = s.renderTemplate("title query parameter", m.Title, peekedBody); err != nil {
|
if m.Title, err = s.renderTemplate(m.Title, peekedBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if priorityStr != "" {
|
|
||||||
renderedPriority, err := s.renderTemplate("priority query parameter", priorityStr, peekedBody)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if m.Priority, err = util.ParsePriority(renderedPriority); err != nil {
|
|
||||||
return errHTTPBadRequestPriorityInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderTemplate renders a template with the given JSON source data.
|
// renderTemplate renders a template with the given JSON source data.
|
||||||
func (s *Server) renderTemplate(name, tpl, source string) (string, error) {
|
func (s *Server) renderTemplate(tpl string, source string) (string, error) {
|
||||||
if templateDisallowedRegex.MatchString(tpl) {
|
if templateDisallowedRegex.MatchString(tpl) {
|
||||||
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
|
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
|
||||||
}
|
}
|
||||||
@@ -1355,7 +1324,7 @@ func (s *Server) renderTemplate(name, tpl, source string) (string, error) {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes))
|
limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes))
|
||||||
if err := t.Execute(limitWriter, data); err != nil {
|
if err := t.Execute(limitWriter, data); err != nil {
|
||||||
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("template %s: %s", name, err.Error())
|
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error())
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines
|
return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines
|
||||||
}
|
}
|
||||||
@@ -1461,16 +1430,12 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var wlock sync.Mutex
|
var wlock sync.Mutex
|
||||||
var closed bool
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// This blocks until any in-flight sub() call finishes writing/flushing the response writer,
|
// Hack: This is the fix for a horrible data race that I have not been able to figure out in quite some time.
|
||||||
// then marks the connection as closed so future sub() calls are no-ops. This prevents a panic
|
// It appears to be happening when the Go HTTP code reads from the socket when closing the request (i.e. AFTER
|
||||||
// from writing to a response writer that has been cleaned up after the handler returns.
|
// this function returns), and causes a data race with the ResponseWriter. Locking wlock here silences the
|
||||||
// See https://github.com/binwiederhier/ntfy/issues/338#issuecomment-1163425889
|
// data race detector. See https://github.com/binwiederhier/ntfy/issues/338#issuecomment-1163425889.
|
||||||
// and https://github.com/binwiederhier/ntfy/pull/1598.
|
wlock.TryLock()
|
||||||
wlock.Lock()
|
|
||||||
closed = true
|
|
||||||
wlock.Unlock()
|
|
||||||
}()
|
}()
|
||||||
sub := func(v *visitor, msg *message) error {
|
sub := func(v *visitor, msg *message) error {
|
||||||
if !filters.Pass(msg) {
|
if !filters.Pass(msg) {
|
||||||
@@ -1482,9 +1447,6 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
|||||||
}
|
}
|
||||||
wlock.Lock()
|
wlock.Lock()
|
||||||
defer wlock.Unlock()
|
defer wlock.Unlock()
|
||||||
if closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := w.Write([]byte(m)); err != nil {
|
if _, err := w.Write([]byte(m)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1675,10 +1637,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
|||||||
logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Trace("WebSocket connection closed")
|
logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Trace("WebSocket connection closed")
|
||||||
return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot
|
return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot
|
||||||
}
|
}
|
||||||
if err != nil {
|
return err
|
||||||
return &errWebSocketPostUpgrade{err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
|
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
|
||||||
return s.writeJSON(w, &apiVersionResponse{
|
|
||||||
Version: s.config.BuildVersion,
|
|
||||||
Commit: s.config.BuildCommit,
|
|
||||||
Date: s.config.BuildDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
users, err := s.userManager.Users()
|
users, err := s.userManager.Users()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/v2/user"
|
"heckel.io/ntfy/v2/user"
|
||||||
"heckel.io/ntfy/v2/util"
|
"heckel.io/ntfy/v2/util"
|
||||||
@@ -10,41 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVersion_Admin(t *testing.T) {
|
|
||||||
c := newTestConfigWithAuthFile(t)
|
|
||||||
c.BuildVersion = "1.2.3"
|
|
||||||
c.BuildCommit = "abcdef0"
|
|
||||||
c.BuildDate = "2026-02-08T00:00:00Z"
|
|
||||||
s := newTestServer(t, c)
|
|
||||||
defer s.closeDatabases()
|
|
||||||
|
|
||||||
// Create admin and regular user
|
|
||||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
|
||||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
|
||||||
|
|
||||||
// Admin can access /v1/version
|
|
||||||
rr := request(t, s, "GET", "/v1/version", "", map[string]string{
|
|
||||||
"Authorization": util.BasicAuth("phil", "phil"),
|
|
||||||
})
|
|
||||||
require.Equal(t, 200, rr.Code)
|
|
||||||
|
|
||||||
var versionResponse apiVersionResponse
|
|
||||||
require.Nil(t, json.NewDecoder(rr.Body).Decode(&versionResponse))
|
|
||||||
require.Equal(t, "1.2.3", versionResponse.Version)
|
|
||||||
require.Equal(t, "abcdef0", versionResponse.Commit)
|
|
||||||
require.Equal(t, "2026-02-08T00:00:00Z", versionResponse.Date)
|
|
||||||
|
|
||||||
// Non-admin user cannot access /v1/version
|
|
||||||
rr = request(t, s, "GET", "/v1/version", "", map[string]string{
|
|
||||||
"Authorization": util.BasicAuth("ben", "ben"),
|
|
||||||
})
|
|
||||||
require.Equal(t, 401, rr.Code)
|
|
||||||
|
|
||||||
// Unauthenticated user cannot access /v1/version
|
|
||||||
rr = request(t, s, "GET", "/v1/version", "", nil)
|
|
||||||
require.Equal(t, 401, rr.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUser_AddRemove(t *testing.T) {
|
func TestUser_AddRemove(t *testing.T) {
|
||||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||||
defer s.closeDatabases()
|
defer s.closeDatabases()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -3290,117 +3289,6 @@ func TestServer_MessageTemplate_Until100_000(t *testing.T) {
|
|||||||
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
|
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
response := request(t, s, "PUT", "/mytopic", `{"priority":"5"}`, map[string]string{
|
|
||||||
"X-Message": "Test message",
|
|
||||||
"X-Priority": "{{.priority}}",
|
|
||||||
"X-Template": "1",
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m := toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, "Test message", m.Message)
|
|
||||||
require.Equal(t, 5, m.Priority)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority_Conditional(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
|
|
||||||
// Test with error status -> priority 5
|
|
||||||
response := request(t, s, "PUT", "/mytopic", `{"status":"Error","message":"Something went wrong"}`, map[string]string{
|
|
||||||
"X-Message": "Status: {{.status}} - {{.message}}",
|
|
||||||
"X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`,
|
|
||||||
"X-Template": "1",
|
|
||||||
})
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m := toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, "Status: Error - Something went wrong", m.Message)
|
|
||||||
require.Equal(t, 5, m.Priority)
|
|
||||||
|
|
||||||
// Test with success status -> priority 3
|
|
||||||
response = request(t, s, "PUT", "/mytopic", `{"status":"Success","message":"All good"}`, map[string]string{
|
|
||||||
"X-Message": "Status: {{.status}} - {{.message}}",
|
|
||||||
"X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`,
|
|
||||||
"X-Template": "1",
|
|
||||||
})
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m = toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, "Status: Success - All good", m.Message)
|
|
||||||
require.Equal(t, 3, m.Priority)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority_NamedValue(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
response := request(t, s, "PUT", "/mytopic", `{"severity":"high"}`, map[string]string{
|
|
||||||
"X-Message": "Alert",
|
|
||||||
"X-Priority": "{{.severity}}",
|
|
||||||
"X-Template": "1",
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m := toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, 4, m.Priority) // "high" = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority_Invalid(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
response := request(t, s, "PUT", "/mytopic", `{"priority":"invalid"}`, map[string]string{
|
|
||||||
"X-Message": "Test message",
|
|
||||||
"X-Priority": "{{.priority}}",
|
|
||||||
"X-Template": "1",
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Equal(t, 400, response.Code)
|
|
||||||
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority_QueryParam(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
response := request(t, s, "PUT", "/mytopic?template=1&priority={{.priority}}", `{"priority":"max"}`, nil)
|
|
||||||
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m := toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, 5, m.Priority) // "max" = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_MessageTemplate_Priority_FromTemplateFile(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
c := newTestConfig(t)
|
|
||||||
c.TemplateDir = t.TempDir()
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "priority-test.yml"), []byte(`
|
|
||||||
title: "{{.title}}"
|
|
||||||
message: "{{.message}}"
|
|
||||||
priority: '{{if eq .level "critical"}}5{{else if eq .level "warning"}}4{{else}}3{{end}}'
|
|
||||||
`), 0644))
|
|
||||||
s := newTestServer(t, c)
|
|
||||||
|
|
||||||
// Test with critical level
|
|
||||||
response := request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"System down","level":"critical"}`, nil)
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m := toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, "Alert", m.Title)
|
|
||||||
require.Equal(t, "System down", m.Message)
|
|
||||||
require.Equal(t, 5, m.Priority)
|
|
||||||
|
|
||||||
// Test with warning level
|
|
||||||
response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"High load","level":"warning"}`, nil)
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m = toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, 4, m.Priority)
|
|
||||||
|
|
||||||
// Test with info level
|
|
||||||
response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"All good","level":"info"}`, nil)
|
|
||||||
require.Equal(t, 200, response.Code)
|
|
||||||
m = toMessage(t, response.Body.String())
|
|
||||||
require.Equal(t, 3, m.Priority)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_DeleteMessage(t *testing.T) {
|
func TestServer_DeleteMessage(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
s := newTestServer(t, newTestConfig(t))
|
s := newTestServer(t, newTestConfig(t))
|
||||||
@@ -3872,189 +3760,3 @@ func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bool) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack()))
|
t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockResponseWriter is a mock ResponseWriter for testing
|
|
||||||
type mockResponseWriter struct {
|
|
||||||
header http.Header
|
|
||||||
statusCode int
|
|
||||||
body []byte
|
|
||||||
writeHeaderHit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMockResponseWriter() *mockResponseWriter {
|
|
||||||
return &mockResponseWriter{
|
|
||||||
header: make(http.Header),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) Header() http.Header {
|
|
||||||
return m.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) Write(b []byte) (int, error) {
|
|
||||||
m.body = append(m.body, b...)
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
m.statusCode = statusCode
|
|
||||||
m.writeHeaderHit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// closableResponseWriter simulates a real HTTP response writer that becomes invalid
|
|
||||||
// after the handler returns. In production, Go's HTTP server calls finishRequest() after
|
|
||||||
// the handler returns, which nils out the underlying bufio.Writer. Any subsequent Flush()
|
|
||||||
// from a straggler Publish goroutine causes a nil pointer panic. This mock tracks whether
|
|
||||||
// any Write or Flush occurred after the handler returned (i.e. after Close was called).
|
|
||||||
type closableResponseWriter struct {
|
|
||||||
header http.Header
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
wroteAfterClose atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClosableResponseWriter() *closableResponseWriter {
|
|
||||||
return &closableResponseWriter{
|
|
||||||
header: make(http.Header),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *closableResponseWriter) Header() http.Header {
|
|
||||||
return w.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *closableResponseWriter) Write(b []byte) (int, error) {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.closed {
|
|
||||||
w.wroteAfterClose.Store(true)
|
|
||||||
return 0, errors.New("write after handler returned")
|
|
||||||
}
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *closableResponseWriter) WriteHeader(statusCode int) {}
|
|
||||||
|
|
||||||
func (w *closableResponseWriter) Flush() {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.closed {
|
|
||||||
w.wroteAfterClose.Store(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close simulates Go's HTTP server cleaning up the response writer after the handler returns.
|
|
||||||
func (w *closableResponseWriter) Close() {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
w.closed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_SubscribeHTTP_NoWriteAfterHandlerReturn(t *testing.T) {
|
|
||||||
// This test reproduces the panic from https://github.com/binwiederhier/ntfy/issues/338:
|
|
||||||
//
|
|
||||||
// panic: runtime error: invalid memory address or nil pointer dereference
|
|
||||||
// bufio.(*Writer).Flush(...)
|
|
||||||
// net/http.(*response).Flush(...)
|
|
||||||
// server.(*Server).handleSubscribeHTTP.func2(...)
|
|
||||||
// server.(*topic).Publish.func1.1(...)
|
|
||||||
//
|
|
||||||
// The race: topic.Publish() copies the subscriber list and calls each subscriber in its own
|
|
||||||
// goroutine. If the subscriber disconnects, the handler returns and Go's HTTP server cleans up
|
|
||||||
// the response writer. But a Publish goroutine that copied the subscriber list BEFORE
|
|
||||||
// Unsubscribe may still call sub() AFTER the handler returns.
|
|
||||||
//
|
|
||||||
// This test deterministically reproduces the scenario by:
|
|
||||||
// 1. Subscribing via handleSubscribeHTTP (which registers a sub closure on the topic)
|
|
||||||
// 2. Copying the subscriber function from the topic (simulating what topic.Publish does)
|
|
||||||
// 3. Cancelling the subscription and waiting for the handler to fully return
|
|
||||||
// 4. Calling the copied subscriber function AFTER the handler has returned
|
|
||||||
// 5. Checking that no write/flush occurred on the (now-invalid) response writer
|
|
||||||
//
|
|
||||||
// Without the wlock+closed fix, calling the subscriber after the handler returns writes to
|
|
||||||
// the closed response writer (which in production causes a nil pointer panic on Flush).
|
|
||||||
// With the fix, the subscriber sees closed=true and returns without writing.
|
|
||||||
t.Parallel()
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
|
|
||||||
rw := newClosableResponseWriter()
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil)
|
|
||||||
require.Nil(t, err)
|
|
||||||
req.RemoteAddr = "9.9.9.9:1234"
|
|
||||||
|
|
||||||
// Start the subscribe handler (blocks until context is cancelled)
|
|
||||||
handlerDone := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
s.handle(rw, req)
|
|
||||||
close(handlerDone)
|
|
||||||
}()
|
|
||||||
time.Sleep(100 * time.Millisecond) // Wait for subscription to be registered
|
|
||||||
|
|
||||||
// Grab a copy of the subscriber function from the topic, exactly as topic.Publish() does
|
|
||||||
// via subscribersCopy(). This must happen BEFORE cancel/Unsubscribe removes the subscriber.
|
|
||||||
s.mu.RLock()
|
|
||||||
tp := s.topics["mytopic"]
|
|
||||||
s.mu.RUnlock()
|
|
||||||
require.NotNil(t, tp)
|
|
||||||
subscribersCopy := tp.subscribersCopy()
|
|
||||||
require.Equal(t, 1, len(subscribersCopy))
|
|
||||||
|
|
||||||
var copiedSub subscriber
|
|
||||||
for _, sub := range subscribersCopy {
|
|
||||||
copiedSub = sub.subscriber
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel the subscription and wait for the handler to fully return.
|
|
||||||
// At this point, the deferred cleanup in handleSubscribeHTTP runs:
|
|
||||||
// - With fix: wlock.Lock() waits for in-flight sub(), sets closed=true, wlock.Unlock()
|
|
||||||
// - Without fix: nothing prevents future sub() calls from writing
|
|
||||||
cancel()
|
|
||||||
<-handlerDone
|
|
||||||
|
|
||||||
// Simulate Go's HTTP server cleaning up the response writer after the handler returns.
|
|
||||||
// In production, this is finishRequest() which nils out the bufio.Writer.
|
|
||||||
rw.Close()
|
|
||||||
|
|
||||||
// Now call the copied subscriber function, simulating a straggler Publish goroutine
|
|
||||||
// that copied the subscriber list before Unsubscribe ran. In production, this is exactly
|
|
||||||
// how the panic occurs: the goroutine spawned by topic.Publish calls sub() after the
|
|
||||||
// handler has already returned and Go has cleaned up the response writer.
|
|
||||||
v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("9.9.9.9"), nil)
|
|
||||||
msg := newDefaultMessage("mytopic", "straggler message")
|
|
||||||
_ = copiedSub(v, msg)
|
|
||||||
|
|
||||||
require.False(t, rw.wroteAfterClose.Load(),
|
|
||||||
"sub() wrote to the response writer after the handler returned; "+
|
|
||||||
"in production this causes a nil pointer panic in bufio.(*Writer).Flush()")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection(t *testing.T) {
|
|
||||||
// Test that handleError does not call WriteHeader for WebSocket errors wrapped
|
|
||||||
// with errWebSocketPostUpgrade (indicating the connection was hijacked)
|
|
||||||
s := newTestServer(t, newTestConfig(t))
|
|
||||||
|
|
||||||
// Create a WebSocket upgrade request
|
|
||||||
r, _ := http.NewRequest("GET", "/mytopic/ws", nil)
|
|
||||||
r.Header.Set("Upgrade", "websocket")
|
|
||||||
r.Header.Set("Connection", "Upgrade")
|
|
||||||
v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("1.2.3.4"), nil)
|
|
||||||
|
|
||||||
// Test post-upgrade errors wrapped with errWebSocketPostUpgrade (should NOT call WriteHeader)
|
|
||||||
postUpgradeErr := &errWebSocketPostUpgrade{errors.New("websocket: close 1000 (normal)")}
|
|
||||||
mock := newMockResponseWriter()
|
|
||||||
s.handleError(mock, r, v, postUpgradeErr)
|
|
||||||
require.False(t, mock.writeHeaderHit, "WriteHeader should not be called for post-upgrade errors")
|
|
||||||
|
|
||||||
// Test pre-upgrade errors (should call WriteHeader)
|
|
||||||
preUpgradeErrors := []error{
|
|
||||||
errHTTPBadRequestWebSocketsUpgradeHeaderMissing,
|
|
||||||
errHTTPTooManyRequestsLimitSubscriptions,
|
|
||||||
errHTTPInternalError,
|
|
||||||
}
|
|
||||||
for _, err := range preUpgradeErrors {
|
|
||||||
mock := newMockResponseWriter()
|
|
||||||
s.handleError(mock, r, v, err)
|
|
||||||
require.True(t, mock.writeHeaderHit, "WriteHeader should be called for error: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ type attachment struct {
|
|||||||
|
|
||||||
type action struct {
|
type action struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Action string `json:"action"` // "view", "broadcast", "http", or "copy"
|
Action string `json:"action"` // "view", "broadcast", or "http"
|
||||||
Label string `json:"label"` // action button label
|
Label string `json:"label"` // action button label
|
||||||
Clear bool `json:"clear"` // clear notification after successful execution
|
Clear bool `json:"clear"` // clear notification after successful execution
|
||||||
URL string `json:"url,omitempty"` // used in "view" and "http" actions
|
URL string `json:"url,omitempty"` // used in "view" and "http" actions
|
||||||
@@ -95,7 +95,6 @@ type action struct {
|
|||||||
Body string `json:"body,omitempty"` // used in "http" action
|
Body string `json:"body,omitempty"` // used in "http" action
|
||||||
Intent string `json:"intent,omitempty"` // used in "broadcast" action
|
Intent string `json:"intent,omitempty"` // used in "broadcast" action
|
||||||
Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
|
Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
|
||||||
Value string `json:"value,omitempty"` // used in "copy" action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAction() *action {
|
func newAction() *action {
|
||||||
@@ -300,7 +299,7 @@ func (t templateMode) FileName() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// templateFile represents a template file with title, message, and priority
|
// templateFile represents a template file with title and message
|
||||||
// It is used for file-based templates, e.g. grafana, influxdb, etc.
|
// It is used for file-based templates, e.g. grafana, influxdb, etc.
|
||||||
//
|
//
|
||||||
// Example YAML:
|
// Example YAML:
|
||||||
@@ -309,23 +308,15 @@ func (t templateMode) FileName() string {
|
|||||||
// message: |
|
// message: |
|
||||||
// This is a {{ .Type }} alert.
|
// This is a {{ .Type }} alert.
|
||||||
// It can be multiline.
|
// It can be multiline.
|
||||||
// priority: '{{ if eq .status "Error" }}5{{ else }}3{{ end }}'
|
|
||||||
type templateFile struct {
|
type templateFile struct {
|
||||||
Title *string `yaml:"title"`
|
Title *string `yaml:"title"`
|
||||||
Message *string `yaml:"message"`
|
Message *string `yaml:"message"`
|
||||||
Priority *string `yaml:"priority"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiHealthResponse struct {
|
type apiHealthResponse struct {
|
||||||
Healthy bool `json:"healthy"`
|
Healthy bool `json:"healthy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiVersionResponse struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Commit string `json:"commit"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiStatsResponse struct {
|
type apiStatsResponse struct {
|
||||||
Messages int64 `json:"messages"`
|
Messages int64 `json:"messages"`
|
||||||
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
|
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,7 +95,12 @@ func coalesce(v ...any) any {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - bool: True if all values are non-empty, false otherwise
|
// - bool: True if all values are non-empty, false otherwise
|
||||||
func all(v ...any) bool {
|
func all(v ...any) bool {
|
||||||
return !slices.ContainsFunc(v, empty)
|
for _, val := range v {
|
||||||
|
if empty(val) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// anyNonEmpty checks if at least one value in a list is non-empty.
|
// anyNonEmpty checks if at least one value in a list is non-empty.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -50,7 +49,12 @@ func FileExists(filename string) bool {
|
|||||||
|
|
||||||
// Contains returns true if needle is contained in haystack
|
// Contains returns true if needle is contained in haystack
|
||||||
func Contains[T comparable](haystack []T, needle T) bool {
|
func Contains[T comparable](haystack []T, needle T) bool {
|
||||||
return slices.Contains(haystack, needle)
|
for _, s := range haystack {
|
||||||
|
if s == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainsIP returns true if any one of the of prefixes contains the ip.
|
// ContainsIP returns true if any one of the of prefixes contains the ip.
|
||||||
|
|||||||
417
web/package-lock.json
generated
417
web/package-lock.json
generated
@@ -46,9 +46,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
|
||||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
|
||||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
"integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -70,21 +70,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.28.6",
|
||||||
"@babel/helper-compilation-targets": "^7.28.6",
|
"@babel/helper-compilation-targets": "^7.28.6",
|
||||||
"@babel/helper-module-transforms": "^7.28.6",
|
"@babel/helper-module-transforms": "^7.28.6",
|
||||||
"@babel/helpers": "^7.28.6",
|
"@babel/helpers": "^7.28.6",
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.28.6",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.28.6",
|
||||||
"@babel/traverse": "^7.29.0",
|
"@babel/traverse": "^7.28.6",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.28.6",
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
@@ -108,13 +108,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
|
||||||
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
|
"integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.28.6",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.28.6",
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@@ -395,12 +395,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
|
||||||
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.29.0"
|
"@babel/types": "^7.28.6"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -572,15 +572,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz",
|
||||||
"integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==",
|
"integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6",
|
"@babel/helper-plugin-utils": "^7.28.6",
|
||||||
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
||||||
"@babel/traverse": "^7.29.0"
|
"@babel/traverse": "^7.28.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -762,9 +762,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
|
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
|
||||||
"version": "7.29.0",
|
"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.29.0.tgz",
|
"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-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
|
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -977,16 +977,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
|
||||||
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
|
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-transforms": "^7.28.6",
|
"@babel/helper-module-transforms": "^7.28.3",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
"@babel/traverse": "^7.29.0"
|
"@babel/traverse": "^7.28.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1013,14 +1013,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
|
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
|
||||||
"version": "7.29.0",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
|
||||||
"integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==",
|
"integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1247,9 +1247,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-regenerator": {
|
"node_modules/@babel/plugin-transform-regenerator": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz",
|
||||||
"integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==",
|
"integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1444,13 +1444,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env": {
|
"node_modules/@babel/preset-env": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz",
|
||||||
"integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==",
|
"integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.29.0",
|
"@babel/compat-data": "^7.28.6",
|
||||||
"@babel/helper-compilation-targets": "^7.28.6",
|
"@babel/helper-compilation-targets": "^7.28.6",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6",
|
"@babel/helper-plugin-utils": "^7.28.6",
|
||||||
"@babel/helper-validator-option": "^7.27.1",
|
"@babel/helper-validator-option": "^7.27.1",
|
||||||
@@ -1464,7 +1464,7 @@
|
|||||||
"@babel/plugin-syntax-import-attributes": "^7.28.6",
|
"@babel/plugin-syntax-import-attributes": "^7.28.6",
|
||||||
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||||
"@babel/plugin-transform-async-generator-functions": "^7.29.0",
|
"@babel/plugin-transform-async-generator-functions": "^7.28.6",
|
||||||
"@babel/plugin-transform-async-to-generator": "^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-scoped-functions": "^7.27.1",
|
||||||
"@babel/plugin-transform-block-scoping": "^7.28.6",
|
"@babel/plugin-transform-block-scoping": "^7.28.6",
|
||||||
@@ -1475,7 +1475,7 @@
|
|||||||
"@babel/plugin-transform-destructuring": "^7.28.5",
|
"@babel/plugin-transform-destructuring": "^7.28.5",
|
||||||
"@babel/plugin-transform-dotall-regex": "^7.28.6",
|
"@babel/plugin-transform-dotall-regex": "^7.28.6",
|
||||||
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
|
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
|
||||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0",
|
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6",
|
||||||
"@babel/plugin-transform-dynamic-import": "^7.27.1",
|
"@babel/plugin-transform-dynamic-import": "^7.27.1",
|
||||||
"@babel/plugin-transform-explicit-resource-management": "^7.28.6",
|
"@babel/plugin-transform-explicit-resource-management": "^7.28.6",
|
||||||
"@babel/plugin-transform-exponentiation-operator": "^7.28.6",
|
"@babel/plugin-transform-exponentiation-operator": "^7.28.6",
|
||||||
@@ -1488,9 +1488,9 @@
|
|||||||
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
|
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
|
||||||
"@babel/plugin-transform-modules-amd": "^7.27.1",
|
"@babel/plugin-transform-modules-amd": "^7.27.1",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||||
"@babel/plugin-transform-modules-systemjs": "^7.29.0",
|
"@babel/plugin-transform-modules-systemjs": "^7.28.5",
|
||||||
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
||||||
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
|
"@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
|
||||||
"@babel/plugin-transform-new-target": "^7.27.1",
|
"@babel/plugin-transform-new-target": "^7.27.1",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
|
||||||
"@babel/plugin-transform-numeric-separator": "^7.28.6",
|
"@babel/plugin-transform-numeric-separator": "^7.28.6",
|
||||||
@@ -1502,7 +1502,7 @@
|
|||||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||||
"@babel/plugin-transform-private-property-in-object": "^7.28.6",
|
"@babel/plugin-transform-private-property-in-object": "^7.28.6",
|
||||||
"@babel/plugin-transform-property-literals": "^7.27.1",
|
"@babel/plugin-transform-property-literals": "^7.27.1",
|
||||||
"@babel/plugin-transform-regenerator": "^7.29.0",
|
"@babel/plugin-transform-regenerator": "^7.28.6",
|
||||||
"@babel/plugin-transform-regexp-modifiers": "^7.28.6",
|
"@babel/plugin-transform-regexp-modifiers": "^7.28.6",
|
||||||
"@babel/plugin-transform-reserved-words": "^7.27.1",
|
"@babel/plugin-transform-reserved-words": "^7.27.1",
|
||||||
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
||||||
@@ -1515,10 +1515,10 @@
|
|||||||
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
||||||
"@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
|
"@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
|
||||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||||
"babel-plugin-polyfill-corejs2": "^0.4.15",
|
"babel-plugin-polyfill-corejs2": "^0.4.14",
|
||||||
"babel-plugin-polyfill-corejs3": "^0.14.0",
|
"babel-plugin-polyfill-corejs3": "^0.13.0",
|
||||||
"babel-plugin-polyfill-regenerator": "^0.6.6",
|
"babel-plugin-polyfill-regenerator": "^0.6.5",
|
||||||
"core-js-compat": "^3.48.0",
|
"core-js-compat": "^3.43.0",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1567,17 +1567,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
|
||||||
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
"integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.28.6",
|
||||||
"@babel/helper-globals": "^7.28.0",
|
"@babel/helper-globals": "^7.28.0",
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.28.6",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.28.6",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.28.6",
|
||||||
"debug": "^4.3.1"
|
"debug": "^4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1585,9 +1585,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
|
||||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
@@ -2309,9 +2309,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/brace-expansion": {
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2798,9 +2798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
|
||||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
"integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2812,9 +2812,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz",
|
||||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
"integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2826,9 +2826,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz",
|
||||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
"integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2840,9 +2840,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz",
|
||||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
"integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2854,9 +2854,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz",
|
||||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
"integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2868,9 +2868,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz",
|
||||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
"integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2882,9 +2882,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz",
|
||||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
"integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2896,9 +2896,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz",
|
||||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
"integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2910,9 +2910,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
"integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2924,9 +2924,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz",
|
||||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
"integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2938,9 +2938,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
"integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -2952,9 +2952,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz",
|
||||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
"integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -2966,9 +2966,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
"integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2980,9 +2980,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz",
|
||||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
"integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2994,9 +2994,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
"integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -3008,9 +3008,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz",
|
||||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
"integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -3022,9 +3022,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
"integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -3036,9 +3036,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
"integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3050,9 +3050,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz",
|
||||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
"integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3064,9 +3064,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz",
|
||||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
"integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3078,9 +3078,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz",
|
||||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
"integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3092,9 +3092,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz",
|
||||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
"integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3106,9 +3106,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz",
|
||||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
"integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -3120,9 +3120,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz",
|
||||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
"integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3134,9 +3134,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
|
||||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
"integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3248,9 +3248,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.11",
|
"version": "19.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||||
"integrity": "sha512-tORuanb01iEzWvMGVGv2ZDhYZVeRMrw453DCSAIn/5yvcSVnMoUMTyf33nQJLahYEnv9xqrTNbgz4qY5EfSh0g==",
|
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3658,14 +3658,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.14.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
|
||||||
"integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==",
|
"integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.6",
|
"@babel/helper-define-polyfill-provider": "^0.6.5",
|
||||||
"core-js-compat": "^3.48.0"
|
"core-js-compat": "^3.43.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
@@ -3702,9 +3702,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.19",
|
"version": "2.9.18",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
||||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3823,9 +3823,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001767",
|
"version": "1.0.30001766",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||||
"integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==",
|
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4267,9 +4267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.286",
|
"version": "1.5.278",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz",
|
||||||
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
|
"integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -5328,7 +5328,7 @@
|
|||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7091,9 +7091,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
"version": "11.2.5",
|
"version": "11.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -7278,24 +7278,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.4"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-i18next": {
|
"node_modules/react-i18next": {
|
||||||
@@ -7333,9 +7333,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
||||||
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
"integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
@@ -7628,9 +7628,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.57.1",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
|
||||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
"integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7644,31 +7644,31 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
"@rollup/rollup-android-arm-eabi": "4.56.0",
|
||||||
"@rollup/rollup-android-arm64": "4.57.1",
|
"@rollup/rollup-android-arm64": "4.56.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
"@rollup/rollup-darwin-arm64": "4.56.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
"@rollup/rollup-darwin-x64": "4.56.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
"@rollup/rollup-freebsd-arm64": "4.56.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
"@rollup/rollup-freebsd-x64": "4.56.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.56.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.56.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
"@rollup/rollup-linux-arm64-musl": "4.56.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
"@rollup/rollup-linux-loong64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
"@rollup/rollup-linux-loong64-musl": "4.56.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
"@rollup/rollup-linux-ppc64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
"@rollup/rollup-linux-ppc64-musl": "4.56.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
"@rollup/rollup-linux-riscv64-musl": "4.56.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
"@rollup/rollup-linux-x64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
"@rollup/rollup-linux-x64-musl": "4.56.0",
|
||||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
"@rollup/rollup-openbsd-x64": "4.56.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
"@rollup/rollup-openharmony-arm64": "4.56.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.56.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.56.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
"@rollup/rollup-win32-x64-gnu": "4.56.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
"@rollup/rollup-win32-x64-msvc": "4.56.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -9331,7 +9331,6 @@
|
|||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9360,13 +9359,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/workbox-build/node_modules/minimatch": {
|
"node_modules/workbox-build/node_modules/minimatch": {
|
||||||
"version": "10.1.2",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/brace-expansion": "^5.0.1"
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
"publish_dialog_attachment_limits_quota_reached": "надвишава квотата, остават {{remainingBytes}}",
|
"publish_dialog_attachment_limits_quota_reached": "надвишава квотата, остават {{remainingBytes}}",
|
||||||
"publish_dialog_priority_high": "Висок приоритет",
|
"publish_dialog_priority_high": "Висок приоритет",
|
||||||
"publish_dialog_priority_default": "Подразбиран приоритет",
|
"publish_dialog_priority_default": "Подразбиран приоритет",
|
||||||
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за дисково пространство",
|
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
||||||
"publish_dialog_tags_label": "Етикети",
|
"publish_dialog_tags_label": "Етикети",
|
||||||
"publish_dialog_email_label": "Адрес на електронна поща",
|
"publish_dialog_email_label": "Адрес на електронна поща",
|
||||||
"publish_dialog_priority_max": "Най-висок приоритет",
|
"publish_dialog_priority_max": "Най-висок приоритет",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
"publish_dialog_tags_placeholder": "Komma-getrennte Liste von Tags, z.B. Warnung, srv1-Backup",
|
"publish_dialog_tags_placeholder": "Komma-getrennte Liste von Tags, z.B. Warnung, srv1-Backup",
|
||||||
"publish_dialog_priority_label": "Priorität",
|
"publish_dialog_priority_label": "Priorität",
|
||||||
"publish_dialog_filename_label": "Dateiname",
|
"publish_dialog_filename_label": "Dateiname",
|
||||||
"publish_dialog_title_placeholder": "Benachrichtigungstitel, z. B. Speicherplatzwarnung",
|
"publish_dialog_title_placeholder": "Benachrichtigungs-Titel, z.B. CPU-Last-Warnung",
|
||||||
"publish_dialog_tags_label": "Tags",
|
"publish_dialog_tags_label": "Tags",
|
||||||
"publish_dialog_click_label": "Klick-URL",
|
"publish_dialog_click_label": "Klick-URL",
|
||||||
"publish_dialog_click_placeholder": "URL die geöffnet werden soll, wenn die Benachrichtigung angeklickt wird",
|
"publish_dialog_click_placeholder": "URL die geöffnet werden soll, wenn die Benachrichtigung angeklickt wird",
|
||||||
|
|||||||
@@ -357,8 +357,6 @@
|
|||||||
"prefs_users_dialog_title_add": "Add user",
|
"prefs_users_dialog_title_add": "Add user",
|
||||||
"prefs_users_dialog_title_edit": "Edit user",
|
"prefs_users_dialog_title_edit": "Edit user",
|
||||||
"prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh",
|
"prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh",
|
||||||
"prefs_users_dialog_base_url_invalid": "Invalid URL format. Must start with http:// or https://",
|
|
||||||
"prefs_users_dialog_base_url_exists": "A user for this service URL already exists",
|
|
||||||
"prefs_users_dialog_username_label": "Username, e.g. phil",
|
"prefs_users_dialog_username_label": "Username, e.g. phil",
|
||||||
"prefs_users_dialog_password_label": "Password",
|
"prefs_users_dialog_password_label": "Password",
|
||||||
"prefs_appearance_title": "Appearance",
|
"prefs_appearance_title": "Appearance",
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"common_cancel": "ביטול",
|
|
||||||
"common_save": "שמירה",
|
|
||||||
"common_add": "הוספה",
|
|
||||||
"common_back": "חזרה",
|
|
||||||
"common_copy_to_clipboard": "העתקה ללוח הגזירים",
|
|
||||||
"signup_title": "יצירת חשבון ntfy",
|
|
||||||
"signup_form_username": "שם משתמש",
|
|
||||||
"signup_form_password": "סיסמה",
|
|
||||||
"signup_form_confirm_password": "אישור סיסמה",
|
|
||||||
"signup_form_button_submit": "הרשמה",
|
|
||||||
"signup_form_toggle_password_visibility": "הצגת/הסתרת סיסמה",
|
|
||||||
"signup_already_have_account": "כבר יש לך חשבון? אפשר להיכנס איתו!",
|
|
||||||
"signup_disabled": "הרשמה כבויה",
|
|
||||||
"signup_error_username_taken": "שם המשתמש {{username}} כבר תפוס",
|
|
||||||
"signup_error_creation_limit_reached": "הגעת למגבלת יצירת חשבונות",
|
|
||||||
"login_title": "כניסה לחשבון ה־ntfy שלך",
|
|
||||||
"login_form_button_submit": "כניסה",
|
|
||||||
"login_link_signup": "הרשמה",
|
|
||||||
"login_disabled": "הכניסה מושבתת",
|
|
||||||
"action_bar_show_menu": "הצגת תפריט",
|
|
||||||
"action_bar_logo_alt": "הלוגו של ntfy",
|
|
||||||
"action_bar_settings": "הגדרות",
|
|
||||||
"action_bar_account": "חשבון",
|
|
||||||
"action_bar_change_display_name": "החלפת שם תצוגה",
|
|
||||||
"action_bar_reservation_add": "שימור נושא",
|
|
||||||
"action_bar_reservation_edit": "החלפת מצב שימור",
|
|
||||||
"action_bar_reservation_delete": "הסרת שימור",
|
|
||||||
"action_bar_reservation_limit_reached": "הגעת למגבלה",
|
|
||||||
"action_bar_send_test_notification": "שליחת התראת ניסוי",
|
|
||||||
"action_bar_clear_notifications": "לפנות את כל ההתראות",
|
|
||||||
"action_bar_mute_notifications": "השתקת התראות",
|
|
||||||
"action_bar_unmute_notifications": "ביטול השתקת התראות",
|
|
||||||
"action_bar_unsubscribe": "ביטול מינוי",
|
|
||||||
"notifications_list_item": "התראה",
|
|
||||||
"notifications_mark_read": "סימון כנקראה",
|
|
||||||
"notifications_delete": "מחיקה",
|
|
||||||
"notifications_copied_to_clipboard": "הועתקה ללוח הגזירים",
|
|
||||||
"notifications_tags": "תגיות",
|
|
||||||
"notifications_priority_x": "עדיפות {{priority}}",
|
|
||||||
"notifications_new_indicator": "התראה חדשה",
|
|
||||||
"notifications_attachment_copy_url_button": "העתקת כתובת",
|
|
||||||
"notifications_attachment_open_title": "מעבר אל {{url}}",
|
|
||||||
"notifications_attachment_open_button": "פתיחת צרופה",
|
|
||||||
"notifications_attachment_link_expires": "תוקף הקישור פג ב־{{date}}",
|
|
||||||
"notifications_attachment_link_expired": "תוקף קישור ההורדה פג",
|
|
||||||
"notifications_actions_failed_notification": "פעולה לא מוצלחת",
|
|
||||||
"notifications_none_for_topic_title": "לא קיבלת התראות בנושא הזה עדיין.",
|
|
||||||
"notifications_none_for_topic_description": "כדי לשלוח התראות לנושא הזה, צריך לשלוח PUT או POST לכתובת הנושא הזה.",
|
|
||||||
"notifications_none_for_any_title": "לא קיבלת התראות כלל.",
|
|
||||||
"notifications_no_subscriptions_title": "נראה שלא נרשמת למינויים עדיין."
|
|
||||||
}
|
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
"publish_dialog_topic_label": "Название темы",
|
"publish_dialog_topic_label": "Название темы",
|
||||||
"publish_dialog_topic_placeholder": "Название темы, например phil_alerts",
|
"publish_dialog_topic_placeholder": "Название темы, например phil_alerts",
|
||||||
"publish_dialog_title_label": "Заголовок",
|
"publish_dialog_title_label": "Заголовок",
|
||||||
"publish_dialog_title_placeholder": "Заголовок уведомления, например, Предупреждение о занятости диска",
|
"publish_dialog_title_placeholder": "Заголовок уведомления, например Disk space alert",
|
||||||
"publish_dialog_message_label": "Сообщение",
|
"publish_dialog_message_label": "Сообщение",
|
||||||
"publish_dialog_message_placeholder": "Введите сообщение здесь",
|
"publish_dialog_message_placeholder": "Введите сообщение здесь",
|
||||||
"publish_dialog_tags_label": "Тэги",
|
"publish_dialog_tags_label": "Тэги",
|
||||||
"publish_dialog_tags_placeholder": "Ярлыки, разделенные запятыми, например: warning, srv1-backup",
|
"publish_dialog_tags_placeholder": "Список тэгов, разделённый запятой, например: warning, srv1-backup",
|
||||||
"publish_dialog_priority_label": "Приоритет",
|
"publish_dialog_priority_label": "Приоритет",
|
||||||
"publish_dialog_click_label": "Ссылка при открытии",
|
"publish_dialog_click_label": "Ссылка при открытии",
|
||||||
"publish_dialog_click_placeholder": "URL-адрес, который откроется при нажатии на уведомление",
|
"publish_dialog_click_placeholder": "URL-адрес, который откроется при нажатии на уведомление",
|
||||||
@@ -242,8 +242,8 @@
|
|||||||
"action_bar_reservation_delete": "Удалить резервирование",
|
"action_bar_reservation_delete": "Удалить резервирование",
|
||||||
"action_bar_profile_title": "Профиль",
|
"action_bar_profile_title": "Профиль",
|
||||||
"action_bar_profile_settings": "Настройки",
|
"action_bar_profile_settings": "Настройки",
|
||||||
"action_bar_profile_logout": "Выйти",
|
"action_bar_profile_logout": "Выход",
|
||||||
"action_bar_sign_in": "Войти",
|
"action_bar_sign_in": "Вход",
|
||||||
"action_bar_sign_up": "Регистрация",
|
"action_bar_sign_up": "Регистрация",
|
||||||
"action_bar_change_display_name": "Изменить псевдоним",
|
"action_bar_change_display_name": "Изменить псевдоним",
|
||||||
"message_bar_publish": "Опубликовать сообщение",
|
"message_bar_publish": "Опубликовать сообщение",
|
||||||
@@ -395,7 +395,7 @@
|
|||||||
"prefs_notifications_web_push_title": "Фоновые уведомления",
|
"prefs_notifications_web_push_title": "Фоновые уведомления",
|
||||||
"prefs_notifications_web_push_enabled_description": "Уведомления приходят даже когда веб-приложение не запущено (через Web Push)",
|
"prefs_notifications_web_push_enabled_description": "Уведомления приходят даже когда веб-приложение не запущено (через Web Push)",
|
||||||
"prefs_notifications_web_push_disabled_description": "Уведомления приходят, когда веб-приложение запущено (через WebSocket)",
|
"prefs_notifications_web_push_disabled_description": "Уведомления приходят, когда веб-приложение запущено (через WebSocket)",
|
||||||
"prefs_appearance_theme_title": "Тема оформления",
|
"prefs_appearance_theme_title": "Тема",
|
||||||
"prefs_notifications_web_push_enabled": "Включено для {{server}}",
|
"prefs_notifications_web_push_enabled": "Включено для {{server}}",
|
||||||
"prefs_notifications_web_push_disabled": "Выключено",
|
"prefs_notifications_web_push_disabled": "Выключено",
|
||||||
"notifications_actions_failed_notification": "Неудачное действие",
|
"notifications_actions_failed_notification": "Неудачное действие",
|
||||||
@@ -403,7 +403,5 @@
|
|||||||
"subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто",
|
"subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто",
|
||||||
"prefs_appearance_theme_system": "Как в системе (по умолчанию)",
|
"prefs_appearance_theme_system": "Как в системе (по умолчанию)",
|
||||||
"prefs_appearance_theme_dark": "Тёмная",
|
"prefs_appearance_theme_dark": "Тёмная",
|
||||||
"prefs_appearance_theme_light": "Светлая",
|
"prefs_appearance_theme_light": "Светлая"
|
||||||
"account_basics_cannot_edit_or_delete_provisioned_user": "Пользователя, созданного автоматически, нельзя изменить или удалить",
|
|
||||||
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Автоматически созданный токен нельзя изменить или удалить"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
|
|||||||
import { NetworkFirst } from "workbox-strategies";
|
import { NetworkFirst } from "workbox-strategies";
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
import { dbAsync } from "../src/app/db";
|
import { dbAsync } from "../src/app/db";
|
||||||
import { ACTION_HTTP, ACTION_VIEW } from "../src/app/actions";
|
|
||||||
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
||||||
import initI18n from "../src/app/i18n";
|
import initI18n from "../src/app/i18n";
|
||||||
import {
|
import {
|
||||||
@@ -238,25 +237,9 @@ const handleClick = async (event) => {
|
|||||||
if (event.action) {
|
if (event.action) {
|
||||||
const action = event.notification.data.message.actions.find(({ label }) => event.action === label);
|
const action = event.notification.data.message.actions.find(({ label }) => event.action === label);
|
||||||
|
|
||||||
// Helper to clear notification and mark as read
|
if (action.action === "view") {
|
||||||
const clearNotification = async () => {
|
|
||||||
event.notification.close();
|
|
||||||
const { subscriptionId, message: msg } = event.notification.data;
|
|
||||||
const seqId = msg.sequence_id || msg.id;
|
|
||||||
if (subscriptionId && seqId) {
|
|
||||||
const db = await dbAsync();
|
|
||||||
await db.notifications.where({ subscriptionId, sequenceId: seqId }).modify({ new: 0 });
|
|
||||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
|
||||||
self.navigator.setAppBadge?.(badgeCount);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (action.action === ACTION_VIEW) {
|
|
||||||
self.clients.openWindow(action.url);
|
self.clients.openWindow(action.url);
|
||||||
if (action.clear) {
|
} else if (action.action === "http") {
|
||||||
await clearNotification();
|
|
||||||
}
|
|
||||||
} else if (action.action === ACTION_HTTP) {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(action.url, {
|
const response = await fetch(action.url, {
|
||||||
method: action.method ?? "POST",
|
method: action.method ?? "POST",
|
||||||
@@ -267,11 +250,6 @@ const handleClick = async (event) => {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only clear on success
|
|
||||||
if (action.clear) {
|
|
||||||
await clearNotification();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[ServiceWorker] Error performing http action", e);
|
console.error("[ServiceWorker] Error performing http action", e);
|
||||||
self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
|
self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
|
||||||
@@ -281,6 +259,10 @@ const handleClick = async (event) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.clear) {
|
||||||
|
event.notification.close();
|
||||||
|
}
|
||||||
} else if (message.click) {
|
} else if (message.click) {
|
||||||
self.clients.openWindow(message.click);
|
self.clients.openWindow(message.click);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
// Action types for ntfy messages
|
|
||||||
// These correspond to the server action types in server/actions.go
|
|
||||||
|
|
||||||
export const ACTION_VIEW = "view";
|
|
||||||
export const ACTION_BROADCAST = "broadcast";
|
|
||||||
export const ACTION_HTTP = "http";
|
|
||||||
export const ACTION_COPY = "copy";
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// and cannot be used in the service worker
|
// and cannot be used in the service worker
|
||||||
|
|
||||||
import emojisMapped from "./emojisMapped";
|
import emojisMapped from "./emojisMapped";
|
||||||
import { ACTION_HTTP, ACTION_VIEW } from "./actions";
|
|
||||||
|
|
||||||
const toEmojis = (tags) => {
|
const toEmojis = (tags) => {
|
||||||
if (!tags) return [];
|
if (!tags) return [];
|
||||||
@@ -61,7 +60,6 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr
|
|||||||
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
||||||
const sequenceId = message.sequence_id || message.id;
|
const sequenceId = message.sequence_id || message.id;
|
||||||
const tag = notificationTag(baseUrl, topic, sequenceId);
|
const tag = notificationTag(baseUrl, topic, sequenceId);
|
||||||
const subscriptionId = `${baseUrl}/${topic}`;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
||||||
return [
|
return [
|
||||||
@@ -77,12 +75,11 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr
|
|||||||
silent: false,
|
silent: false,
|
||||||
// This is used by the notification onclick event
|
// This is used by the notification onclick event
|
||||||
data: {
|
data: {
|
||||||
subscriptionId,
|
|
||||||
message,
|
message,
|
||||||
topicRoute,
|
topicRoute,
|
||||||
},
|
},
|
||||||
actions: message.actions
|
actions: message.actions
|
||||||
?.filter(({ action }) => action === ACTION_VIEW || action === ACTION_HTTP)
|
?.filter(({ action }) => action === "view" || action === "http")
|
||||||
.map(({ label }) => ({
|
.map(({ label }) => ({
|
||||||
action: label,
|
action: label,
|
||||||
title: label,
|
title: label,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import pop from "../sounds/pop.mp3";
|
|||||||
import popSwoosh from "../sounds/pop-swoosh.mp3";
|
import popSwoosh from "../sounds/pop-swoosh.mp3";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import emojisMapped from "./emojisMapped";
|
import emojisMapped from "./emojisMapped";
|
||||||
import { THEME } from "./Prefs";
|
|
||||||
|
|
||||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
@@ -275,84 +274,6 @@ export const urlB64ToUint8Array = (base64String) => {
|
|||||||
return outputArray;
|
return outputArray;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const darkModeEnabled = (prefersDarkMode, themePreference) => {
|
|
||||||
switch (themePreference) {
|
|
||||||
case THEME.DARK:
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case THEME.LIGHT:
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case THEME.SYSTEM:
|
|
||||||
default:
|
|
||||||
return prefersDarkMode;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Canvas-based favicon with a red notification dot when there are unread messages
|
|
||||||
let faviconCanvas;
|
|
||||||
let faviconOriginalIcon;
|
|
||||||
|
|
||||||
const loadFaviconIcon = () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
if (faviconOriginalIcon) {
|
|
||||||
resolve(faviconOriginalIcon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
faviconOriginalIcon = img;
|
|
||||||
resolve(img);
|
|
||||||
};
|
|
||||||
img.onerror = () => resolve(null);
|
|
||||||
// Use PNG instead of ICO — .ico files can't be reliably drawn to canvas in all browsers
|
|
||||||
img.src = "/static/images/ntfy.png";
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateFavicon = async (count) => {
|
|
||||||
const size = 32;
|
|
||||||
const img = await loadFaviconIcon();
|
|
||||||
if (!img) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!faviconCanvas) {
|
|
||||||
faviconCanvas = document.createElement("canvas");
|
|
||||||
faviconCanvas.width = size;
|
|
||||||
faviconCanvas.height = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = faviconCanvas.getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, size, size);
|
|
||||||
ctx.drawImage(img, 0, 0, size, size);
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
const dotRadius = 5;
|
|
||||||
const borderWidth = 2;
|
|
||||||
const dotX = size - dotRadius - borderWidth + 1;
|
|
||||||
const dotY = size - dotRadius - borderWidth + 1;
|
|
||||||
|
|
||||||
// Transparent border: erase a ring around the dot so the icon doesn't bleed into it
|
|
||||||
ctx.save();
|
|
||||||
ctx.globalCompositeOperation = "destination-out";
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(dotX, dotY, dotRadius + borderWidth, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
// Red dot
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(dotX, dotY, dotRadius, 0, 2 * Math.PI);
|
|
||||||
ctx.fillStyle = "#dc3545";
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.querySelector("link[rel='icon']");
|
|
||||||
if (link) {
|
|
||||||
link.href = faviconCanvas.toDataURL("image/png");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const copyToClipboard = (text) => {
|
export const copyToClipboard = (text) => {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
return navigator.clipboard.writeText(text);
|
return navigator.clipboard.writeText(text);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ActionBar from "./ActionBar";
|
|||||||
import Preferences from "./Preferences";
|
import Preferences from "./Preferences";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import userManager from "../app/UserManager";
|
import userManager from "../app/UserManager";
|
||||||
import { expandUrl, getKebabCaseLangStr, darkModeEnabled, updateFavicon } from "../app/utils";
|
import { expandUrl, getKebabCaseLangStr } from "../app/utils";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
|
import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
|
||||||
@@ -21,7 +21,7 @@ import Login from "./Login";
|
|||||||
import Signup from "./Signup";
|
import Signup from "./Signup";
|
||||||
import Account from "./Account";
|
import Account from "./Account";
|
||||||
import initI18n from "../app/i18n"; // Translations!
|
import initI18n from "../app/i18n"; // Translations!
|
||||||
import prefs from "../app/Prefs";
|
import prefs, { THEME } from "../app/Prefs";
|
||||||
import RTLCacheProvider from "./RTLCacheProvider";
|
import RTLCacheProvider from "./RTLCacheProvider";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
|
|
||||||
@@ -29,6 +29,20 @@ initI18n();
|
|||||||
|
|
||||||
export const AccountContext = createContext(null);
|
export const AccountContext = createContext(null);
|
||||||
|
|
||||||
|
const darkModeEnabled = (prefersDarkMode, themePreference) => {
|
||||||
|
switch (themePreference) {
|
||||||
|
case THEME.DARK:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case THEME.LIGHT:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case THEME.SYSTEM:
|
||||||
|
default:
|
||||||
|
return prefersDarkMode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const languageDir = i18n.dir();
|
const languageDir = i18n.dir();
|
||||||
@@ -83,7 +97,6 @@ const App = () => {
|
|||||||
const updateTitle = (newNotificationsCount) => {
|
const updateTitle = (newNotificationsCount) => {
|
||||||
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
|
||||||
window.navigator.setAppBadge?.(newNotificationsCount);
|
window.navigator.setAppBadge?.(newNotificationsCount);
|
||||||
updateFavicon(newNotificationsCount);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
|
|||||||
@@ -33,14 +33,12 @@ import {
|
|||||||
maybeActionErrors,
|
maybeActionErrors,
|
||||||
openUrl,
|
openUrl,
|
||||||
shortUrl,
|
shortUrl,
|
||||||
topicUrl,
|
topicShortUrl,
|
||||||
unmatchedTags,
|
unmatchedTags,
|
||||||
} from "../app/utils";
|
} from "../app/utils";
|
||||||
import { ACTION_BROADCAST, ACTION_COPY, ACTION_HTTP, ACTION_VIEW } from "../app/actions";
|
|
||||||
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
||||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import notifier from "../app/Notifier";
|
|
||||||
import priority1 from "../img/priority-1.svg";
|
import priority1 from "../img/priority-1.svg";
|
||||||
import priority2 from "../img/priority-2.svg";
|
import priority2 from "../img/priority-2.svg";
|
||||||
import priority4 from "../img/priority-4.svg";
|
import priority4 from "../img/priority-4.svg";
|
||||||
@@ -190,7 +188,7 @@ const MarkdownContainer = styled("div")`
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: 1.5;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote,
|
blockquote,
|
||||||
@@ -305,7 +303,7 @@ const NotificationItem = (props) => {
|
|||||||
{formatTitle(notification)}
|
{formatTitle(notification)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Typography variant="body1" sx={{ whiteSpace: "pre-line", overflowX: "auto" }}>
|
<Typography variant="body1" sx={{ whiteSpace: "pre-line" }}>
|
||||||
<NotificationBody notification={notification} />
|
<NotificationBody notification={notification} />
|
||||||
{maybeActionErrors(notification)}
|
{maybeActionErrors(notification)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -346,7 +344,7 @@ const NotificationItem = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{hasUserActions && <UserActions notification={notification} onShowSnack={props.onShowSnack} />}
|
{hasUserActions && <UserActions notification={notification} />}
|
||||||
</CardActions>
|
</CardActions>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -488,7 +486,7 @@ const Image = (props) => {
|
|||||||
const UserActions = (props) => (
|
const UserActions = (props) => (
|
||||||
<>
|
<>
|
||||||
{props.notification.actions.map((action) => (
|
{props.notification.actions.map((action) => (
|
||||||
<UserAction key={action.id} notification={props.notification} action={action} onShowSnack={props.onShowSnack} />
|
<UserAction key={action.id} notification={props.notification} action={action} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -510,15 +508,6 @@ const updateActionStatus = (notification, action, progress, error) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearNotification = async (notification) => {
|
|
||||||
console.log(`[Notifications] Clearing notification ${notification.id}`);
|
|
||||||
const subscription = await subscriptionManager.get(notification.subscriptionId);
|
|
||||||
if (subscription) {
|
|
||||||
await notifier.cancel(subscription, notification);
|
|
||||||
}
|
|
||||||
await subscriptionManager.markNotificationRead(notification.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const performHttpAction = async (notification, action) => {
|
const performHttpAction = async (notification, action) => {
|
||||||
console.log(`[Notifications] Performing HTTP user action`, action);
|
console.log(`[Notifications] Performing HTTP user action`, action);
|
||||||
try {
|
try {
|
||||||
@@ -534,9 +523,6 @@ const performHttpAction = async (notification, action) => {
|
|||||||
const success = response.status >= 200 && response.status <= 299;
|
const success = response.status >= 200 && response.status <= 299;
|
||||||
if (success) {
|
if (success) {
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
||||||
if (action.clear) {
|
|
||||||
await clearNotification(notification);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
|
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -550,7 +536,7 @@ const UserAction = (props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { notification } = props;
|
const { notification } = props;
|
||||||
const { action } = props;
|
const { action } = props;
|
||||||
if (action.action === ACTION_BROADCAST) {
|
if (action.action === "broadcast") {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t("notifications_actions_not_supported")}>
|
<Tooltip title={t("notifications_actions_not_supported")}>
|
||||||
<span>
|
<span>
|
||||||
@@ -561,17 +547,11 @@ const UserAction = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (action.action === ACTION_VIEW) {
|
if (action.action === "view") {
|
||||||
const handleClick = () => {
|
|
||||||
openUrl(action.url);
|
|
||||||
if (action.clear) {
|
|
||||||
clearNotification(notification);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleClick}
|
onClick={() => openUrl(action.url)}
|
||||||
aria-label={t("notifications_actions_open_url_title", {
|
aria-label={t("notifications_actions_open_url_title", {
|
||||||
url: action.url,
|
url: action.url,
|
||||||
})}
|
})}
|
||||||
@@ -581,7 +561,7 @@ const UserAction = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (action.action === ACTION_HTTP) {
|
if (action.action === "http") {
|
||||||
const method = action.method ?? "POST";
|
const method = action.method ?? "POST";
|
||||||
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
|
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
|
||||||
return (
|
return (
|
||||||
@@ -603,28 +583,12 @@ const UserAction = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (action.action === ACTION_COPY) {
|
|
||||||
const handleClick = async () => {
|
|
||||||
await copyToClipboard(action.value);
|
|
||||||
props.onShowSnack();
|
|
||||||
if (action.clear) {
|
|
||||||
await clearNotification(notification);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Tooltip title={t("common_copy_to_clipboard")}>
|
|
||||||
<Button onClick={handleClick} aria-label={t("common_copy_to_clipboard")}>
|
|
||||||
{action.label}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null; // Others
|
return null; // Others
|
||||||
};
|
};
|
||||||
|
|
||||||
const NoNotifications = (props) => {
|
const NoNotifications = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const topicUrlResolved = topicUrl(props.subscription.baseUrl, props.subscription.topic);
|
const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
@@ -637,7 +601,7 @@ const NoNotifications = (props) => {
|
|||||||
{t("notifications_example")}:<br />
|
{t("notifications_example")}:<br />
|
||||||
<tt>
|
<tt>
|
||||||
{'$ curl -d "Hi" '}
|
{'$ curl -d "Hi" '}
|
||||||
{topicUrlResolved}
|
{topicShortUrlResolved}
|
||||||
</tt>
|
</tt>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
@@ -650,7 +614,7 @@ const NoNotifications = (props) => {
|
|||||||
const NoNotificationsWithoutSubscription = (props) => {
|
const NoNotificationsWithoutSubscription = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const subscription = props.subscriptions[0];
|
const subscription = props.subscriptions[0];
|
||||||
const topicUrlResolved = topicUrl(subscription.baseUrl, subscription.topic);
|
const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
@@ -663,7 +627,7 @@ const NoNotificationsWithoutSubscription = (props) => {
|
|||||||
{t("notifications_example")}:<br />
|
{t("notifications_example")}:<br />
|
||||||
<tt>
|
<tt>
|
||||||
{'$ curl -d "Hi" '}
|
{'$ curl -d "Hi" '}
|
||||||
{topicUrlResolved}
|
{topicShortUrlResolved}
|
||||||
</tt>
|
</tt>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
|||||||
@@ -429,23 +429,13 @@ const UserDialog = (props) => {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
const editMode = props.user !== null;
|
const editMode = props.user !== null;
|
||||||
const baseUrlValid = baseUrl.length === 0 || validUrl(baseUrl);
|
|
||||||
const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl);
|
|
||||||
const baseUrlError = baseUrl.length > 0 && (!baseUrlValid || baseUrlExists);
|
|
||||||
const addButtonEnabled = (() => {
|
const addButtonEnabled = (() => {
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
return username.length > 0 && password.length > 0;
|
return username.length > 0 && password.length > 0;
|
||||||
}
|
}
|
||||||
return validUrl(baseUrl) && !baseUrlExists && username.length > 0 && password.length > 0;
|
const baseUrlValid = validUrl(baseUrl);
|
||||||
})();
|
const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl);
|
||||||
const baseUrlHelperText = (() => {
|
return baseUrlValid && !baseUrlExists && username.length > 0 && password.length > 0;
|
||||||
if (baseUrl.length > 0 && !baseUrlValid) {
|
|
||||||
return t("prefs_users_dialog_base_url_invalid");
|
|
||||||
}
|
|
||||||
if (baseUrlExists) {
|
|
||||||
return t("prefs_users_dialog_base_url_exists");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
})();
|
})();
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
props.onSubmit({
|
props.onSubmit({
|
||||||
@@ -477,8 +467,6 @@ const UserDialog = (props) => {
|
|||||||
type="url"
|
type="url"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
error={baseUrlError}
|
|
||||||
helperText={baseUrlHelperText}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
Reference in New Issue
Block a user