Compare commits
360 commits
img-attach
...
main
Author | SHA1 | Date | |
---|---|---|---|
a06577faac | |||
668c472ee6 | |||
|
abe7275f0c | ||
|
d4af2be7a0 | ||
|
8dd4c3e3c0 | ||
|
af25f164ed | ||
|
64ede0f11c | ||
|
d3565c9b87 | ||
|
c332c132fa | ||
|
b3534aecda | ||
|
8e04912201 | ||
|
909f9b3d24 | ||
|
cad38573d7 | ||
|
a3663e43e4 | ||
|
6d451785f0 | ||
|
7791901b2d | ||
|
2afe1fbeed | ||
|
e2097e856e | ||
|
03e7a3ea65 | ||
|
27f8cc0e52 | ||
|
32efbd5823 | ||
|
6dbdabf9fd | ||
|
75d57b9f04 | ||
|
554547b431 | ||
|
b811da6b83 | ||
|
ca6bc1dcb0 | ||
|
7c3fd42a86 | ||
|
04f12d1e2f | ||
|
c6b8ea90b7 | ||
|
7f8fb8d571 | ||
|
f8cfb084e0 | ||
|
70b084457a | ||
|
6c12244587 | ||
|
e7c0365079 | ||
|
43b11de596 | ||
|
ef45ea5a50 | ||
|
483edb70bf | ||
|
7516d25bc6 | ||
|
2f2918bd3b | ||
|
73d2b3363b | ||
|
ba0cc7fbf9 | ||
|
b7f37138f8 | ||
|
53a451671c | ||
|
65dff6e8e3 | ||
|
03a2de961d | ||
|
b94310a4cc | ||
|
9c594da847 | ||
|
93e62de3d2 | ||
|
a3efbb3466 | ||
|
aaf01b98d2 | ||
|
af037b9d70 | ||
|
5dafd7e4a7 | ||
|
e2b5f4a9fb | ||
|
2e58f0db10 | ||
|
26b31acbae | ||
|
66e96244ef | ||
|
4dc0183901 | ||
|
d33eded060 | ||
|
5913142389 | ||
|
66ef28c2e2 | ||
|
19c30fc411 | ||
|
50bed826d0 | ||
|
b5851dd6d4 | ||
|
ff1ee7d292 | ||
|
9455428048 | ||
|
0f919f3d49 | ||
|
d556a675e9 | ||
|
bfc1fa5181 | ||
|
d9387dac99 | ||
|
4818ee57b6 | ||
|
addb5efebb | ||
|
8adb9ee633 | ||
|
418fc98d1a | ||
|
beffe4a1f2 | ||
|
ef15b44a1b | ||
|
bc802bfc77 | ||
|
d10a5df3df | ||
|
b05d27ce45 | ||
|
e61c9fdde9 | ||
|
d2e2791729 | ||
|
68a7756621 | ||
|
42063cbd5c | ||
|
a407a2e0f8 | ||
|
6ec1ccf7a3 | ||
|
044f4182d0 | ||
|
bae30d79c9 | ||
|
25a60969fb | ||
|
528a67722b | ||
|
d29dc95962 | ||
|
fc3d4dcf5e | ||
|
3d4218324f | ||
|
f6fbb45978 | ||
|
dee16f543d | ||
|
9959d1aa43 | ||
|
76146c4e74 | ||
|
8a8023fcf8 | ||
|
4b0d1e448d | ||
|
6748a2f2f3 | ||
|
4c4d772a5f | ||
|
85740d810b | ||
|
2305ebca24 | ||
|
59bf388534 | ||
|
3066b95a6d | ||
|
1bd77a83bd | ||
|
d0b7336da7 | ||
|
c80f71bd9b | ||
|
15fa3b7d9f | ||
|
e2d7f2cf29 | ||
|
3031fb910f | ||
|
d999dbe0a0 | ||
|
60d5e66e34 | ||
|
c6964502c4 | ||
|
ca2633ff82 | ||
|
a1625c7f15 | ||
|
30a913c05c | ||
|
1d02933481 | ||
|
62c2ec0614 | ||
|
45ca20dec9 | ||
|
de362d2322 | ||
|
115e6e9cf8 | ||
|
f17538b7df | ||
|
6f68c8cd1f | ||
|
02dd72ba57 | ||
|
63629efae7 | ||
|
9015b27803 | ||
|
a5f0670f7f | ||
|
d7db395016 | ||
|
99eef493d2 | ||
|
0d395249ff | ||
|
5cf1da974a | ||
|
2f0ec88f40 | ||
|
d9d3c4a724 | ||
|
bc4d4f424a | ||
|
67459650d4 | ||
|
c31bce1e2d | ||
|
3e3b556108 | ||
|
723daf9497 | ||
|
f77958fc35 | ||
|
ea9f2c6e35 | ||
|
7d20238423 | ||
|
31131db756 | ||
|
17e634c563 | ||
|
a7dc3d84e0 | ||
|
b80aec90d0 | ||
|
8544733048 | ||
|
140fdcca81 | ||
|
2e08c48742 | ||
|
4800bb05d2 | ||
|
384cabede5 | ||
|
4e9eeb1fa1 | ||
|
a534cc9eca | ||
|
e52132c85b | ||
|
76667ffcf9 | ||
|
8ba4b72b37 | ||
|
81e1417ce5 | ||
|
c1576b5b19 | ||
|
86cc3b9607 | ||
|
c7f85e6283 | ||
|
6a93dc9d54 | ||
|
dfd08b337c | ||
|
2d1f2f319f | ||
|
68f82b9182 | ||
|
c8f880c701 | ||
|
f2d3f0bdf9 | ||
|
9f8c63c7d5 | ||
|
2b5a1a7a1c | ||
|
499b2fb0d6 | ||
|
b7679c7826 | ||
|
ce01a66ff3 | ||
|
7582be1a39 | ||
|
f989fd0743 | ||
|
097e84aeed | ||
|
faadb5148f | ||
|
8d9fa31f3d | ||
|
56ed4f0515 | ||
|
43981bb675 | ||
|
cd38511ad4 | ||
|
53f13fd811 | ||
|
77cc52e4ac | ||
|
35cb4606f6 | ||
|
d01ed355e0 | ||
|
495fb24b9a | ||
|
911fe9e9f8 | ||
|
311ffc3672 | ||
|
7a1488fcd3 | ||
|
9f255aee25 | ||
|
67603e58bf | ||
|
4267c0d9b6 | ||
|
88eb728fe3 | ||
|
26c835cdd1 | ||
|
7d3d697a20 | ||
|
798ee3c23c | ||
|
7581058c93 | ||
|
4f0ddfc30d | ||
|
0b918464c1 | ||
|
57bd37ef2f | ||
|
9fa1288dbc | ||
|
55eed868fa | ||
|
abb1baeecd | ||
|
5784b07f14 | ||
|
8e1e0b3740 | ||
|
3f42e0e945 | ||
|
9146e439d2 | ||
|
7a14a0b81f | ||
|
9247475ab2 | ||
|
6b4c04c390 | ||
|
e8216ae9e7 | ||
|
365a0b2832 | ||
|
f78389b6ef | ||
|
0d231d8bd9 | ||
|
d838790b8f | ||
|
9ce3545901 | ||
|
64ac111d55 | ||
|
e9f170a197 | ||
|
e359499e79 | ||
|
48a5a55e2f | ||
|
4828e3a691 | ||
|
e607944ad1 | ||
|
d790ad91e2 | ||
|
4f39c7c155 | ||
|
8db569e8a5 | ||
|
f3932e4b65 | ||
|
d40b776205 | ||
|
9dbac2cb33 | ||
|
9216dbe28a | ||
|
95cfe16676 | ||
|
dabb6a481f | ||
|
d294a692d2 | ||
|
0266c707cc | ||
|
0b3e268f2c | ||
|
12df164245 | ||
|
d51ca20992 | ||
|
4a1adaeab2 | ||
|
fd5bfd161d | ||
|
0c496ca223 | ||
|
175ab5ea76 | ||
|
5627097a6c | ||
|
94fb23ba17 | ||
|
dbd8ed14bf | ||
|
789078e916 | ||
|
833293ad77 | ||
|
a8d3297c4e | ||
|
532fd3c560 | ||
|
0c937d02df | ||
|
8a800a4cb2 | ||
|
8f6f97b8e4 | ||
|
79df1c9040 | ||
|
9a71c3d8dc | ||
|
74788893e9 | ||
|
5c0ecc0250 | ||
|
c0ac2c95ca | ||
|
be4c80e201 | ||
|
32a110b601 | ||
|
48d1f7887d | ||
|
dd02267f9b | ||
|
142a297552 | ||
|
9aeea4d9fa | ||
|
e8ecd6b006 | ||
|
71b961d3f3 | ||
|
271056a4aa | ||
|
141565d9d2 | ||
|
c400c5571f | ||
|
d266579be1 | ||
|
f61c67e6be | ||
|
5f6d753cb7 | ||
|
8211b4cc24 | ||
|
000a3e005c | ||
|
d7aacb8b24 | ||
|
6615aea5dc | ||
|
27a4e58fb1 | ||
|
4c7dc4c1ba | ||
|
5ce78660cf | ||
|
89f5cc577e | ||
|
dc7dd836c6 | ||
|
88c6b4adae | ||
|
020996ea04 | ||
|
30a8f66db2 | ||
|
9ba733d4e0 | ||
|
fafe478e5c | ||
|
b7bb4459f9 | ||
|
3cd61d8278 | ||
|
2d45e397a7 | ||
|
ff7e894e4c | ||
|
7db25d71dd | ||
|
2283cc4ce6 | ||
|
341e84f643 | ||
|
c43a1166e2 | ||
|
6e95d62726 | ||
|
b197ea3ab6 | ||
|
fa418eef16 | ||
|
83eb4c39e5 | ||
|
2dcad150eb | ||
|
eebe4f8920 | ||
|
4dc89f6bc5 | ||
|
9403873a7b | ||
|
ad36f5db46 | ||
|
e96e35b40b | ||
|
aeb60735dc | ||
|
67948d0767 | ||
|
e2120bc66d | ||
|
67b9d2eaf6 | ||
|
7083ed9f6b | ||
|
790fd43369 | ||
|
6b38499bdc | ||
|
cf050cc289 | ||
|
390d42c607 | ||
|
8ccfa5c3fb | ||
|
8073bb4e24 | ||
|
9e19183471 | ||
|
ae3e8a0094 | ||
|
2d0c043dfd | ||
|
a8def0aed2 | ||
|
4e44b034bd | ||
|
e6c83b6efb | ||
|
1dbcfe3c6e | ||
|
58992fc795 | ||
|
eb220544a3 | ||
|
9d5556c7f5 | ||
|
1abcc88fce | ||
|
2e8292a65f | ||
|
4704b2a0e4 | ||
|
9e4eafe8d5 | ||
|
966ffe1669 | ||
|
9d38aeb863 | ||
|
d3ac976d05 | ||
|
4ce6fdcc5a | ||
|
75a4b5bd88 | ||
|
2f5acee798 | ||
|
46798ac322 | ||
|
a8db08c7d4 | ||
|
f3db0e083e | ||
|
18edff9afe | ||
|
03aa67ed68 | ||
|
46f34ca1e3 | ||
|
0f0074cbab | ||
|
47ad024ec7 | ||
|
4944e3ae4b | ||
|
7aa3d8f59b | ||
|
0c25425346 | ||
|
4648f83669 | ||
|
44913c1668 | ||
|
20c7650e51 | ||
|
e8139ad655 | ||
|
9e0687e142 | ||
|
7f3e4b5f47 | ||
|
7b23158e0a | ||
|
f94bb1aa30 | ||
|
a9fef387fa | ||
|
ff5c854192 | ||
|
733ef4664b | ||
|
e89c62174d | ||
|
78e437057c | ||
|
7cdd86c99f | ||
|
c045f4d21f | ||
|
c65b83a6f5 | ||
|
2b2753be21 | ||
|
fe3db1375a | ||
|
2e9eff69d7 | ||
|
7d46f1eed9 | ||
|
7812eb9d19 |
2
.github/workflows/build.yaml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.19.x'
|
go-version: '1.20.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/release.yaml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.19.x'
|
go-version: '1.20.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/test.yaml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.19.x'
|
go-version: '1.20.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ secrets/
|
||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
__pycache__
|
__pycache__
|
||||||
|
web/dev-dist/
|
|
@ -71,7 +71,7 @@ builds:
|
||||||
nfpms:
|
nfpms:
|
||||||
-
|
-
|
||||||
package_name: ntfy
|
package_name: ntfy
|
||||||
homepage: https://heckel.io/ntfy
|
homepage: https://git.zio.sh/astra/ntfy/v2
|
||||||
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
|
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
|
||||||
description: Simple pub-sub notification service
|
description: Simple pub-sub notification service
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
@ -119,8 +119,6 @@ archives:
|
||||||
- server/ntfy.service
|
- server/ntfy.service
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
- client/ntfy-client.service
|
- client/ntfy-client.service
|
||||||
replacements:
|
|
||||||
amd64: x86_64
|
|
||||||
-
|
-
|
||||||
id: ntfy_windows
|
id: ntfy_windows
|
||||||
builds:
|
builds:
|
||||||
|
@ -131,8 +129,6 @@ archives:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
replacements:
|
|
||||||
amd64: x86_64
|
|
||||||
-
|
-
|
||||||
id: ntfy_darwin
|
id: ntfy_darwin
|
||||||
builds:
|
builds:
|
||||||
|
@ -142,8 +138,6 @@ archives:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
replacements:
|
|
||||||
darwin: macOS
|
|
||||||
universal_binaries:
|
universal_binaries:
|
||||||
-
|
-
|
||||||
id: ntfy_darwin_all
|
id: ntfy_darwin_all
|
||||||
|
@ -170,14 +164,14 @@ dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile-arm
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--platform=linux/arm64/v8"
|
- "--platform=linux/arm64/v8"
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile-arm
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 7
|
goarm: 7
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
|
@ -185,7 +179,7 @@ dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile-arm
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 6
|
goarm: 6
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
|
|
|
@ -9,6 +9,7 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
||||||
LABEL org.opencontainers.image.title="ntfy"
|
LABEL org.opencontainers.image.title="ntfy"
|
||||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
COPY ntfy /usr/bin
|
COPY ntfy /usr/bin
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
|
18
Dockerfile-arm
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
FROM alpine
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
|
||||||
|
LABEL org.opencontainers.image.url="https://ntfy.sh/"
|
||||||
|
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
||||||
|
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
|
||||||
|
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
||||||
|
LABEL org.opencontainers.image.title="ntfy"
|
||||||
|
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||||
|
|
||||||
|
# Alpine does not support adding "tzdata" on ARM anymore, see
|
||||||
|
# https://github.com/binwiederhier/ntfy/issues/894
|
||||||
|
|
||||||
|
COPY ntfy /usr/bin
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
ENTRYPOINT ["ntfy"]
|
|
@ -1,14 +1,18 @@
|
||||||
FROM golang:1.19-bullseye as builder
|
FROM golang:1.20-bullseye as builder
|
||||||
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT=unknown
|
||||||
|
ARG NODE_MAJOR=18
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update && apt-get install -y \
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
|
build-essential ca-certificates curl gnupg \
|
||||||
RUN apt-get install -y \
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
build-essential \
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
nodejs \
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
|
||||||
python3-pip
|
&& apt-get update \
|
||||||
|
&& apt-get install -y \
|
||||||
|
python3-pip nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD Makefile .
|
ADD Makefile .
|
||||||
|
|
25
Makefile
|
@ -39,8 +39,8 @@ help:
|
||||||
@echo " make web-deps - Install web app dependencies (npm install the universe)"
|
@echo " make web-deps - Install web app dependencies (npm install the universe)"
|
||||||
@echo " make web-build - Actually build the web app"
|
@echo " make web-build - Actually build the web app"
|
||||||
@echo " make web-lint - Run eslint on the web app"
|
@echo " make web-lint - Run eslint on the web app"
|
||||||
@echo " make web-format - Run prettier on the web app"
|
@echo " make web-fmt - Run prettier on the web app"
|
||||||
@echo " make web-format-check - Run prettier on the web app, but don't change anything"
|
@echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build documentation:"
|
@echo "Build documentation:"
|
||||||
@echo " make docs - Build the documentation"
|
@echo " make docs - Build the documentation"
|
||||||
|
@ -111,18 +111,7 @@ build-deps-ubuntu:
|
||||||
docs: docs-deps docs-build
|
docs: docs-deps docs-build
|
||||||
|
|
||||||
docs-build: .PHONY
|
docs-build: .PHONY
|
||||||
@if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
|
mkdocs build
|
||||||
if which python3.8; then \
|
|
||||||
echo "python3.8 $(shell which mkdocs) build"; \
|
|
||||||
python3.8 $(shell which mkdocs) build; \
|
|
||||||
else \
|
|
||||||
echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
else \
|
|
||||||
echo "mkdocs build"; \
|
|
||||||
mkdocs build; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
docs-deps: .PHONY
|
docs-deps: .PHONY
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
@ -151,10 +140,10 @@ web-deps:
|
||||||
web-deps-update:
|
web-deps-update:
|
||||||
cd web && npm update
|
cd web && npm update
|
||||||
|
|
||||||
web-format:
|
web-fmt:
|
||||||
cd web && npm run format
|
cd web && npm run format
|
||||||
|
|
||||||
web-format-check:
|
web-fmt-check:
|
||||||
cd web && npm run format:check
|
cd web && npm run format:check
|
||||||
|
|
||||||
web-lint:
|
web-lint:
|
||||||
|
@ -248,7 +237,7 @@ cli-build-results:
|
||||||
|
|
||||||
# Test/check targets
|
# Test/check targets
|
||||||
|
|
||||||
check: test web-format-check fmt-check vet web-lint lint staticcheck
|
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
|
||||||
|
|
||||||
test: .PHONY
|
test: .PHONY
|
||||||
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
||||||
|
@ -275,7 +264,7 @@ coverage-upload:
|
||||||
|
|
||||||
# Lint/formatting targets
|
# Lint/formatting targets
|
||||||
|
|
||||||
fmt:
|
fmt: web-fmt
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
|
||||||
fmt-check:
|
fmt-check:
|
||||||
|
|
173
README.md
|
@ -1,180 +1,9 @@
|
||||||

|
|
||||||
|
|
||||||
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
||||||
[](https://github.com/binwiederhier/ntfy/releases/latest)
|
|
||||||
[](https://pkg.go.dev/heckel.io/ntfy)
|
|
||||||
[](https://github.com/binwiederhier/ntfy/actions)
|
|
||||||
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
|
|
||||||
[](https://codecov.io/gh/binwiederhier/ntfy)
|
|
||||||
[](https://discord.gg/cT7ECsZj9w)
|
|
||||||
[](https://matrix.to/#/#ntfy:matrix.org)
|
|
||||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
|
||||||
[](https://www.reddit.com/r/ntfy/)
|
|
||||||
[](https://ntfy.statuspage.io/)
|
|
||||||
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
|
||||||
|
|
||||||
**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
|
**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
|
||||||
notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer,
|
notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer,
|
||||||
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
|
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
|
||||||
so since ntfy is open source.
|
so since ntfy is open source.
|
||||||
|
|
||||||
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android)
|
|
||||||
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).
|
|
||||||
|
|
||||||
<p>
|
### This is a fork of [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy)
|
||||||
<img src=".github/images/screenshot-curl.png" height="180">
|
|
||||||
<img src=".github/images/screenshot-web-detail.png" height="180">
|
|
||||||
<img src=".github/images/screenshot-phone-main.jpg" height="180">
|
|
||||||
<img src=".github/images/screenshot-phone-detail.jpg" height="180">
|
|
||||||
<img src=".github/images/screenshot-phone-notification.jpg" height="180">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
|
|
||||||
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $3.33/month** (if you use promo code `MYTOPIC`, limited time only). You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy). I would be very humbled by your sponsorship. ❤️
|
|
||||||
|
|
||||||
## **[Documentation](https://ntfy.sh/docs/)**
|
|
||||||
|
|
||||||
[Getting started](https://ntfy.sh/docs/) |
|
|
||||||
[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
|
|
||||||
[API](https://ntfy.sh/docs/publish/) |
|
|
||||||
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
|
|
||||||
[Building](https://ntfy.sh/docs/develop/)
|
|
||||||
|
|
||||||
## Chat / forum
|
|
||||||
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
|
|
||||||
works best for you:
|
|
||||||
|
|
||||||
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
|
||||||
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
|
||||||
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
|
|
||||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
|
||||||
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
|
|
||||||
|
|
||||||
## Announcements / beta testers
|
|
||||||
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
|
|
||||||
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
|
|
||||||
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
|
||||||
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
|
||||||
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
|
||||||
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/ntfy/">
|
|
||||||
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
|
||||||
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
|
||||||
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
|
||||||
|
|
||||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Lexevolution"><img src="https://github.com/Lexevolution.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/johnnyip"><img src="https://github.com/johnnyip.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/JonDerThan"><img src="https://github.com/JonDerThan.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/12nick12"><img src="https://github.com/12nick12.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/eanplatter"><img src="https://github.com/eanplatter.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/fnoelscher"><img src="https://github.com/fnoelscher.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/bnorick"><img src="https://github.com/bnorick.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/snh"><img src="https://github.com/snh.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/hen-x"><img src="https://github.com/hen-x.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/JamieGoodson"><img src="https://github.com/JamieGoodson.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/cremesk"><img src="https://github.com/cremesk.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/dangowans"><img src="https://github.com/dangowans.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/tonyakwei"><img src="https://github.com/tonyakwei.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/crosbyh"><img src="https://github.com/crosbyh.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/mdlnr"><img src="https://github.com/mdlnr.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/p-samuel"><img src="https://github.com/p-samuel.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/zugaldia"><img src="https://github.com/zugaldia.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/NathanSweet"><img src="https://github.com/NathanSweet.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/msdeibel"><img src="https://github.com/msdeibel.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/ksurl"><img src="https://github.com/ksurl.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/CodingTimeDEV"><img src="https://github.com/CodingTimeDEV.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Terrormixer3000"><img src="https://github.com/Terrormixer3000.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/voroskoi"><img src="https://github.com/voroskoi.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Nickwasused"><img src="https://github.com/Nickwasused.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/vinhdizzo"><img src="https://github.com/vinhdizzo.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Ge0rg3"><img src="https://github.com/Ge0rg3.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/biopsin"><img src="https://github.com/biopsin.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/thebino"><img src="https://github.com/thebino.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/sky4055"><img src="https://github.com/sky4055.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/julianlam"><img src="https://github.com/julianlam.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/andreapx"><img src="https://github.com/andreapx.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/billycao"><img src="https://github.com/billycao.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/zoic21"><img src="https://github.com/zoic21.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/IanKulin"><img src="https://github.com/IanKulin.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Joachim256"><img src="https://github.com/Joachim256.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/overtone1000"><img src="https://github.com/overtone1000.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/oakd"><img src="https://github.com/oakd.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/KucharczykL"><img src="https://github.com/KucharczykL.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/hansbickhofe"><img src="https://github.com/hansbickhofe.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/caseodilla"><img src="https://github.com/caseodilla.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/0xAF"><img src="https://github.com/0xAF.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/soonoo"><img src="https://github.com/soonoo.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/Twisterado"><img src="https://github.com/Twisterado.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/ScrumpyJack"><img src="https://github.com/ScrumpyJack.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/andrejarrell"><img src="https://github.com/andrejarrell.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/oaustegard"><img src="https://github.com/oaustegard.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/CreativeWarlock"><img src="https://github.com/CreativeWarlock.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
|
|
||||||
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
|
|
||||||
|
|
||||||
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
|
|
||||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
|
||||||
|
|
||||||
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
|
||||||
|
|
||||||
## Code of Conduct
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
|
|
||||||
|
|
||||||
_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
|
||||||
|
|
||||||
## License
|
|
||||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
|
||||||
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
|
|
||||||
|
|
||||||
Third party libraries and resources:
|
|
||||||
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
|
|
||||||
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
|
|
||||||
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
|
|
||||||
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
|
|
||||||
* [React](https://reactjs.org/) (MIT) is used for the web app
|
|
||||||
* [Material UI components](https://mui.com/) (MIT) are used in the web app
|
|
||||||
* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app
|
|
||||||
* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB
|
|
||||||
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
|
|
||||||
* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails
|
|
||||||
* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests
|
|
||||||
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
|
|
||||||
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
|
|
||||||
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
|
|
||||||
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page
|
|
||||||
* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files
|
|
||||||
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
|
|
||||||
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
|
|
||||||
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
|
@ -7,7 +7,10 @@
|
||||||
|
|
||||||
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
||||||
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
||||||
# use empty double-quotes ("")
|
# use empty double-quotes ("").
|
||||||
|
#
|
||||||
|
# To override the default user:password combination or default token for a particular subscription (e.g., to send
|
||||||
|
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").
|
||||||
|
|
||||||
# default-token:
|
# default-token:
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
|
@ -23,9 +23,9 @@ type Config struct {
|
||||||
// Subscribe is the struct for a Subscription within Config
|
// Subscribe is the struct for a Subscription within Config
|
||||||
type Subscribe struct {
|
type Subscribe struct {
|
||||||
Topic string `yaml:"topic"`
|
Topic string `yaml:"topic"`
|
||||||
User string `yaml:"user"`
|
User *string `yaml:"user"`
|
||||||
Password *string `yaml:"password"`
|
Password *string `yaml:"password"`
|
||||||
Token string `yaml:"token"`
|
Token *string `yaml:"token"`
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"`
|
||||||
If map[string]string `yaml:"if"`
|
If map[string]string `yaml:"if"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package client_test
|
package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -37,7 +37,7 @@ subscribe:
|
||||||
require.Equal(t, 4, len(conf.Subscribe))
|
require.Equal(t, 4, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||||
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
||||||
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
||||||
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
||||||
|
@ -67,7 +67,7 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||||
require.Equal(t, "", *conf.Subscribe[0].Password)
|
require.Equal(t, "", *conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ subscribe:
|
||||||
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].User)
|
require.Nil(t, conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Token)
|
require.Nil(t, conf.Subscribe[0].Token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -72,6 +72,11 @@ func WithAttach(attach string) PublishOption {
|
||||||
return WithHeader("X-Attach", attach)
|
return WithHeader("X-Attach", attach)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMarkdown instructs the server to interpret the message body as Markdown
|
||||||
|
func WithMarkdown() PublishOption {
|
||||||
|
return WithHeader("X-Markdown", "yes")
|
||||||
|
}
|
||||||
|
|
||||||
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
|
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
|
||||||
func WithFilename(filename string) PublishOption {
|
func WithFilename(filename string) PublishOption {
|
||||||
return WithHeader("X-Filename", filename)
|
return WithHeader("X-Filename", filename)
|
||||||
|
@ -92,6 +97,11 @@ func WithBearerAuth(token string) PublishOption {
|
||||||
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithEmptyAuth clears the Authorization header
|
||||||
|
func WithEmptyAuth() PublishOption {
|
||||||
|
return RemoveHeader("Authorization")
|
||||||
|
}
|
||||||
|
|
||||||
// WithNoCache instructs the server not to cache the message server-side
|
// WithNoCache instructs the server not to cache the message server-side
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
return WithHeader("X-Cache", "no")
|
return WithHeader("X-Cache", "no")
|
||||||
|
@ -182,3 +192,13 @@ func WithQueryParam(param, value string) RequestOption {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveHeader is a generic option to remove a header from a request
|
||||||
|
func RemoveHeader(header string) RequestOption {
|
||||||
|
return func(r *http.Request) error {
|
||||||
|
if header != "" {
|
||||||
|
delete(r.Header, header)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,9 +3,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -31,6 +31,7 @@ var flagsPublish = append(
|
||||||
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
||||||
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
||||||
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
||||||
|
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
|
||||||
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
||||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||||
|
@ -95,6 +96,7 @@ func execPublish(c *cli.Context) error {
|
||||||
icon := c.String("icon")
|
icon := c.String("icon")
|
||||||
actions := c.String("actions")
|
actions := c.String("actions")
|
||||||
attach := c.String("attach")
|
attach := c.String("attach")
|
||||||
|
markdown := c.Bool("markdown")
|
||||||
filename := c.String("filename")
|
filename := c.String("filename")
|
||||||
file := c.String("file")
|
file := c.String("file")
|
||||||
email := c.String("email")
|
email := c.String("email")
|
||||||
|
@ -140,6 +142,9 @@ func execPublish(c *cli.Context) error {
|
||||||
if attach != "" {
|
if attach != "" {
|
||||||
options = append(options, client.WithAttach(attach))
|
options = append(options, client.WithAttach(attach))
|
||||||
}
|
}
|
||||||
|
if markdown {
|
||||||
|
options = append(options, client.WithMarkdown())
|
||||||
|
}
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
options = append(options, client.WithFilename(filename))
|
options = append(options, client.WithFilename(filename))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
|
25
cmd/serve.go
|
@ -5,8 +5,8 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"github.com/stripe/stripe-go/v74"
|
"github.com/stripe/stripe-go/v74"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
@ -17,12 +17,12 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -94,6 +94,11 @@ var flagsServe = append(
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdServe = &cli.Command{
|
var cmdServe = &cli.Command{
|
||||||
|
@ -129,6 +134,11 @@ func execServe(c *cli.Context) error {
|
||||||
keyFile := c.String("key-file")
|
keyFile := c.String("key-file")
|
||||||
certFile := c.String("cert-file")
|
certFile := c.String("cert-file")
|
||||||
firebaseKeyFile := c.String("firebase-key-file")
|
firebaseKeyFile := c.String("firebase-key-file")
|
||||||
|
webPushPrivateKey := c.String("web-push-private-key")
|
||||||
|
webPushPublicKey := c.String("web-push-public-key")
|
||||||
|
webPushFile := c.String("web-push-file")
|
||||||
|
webPushEmailAddress := c.String("web-push-email-address")
|
||||||
|
webPushStartupQueries := c.String("web-push-startup-queries")
|
||||||
cacheFile := c.String("cache-file")
|
cacheFile := c.String("cache-file")
|
||||||
cacheDuration := c.Duration("cache-duration")
|
cacheDuration := c.Duration("cache-duration")
|
||||||
cacheStartupQueries := c.String("cache-startup-queries")
|
cacheStartupQueries := c.String("cache-startup-queries")
|
||||||
|
@ -183,6 +193,8 @@ func execServe(c *cli.Context) error {
|
||||||
// Check values
|
// Check values
|
||||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||||
return errors.New("if set, FCM key file must exist")
|
return errors.New("if set, FCM key file must exist")
|
||||||
|
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
|
||||||
|
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
|
||||||
} else if keepaliveInterval < 5*time.Second {
|
} else if keepaliveInterval < 5*time.Second {
|
||||||
return errors.New("keepalive interval cannot be lower than five seconds")
|
return errors.New("keepalive interval cannot be lower than five seconds")
|
||||||
} else if managerInterval < 5*time.Second {
|
} else if managerInterval < 5*time.Second {
|
||||||
|
@ -347,6 +359,11 @@ func execServe(c *cli.Context) error {
|
||||||
conf.MetricsListenHTTP = metricsListenHTTP
|
conf.MetricsListenHTTP = metricsListenHTTP
|
||||||
conf.ProfileListenHTTP = profileListenHTTP
|
conf.ProfileListenHTTP = profileListenHTTP
|
||||||
conf.Version = c.App.Version
|
conf.Version = c.App.Version
|
||||||
|
conf.WebPushPrivateKey = webPushPrivateKey
|
||||||
|
conf.WebPushPublicKey = webPushPublicKey
|
||||||
|
conf.WebPushFile = webPushFile
|
||||||
|
conf.WebPushEmailAddress = webPushEmailAddress
|
||||||
|
conf.WebPushStartupQueries = webPushStartupQueries
|
||||||
|
|
||||||
// Set up hot-reloading of config
|
// Set up hot-reloading of config
|
||||||
go sigHandlerConfigReload(config)
|
go sigHandlerConfigReload(config)
|
||||||
|
|
|
@ -9,12 +9,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -3,10 +3,10 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/client"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/client"
|
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
@ -225,12 +225,17 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
||||||
// check for subscription token then subscription user:pass
|
// if an explicit empty token or empty user:pass is given, exit without auth
|
||||||
if s.Token != "" {
|
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
|
||||||
return client.WithBearerAuth(s.Token)
|
return client.WithEmptyAuth()
|
||||||
}
|
}
|
||||||
if s.User != "" && s.Password != nil {
|
|
||||||
return client.WithBasicAuth(s.User, *s.Password)
|
// check for subscription token then subscription user:pass
|
||||||
|
if s.Token != nil && *s.Token != "" {
|
||||||
|
return client.WithBearerAuth(*s.Token)
|
||||||
|
}
|
||||||
|
if s.User != nil && *s.User != "" && s.Password != nil {
|
||||||
|
return client.WithBasicAuth(*s.User, *s.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
||||||
|
|
|
@ -330,7 +330,7 @@ default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
}
|
}
|
||||||
|
@ -355,7 +355,63 @@ default-password: mypass
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: mypass
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
user: ""
|
||||||
|
password: ""
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
token: ""
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/user"
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/test"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/test"
|
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
48
cmd/webpush.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
//go:build !noserver
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdWebPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdWebPush = &cli.Command{
|
||||||
|
Name: "webpush",
|
||||||
|
Usage: "Generate keys, in the future manage web push subscriptions",
|
||||||
|
UsageText: "ntfy webpush [keys]",
|
||||||
|
Category: categoryServer,
|
||||||
|
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Action: generateWebPushKeys,
|
||||||
|
Name: "keys",
|
||||||
|
Usage: "Generate VAPID keys to enable browser background push notifications",
|
||||||
|
UsageText: "ntfy webpush keys",
|
||||||
|
Category: categoryServer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateWebPushKeys(c *cli.Context) error {
|
||||||
|
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
|
||||||
|
|
||||||
|
web-push-public-key: %s
|
||||||
|
web-push-private-key: %s
|
||||||
|
web-push-file: /var/cache/ntfy/webpush.db # or similar
|
||||||
|
web-push-email-address: <email address>
|
||||||
|
|
||||||
|
See https://ntfy.sh/docs/config/#web-push for details.
|
||||||
|
`, publicKey, privateKey)
|
||||||
|
return err
|
||||||
|
}
|
24
cmd/webpush_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
|
||||||
|
app, _, _, stderr := newTestApp()
|
||||||
|
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
|
||||||
|
require.Contains(t, stderr.String(), "Web Push keys generated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||||
|
webPushArgs := []string{
|
||||||
|
"ntfy",
|
||||||
|
"--log-level=ERROR",
|
||||||
|
"webpush",
|
||||||
|
}
|
||||||
|
return app.Run(append(webPushArgs, args...))
|
||||||
|
}
|
148
docs/config.md
|
@ -44,6 +44,14 @@ Here are a few working sample configs:
|
||||||
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
=== "server.yml (behind proxy, with cache + attachments)"
|
||||||
|
``` yaml
|
||||||
|
base-url: "http://ntfy.example.com"
|
||||||
|
listen-http: ":2586"
|
||||||
|
cache-file: "/var/cache/ntfy/cache.db"
|
||||||
|
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||||
|
```
|
||||||
|
|
||||||
=== "server.yml (ntfy.sh config)"
|
=== "server.yml (ntfy.sh config)"
|
||||||
``` yaml
|
``` yaml
|
||||||
# All the things: Behind a proxy, Firebase, cache, attachments,
|
# All the things: Behind a proxy, Firebase, cache, attachments,
|
||||||
|
@ -458,6 +466,31 @@ $ dig A mx1.ntfy.sh +short
|
||||||
3.139.215.220
|
3.139.215.220
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Local-only email
|
||||||
|
If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
|
||||||
|
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
|
||||||
|
anything).
|
||||||
|
|
||||||
|
=== "/etc/ntfy/server.yml"
|
||||||
|
``` yaml
|
||||||
|
smtp-server-listen: ":25"
|
||||||
|
smtp-server-domain: "example.com"
|
||||||
|
smtp-server-addr-prefix: "ntfy-" # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
|
||||||
|
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
|
||||||
|
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
|
||||||
|
In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
|
||||||
|
`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
|
||||||
|
|
||||||
|
So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
|
||||||
|
topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
|
||||||
|
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
|
||||||
|
|
||||||
|
If the internal service lets you use define an email "Subject", it will become the title of the notification.
|
||||||
|
The body of the email will become the message of the notification.
|
||||||
|
|
||||||
## Behind a proxy (TLS, etc.)
|
## Behind a proxy (TLS, etc.)
|
||||||
!!! warning
|
!!! warning
|
||||||
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
||||||
|
@ -649,8 +682,8 @@ or the root domain:
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName ntfy.sh
|
ServerName ntfy.sh
|
||||||
|
|
||||||
# Proxy connections to ntfy (requires "a2enmod proxy")
|
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
||||||
ProxyPass / http://127.0.0.1:2586/
|
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
||||||
ProxyPassReverse / http://127.0.0.1:2586/
|
ProxyPassReverse / http://127.0.0.1:2586/
|
||||||
|
|
||||||
SetEnv proxy-nokeepalive 1
|
SetEnv proxy-nokeepalive 1
|
||||||
|
@ -659,18 +692,12 @@ or the root domain:
|
||||||
# Higher than the max message size of 4096 bytes
|
# Higher than the max message size of 4096 bytes
|
||||||
LimitRequestBody 102400
|
LimitRequestBody 102400
|
||||||
|
|
||||||
# Enable mod_rewrite (requires "a2enmod rewrite")
|
|
||||||
RewriteEngine on
|
|
||||||
|
|
||||||
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
|
||||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
|
||||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
|
||||||
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
|
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
|
||||||
# it to work with curl without the annoying https:// prefix
|
# it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
|
||||||
RewriteCond %{REQUEST_METHOD} GET
|
<If "%{REQUEST_METHOD} == 'GET'">
|
||||||
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
|
RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
|
||||||
|
</If>
|
||||||
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
||||||
<VirtualHost *:443>
|
<VirtualHost *:443>
|
||||||
|
@ -681,8 +708,8 @@ or the root domain:
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
|
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
|
||||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||||
|
|
||||||
# Proxy connections to ntfy (requires "a2enmod proxy")
|
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
||||||
ProxyPass / http://127.0.0.1:2586/
|
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
||||||
ProxyPassReverse / http://127.0.0.1:2586/
|
ProxyPassReverse / http://127.0.0.1:2586/
|
||||||
|
|
||||||
SetEnv proxy-nokeepalive 1
|
SetEnv proxy-nokeepalive 1
|
||||||
|
@ -691,13 +718,6 @@ or the root domain:
|
||||||
# Higher than the max message size of 4096 bytes
|
# Higher than the max message size of 4096 bytes
|
||||||
LimitRequestBody 102400
|
LimitRequestBody 102400
|
||||||
|
|
||||||
# Enable mod_rewrite (requires "a2enmod rewrite")
|
|
||||||
RewriteEngine on
|
|
||||||
|
|
||||||
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
|
||||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
|
||||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
|
||||||
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -789,6 +809,57 @@ Note that the self-hosted server literally sends the message `New message` for e
|
||||||
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
|
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
|
||||||
it'll show `New message` as a popup.
|
it'll show `New message` as a popup.
|
||||||
|
|
||||||
|
## Web Push
|
||||||
|
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
|
||||||
|
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
|
||||||
|
When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the
|
||||||
|
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
|
||||||
|
forward it to the browser.
|
||||||
|
|
||||||
|
To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
|
||||||
|
a database to keep track of the browser's subscriptions, and an admin email address (you):
|
||||||
|
|
||||||
|
- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
|
||||||
|
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
|
||||||
|
- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
|
||||||
|
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
|
||||||
|
- `web-push-startup-queries` is an optional list of queries to run on startup`
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
|
||||||
|
- Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A _valid_
|
||||||
|
certificate is required, as service workers will not run on origins with untrusted certificates.
|
||||||
|
|
||||||
|
- Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This
|
||||||
|
is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.
|
||||||
|
|
||||||
|
To configure VAPID keys, first generate them:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ntfy webpush keys
|
||||||
|
Web Push keys generated.
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
|
||||||
|
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
|
||||||
|
web-push-file: /var/cache/ntfy/webpush.db
|
||||||
|
web-push-email-address: sysadmin@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days,
|
||||||
|
and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
|
||||||
|
subscriptions are also removed automatically.
|
||||||
|
|
||||||
|
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
|
||||||
|
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.
|
||||||
|
|
||||||
|
Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and
|
||||||
|
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.
|
||||||
|
|
||||||
## Tiers
|
## Tiers
|
||||||
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
|
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
|
||||||
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
|
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
|
||||||
|
@ -1109,10 +1180,10 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||||
|
|
||||||
## Health checks
|
## Health checks
|
||||||
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
|
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
|
||||||
If a non-200 HTTP status code is returned or if the returned `health` field is `false` the ntfy service should be considered as unhealthy.
|
If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"health":true}
|
{"healthy":true}
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
|
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
|
||||||
|
@ -1285,13 +1356,17 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
|
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
|
||||||
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
||||||
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
|
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
|
||||||
|
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate |
|
||||||
|
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate |
|
||||||
|
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
|
||||||
|
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
|
||||||
|
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
|
||||||
|
|
||||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
```
|
```
|
||||||
$ ntfy serve --help
|
|
||||||
NAME:
|
NAME:
|
||||||
ntfy serve - Run the ntfy server
|
ntfy serve - Run the ntfy server
|
||||||
|
|
||||||
|
@ -1321,8 +1396,8 @@ OPTIONS:
|
||||||
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
|
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
|
||||||
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
||||||
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
||||||
--listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
--listen-http value, --listen_http value, -l value ip:port used as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
||||||
--listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
||||||
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
|
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
|
||||||
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
|
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
|
||||||
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
|
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
|
||||||
|
@ -1343,11 +1418,12 @@ OPTIONS:
|
||||||
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
|
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
|
||||||
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
||||||
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
|
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
|
||||||
--web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT]
|
--web-root value, --web_root value sets root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
|
||||||
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
||||||
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
|
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
|
||||||
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
|
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
|
||||||
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
|
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
|
||||||
|
--upstream-access-token value, --upstream_access_token value access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN]
|
||||||
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
|
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
|
||||||
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
|
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
|
||||||
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
|
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
|
||||||
|
@ -1355,6 +1431,10 @@ OPTIONS:
|
||||||
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
|
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
|
||||||
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
|
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
|
||||||
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
|
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
|
||||||
|
--twilio-account value, --twilio_account value Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT]
|
||||||
|
--twilio-auth-token value, --twilio_auth_token value Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
|
||||||
|
--twilio-phone-number value, --twilio_phone_number value Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
|
||||||
|
--twilio-verify-service value, --twilio_verify_service value Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
|
||||||
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
||||||
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
||||||
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||||
|
@ -1365,10 +1445,18 @@ OPTIONS:
|
||||||
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
|
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
|
||||||
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
||||||
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||||
|
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
|
||||||
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
||||||
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
|
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
|
||||||
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
|
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
|
||||||
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
|
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
|
||||||
--help, -h show help (default: false)
|
--enable-metrics, --enable_metrics if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS]
|
||||||
|
--metrics-listen-http value, --metrics_listen_http value ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP]
|
||||||
|
--profile-listen-http value, --profile_listen_http value ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP]
|
||||||
|
--web-push-public-key value, --web_push_public_key value public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY]
|
||||||
|
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
|
||||||
|
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
|
||||||
|
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
|
||||||
|
--web-push-startup-queries value, --web_push_startup-queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
||||||
|
--help, -h show help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ server consists of three components:
|
||||||
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
||||||
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
|
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
|
||||||
build the docs.
|
build the docs.
|
||||||
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
|
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/)
|
||||||
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
|
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
|
||||||
and install all the 100,000 dependencies (*sigh*).
|
and install all the 100,000 dependencies (*sigh*).
|
||||||
|
|
||||||
|
@ -241,6 +241,41 @@ $ cd web
|
||||||
$ npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testing Web Push locally
|
||||||
|
|
||||||
|
Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http>
|
||||||
|
|
||||||
|
#### With the dev servers
|
||||||
|
|
||||||
|
1. Get web push keys `go run main.go webpush keys`
|
||||||
|
|
||||||
|
2. Run the server with web push enabled
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run main.go \
|
||||||
|
--log-level debug \
|
||||||
|
serve \
|
||||||
|
--web-push-public-key KEY \
|
||||||
|
--web-push-private-key KEY \
|
||||||
|
--web-push-email-address <email> \
|
||||||
|
--web-push-file=/tmp/webpush.db
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In `web/public/config.js`:
|
||||||
|
|
||||||
|
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
|
||||||
|
|
||||||
|
- Set the `web_push_public_key` correctly.
|
||||||
|
|
||||||
|
4. Run `npm run start`
|
||||||
|
|
||||||
|
#### With a built package
|
||||||
|
|
||||||
|
1. Run `make web-build`
|
||||||
|
|
||||||
|
2. Run the server (step 2 above)
|
||||||
|
|
||||||
|
3. Open <http://localhost/>
|
||||||
### Build the docs
|
### Build the docs
|
||||||
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
|
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
|
||||||
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
|
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
|
||||||
|
@ -394,7 +429,7 @@ steps:
|
||||||
|
|
||||||
### XCode setup
|
### XCode setup
|
||||||
|
|
||||||
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
|
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
|
||||||
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
|
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
|
||||||
1. Similarly, install the SQLite.swift package dependency in XCode
|
1. Similarly, install the SQLite.swift package dependency in XCode
|
||||||
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
<!-- This file was generated by scripts/emoji-convert.sh -->
|
<!-- This file was generated by scripts/emoji-convert.sh -->
|
||||||
|
|
||||||
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||||
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
||||||
[tagging and emojis page](../publish/#tags-emojis).
|
[tagging and emojis page](publish.md#tags-emojis).
|
||||||
|
|
||||||
<table class="remove-md-box emoji-table"><tr>
|
<table class="remove-md-box emoji-table"><tr>
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,21 @@ You can send a message during a workflow run with curl. Here is an example sendi
|
||||||
${{ secrets.NTFY_URL }}
|
${{ secrets.NTFY_URL }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Changedetection.io
|
||||||
|
ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
|
||||||
|
[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
|
||||||
|
uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
|
||||||
|
|
||||||
|
To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
|
||||||
|
to the notification list.
|
||||||
|
|
||||||
|
For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
|
||||||
|
|
||||||
|
In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
|
||||||
|
the special ntfy Apprise Notification URL to the Notification List.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Watchtower (shoutrrr)
|
## Watchtower (shoutrrr)
|
||||||
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
|
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
|
||||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||||
|
|
22
docs/faq.md
|
@ -76,7 +76,29 @@ However, if you still want to disable it, you can do so with the `web-root: disa
|
||||||
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
||||||
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
||||||
|
|
||||||
|
## If topic names are public, could I not just brute force them?
|
||||||
|
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
|
||||||
|
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
|
||||||
|
the topic is as good as a good password.
|
||||||
|
|
||||||
|
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
|
||||||
|
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
|
||||||
|
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
|
||||||
|
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
|
||||||
|
|
||||||
|
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
|
||||||
|
|
||||||
## Where can I donate?
|
## Where can I donate?
|
||||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
||||||
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
||||||
appreciated.
|
appreciated.
|
||||||
|
|
||||||
|
## Can I email you? Can I DM you on Discord/Matrix?
|
||||||
|
While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org),
|
||||||
|
[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally
|
||||||
|
**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a
|
||||||
|
[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities.
|
||||||
|
|
||||||
|
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
|
||||||
|
in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users
|
||||||
|
may be able to help out. I hope you understand.
|
||||||
|
|
|
@ -14,14 +14,15 @@ We support amd64, armv7 and arm64.
|
||||||
|
|
||||||
1. Install ntfy using one of the methods described below
|
1. Install ntfy using one of the methods described below
|
||||||
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
||||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||||
|
|
||||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||||
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
||||||
for details).
|
for details).
|
||||||
|
|
||||||
If you like video tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU).
|
If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
|
||||||
It's short and to the point. _I am not affiliated with Kris, I just liked the video._
|
[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
|
||||||
|
resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
|
||||||
|
|
||||||
## Linux binaries
|
## Linux binaries
|
||||||
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
||||||
|
@ -29,37 +30,37 @@ deb/rpm packages.
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_x86_64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.tar.gz
|
||||||
tar zxvf ntfy_2.5.0_linux_x86_64.tar.gz
|
tar zxvf ntfy_2.7.0_linux_amd64.tar.gz
|
||||||
sudo cp -a ntfy_2.5.0_linux_x86_64/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.7.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.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.5.0/ntfy_2.5.0_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_2.5.0_linux_armv6.tar.gz
|
tar zxvf ntfy_2.7.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_2.5.0_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.7.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.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.5.0/ntfy_2.5.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_2.5.0_linux_armv7.tar.gz
|
tar zxvf ntfy_2.7.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_2.5.0_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.7.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.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.5.0/ntfy_2.5.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_2.5.0_linux_arm64.tar.gz
|
tar zxvf ntfy_2.7.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_2.5.0_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.7.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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
|
||||||
|
@ -117,7 +118,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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
|
||||||
|
@ -125,7 +126,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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
|
||||||
|
@ -133,7 +134,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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
|
||||||
|
@ -143,34 +144,36 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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.5.0/ntfy_2.5.0_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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.5.0/ntfy_2.5.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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.5.0/ntfy_2.5.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arch Linux
|
## Arch Linux
|
||||||
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
|
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/).
|
||||||
|
You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download,
|
||||||
|
build and install ntfy and keep it up to date.
|
||||||
```
|
```
|
||||||
paru -S ntfysh-bin
|
paru -S ntfysh-bin
|
||||||
```
|
```
|
||||||
|
@ -192,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
|
||||||
|
|
||||||
## 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.5.0/ntfy_2.5.0_macOS_all.tar.gz),
|
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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.5.0/ntfy_2.5.0_macOS_all.tar.gz > ntfy_2.5.0_macOS_all.tar.gz
|
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz > ntfy_2.7.0_darwin_all.tar.gz
|
||||||
tar zxvf ntfy_2.5.0_macOS_all.tar.gz
|
tar zxvf ntfy_2.7.0_darwin_all.tar.gz
|
||||||
sudo cp -a ntfy_2.5.0_macOS_all/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.7.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||||
mkdir ~/Library/Application\ Support/ntfy
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
cp ntfy_2.5.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
cp ntfy_2.7.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
ntfy --help
|
ntfy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -221,7 +224,7 @@ brew install ntfy
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
||||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_windows_x86_64.zip),
|
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.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%`.
|
||||||
|
|
||||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||||
|
@ -277,7 +280,7 @@ docker run \
|
||||||
|
|
||||||
Using docker-compose with non-root user and healthchecks enabled:
|
Using docker-compose with non-root user and healthchecks enabled:
|
||||||
```yaml
|
```yaml
|
||||||
version: "2.1"
|
version: "2.3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ntfy:
|
ntfy:
|
||||||
|
|
|
@ -23,6 +23,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
||||||
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
|
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
|
||||||
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
|
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
|
||||||
|
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
|
||||||
|
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
|
||||||
|
|
||||||
## Integration via HTTP/SMTP/etc.
|
## Integration via HTTP/SMTP/etc.
|
||||||
|
|
||||||
|
@ -55,6 +57,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
|
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
|
||||||
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
||||||
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
|
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
|
||||||
|
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
|
||||||
|
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
|
||||||
|
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
|
||||||
|
|
||||||
## CLIs + GUIs
|
## CLIs + GUIs
|
||||||
|
|
||||||
|
@ -121,9 +126,38 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
|
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
|
||||||
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
|
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
|
||||||
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
|
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
|
||||||
|
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup and shutdown (Go)
|
||||||
|
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
|
||||||
|
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
|
||||||
|
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
|
||||||
|
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
|
||||||
|
- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
|
||||||
|
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
|
||||||
|
- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
|
||||||
|
- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
|
||||||
|
- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
|
||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
|
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
|
||||||
|
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
|
||||||
|
- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
|
||||||
|
- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
|
||||||
|
- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
|
||||||
|
- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
|
||||||
|
- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
|
||||||
|
- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
|
||||||
|
- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
|
||||||
|
- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
|
||||||
|
- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
|
||||||
|
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
|
||||||
|
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
|
||||||
|
- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
|
||||||
|
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
|
||||||
|
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
|
||||||
|
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
|
||||||
|
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
|
||||||
|
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
|
||||||
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
|
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
|
||||||
- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
|
- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
|
||||||
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
|
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
|
||||||
|
@ -147,6 +181,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
||||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||||
|
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022
|
||||||
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||||
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||||
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
|
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
|
||||||
|
@ -203,6 +238,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
||||||
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
|
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
|
||||||
|
| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
|
||||||
|
|
||||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Known issues
|
# Known issues
|
||||||
This is an incomplete list of known issues with the ntfy server, Android app, and iOS app. You can find a complete
|
This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete
|
||||||
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
|
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
|
||||||
to have the prominent ones here to link to.
|
to have the prominent ones here to link to.
|
||||||
|
|
||||||
|
@ -26,3 +26,18 @@ Be sure that in your selfhosted server:
|
||||||
|
|
||||||
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
||||||
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
||||||
|
|
||||||
|
## iOS app seeing "New message", but not real message content
|
||||||
|
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
|
||||||
|
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
|
||||||
|
|
||||||
|
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
|
||||||
|
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
|
||||||
|
|
||||||
|
## Safari does not play sounds for web push notifications
|
||||||
|
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
|
||||||
|
iOS 17 / Safari 17, which will be released later in 2023.
|
||||||
|
|
||||||
|
## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787))
|
||||||
|
When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a
|
||||||
|
[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found.
|
||||||
|
|
125
docs/publish.md
|
@ -457,6 +457,7 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
|
||||||
=== "PowerShell"
|
=== "PowerShell"
|
||||||
``` powershell
|
``` powershell
|
||||||
$Request = @{
|
$Request = @{
|
||||||
|
Method = 'POST'
|
||||||
URI = "https://ntfy.sh/phil_alerts"
|
URI = "https://ntfy.sh/phil_alerts"
|
||||||
Headers = @{
|
Headers = @{
|
||||||
Priority = "5"
|
Priority = "5"
|
||||||
|
@ -623,6 +624,109 @@ them with a comma, e.g. `tag1,tag2,tag3`.
|
||||||
as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
|
as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
|
||||||
or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
|
or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
|
||||||
|
|
||||||
|
## Markdown formatting
|
||||||
|
_Supported on:_ :material-firefox:
|
||||||
|
|
||||||
|
You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use
|
||||||
|
**bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now):
|
||||||
|
|
||||||
|
- [Emphasis](https://www.markdownguide.org/basic-syntax/#emphasis) such as **bold** (`**bold**`), *italics* (`*italics*`)
|
||||||
|
- [Links](https://www.markdownguide.org/basic-syntax/#links) (`[some tool](https://ntfy.sh)`)
|
||||||
|
- [Images](https://www.markdownguide.org/basic-syntax/#images) (``)
|
||||||
|
- [Code blocks](https://www.markdownguide.org/basic-syntax/#code-blocks) (` ```code blocks``` `) and [inline code](https://www.markdownguide.org/basic-syntax/#inline-code) (`` `inline code` ``)
|
||||||
|
- [Headings](https://www.markdownguide.org/basic-syntax/#headings) (`# headings`, `## headings`, etc.)
|
||||||
|
- [Lists](https://www.markdownguide.org/basic-syntax/#lists) (`- lists`, `1. lists`, etc.)
|
||||||
|
- [Blockquotes](https://www.markdownguide.org/basic-syntax/#blockquotes) (`> blockquotes`)
|
||||||
|
- [Horizontal rules](https://www.markdownguide.org/basic-syntax/#horizontal-rules) (`---`)
|
||||||
|
|
||||||
|
By default, messages sent to ntfy are rendered as plain text. To enable Markdown, set the `X-Markdown` header (or any of
|
||||||
|
its aliases: `Markdown`, or `md`) to `true` (or `1` or `yes`), or set the `Content-Type` header to `text/markdown`.
|
||||||
|
As of today, **Markdown is only supported in the web app.** Here's an example of how to enable Markdown formatting:
|
||||||
|
|
||||||
|
=== "Command line (curl)"
|
||||||
|
```
|
||||||
|
curl \
|
||||||
|
-d "Look ma, **bold text**, *italics*, ..." \
|
||||||
|
-H "Markdown: yes" \
|
||||||
|
ntfy.sh/mytopic
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy CLI"
|
||||||
|
```
|
||||||
|
ntfy publish \
|
||||||
|
--markdown \
|
||||||
|
mytopic \
|
||||||
|
"Look ma, **bold text**, *italics*, ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "HTTP"
|
||||||
|
``` http
|
||||||
|
POST /mytopic HTTP/1.1
|
||||||
|
Host: ntfy.sh
|
||||||
|
Markdown: yes
|
||||||
|
|
||||||
|
Look ma, **bold text**, *italics*, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "JavaScript"
|
||||||
|
``` javascript
|
||||||
|
fetch('https://ntfy.sh/mytopic', {
|
||||||
|
method: 'POST', // PUT works too
|
||||||
|
body: 'Look ma, **bold text**, *italics*, ...',
|
||||||
|
headers: { 'Markdown': 'yes' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Go"
|
||||||
|
``` go
|
||||||
|
http.Post("https://ntfy.sh/mytopic", "text/markdown",
|
||||||
|
strings.NewReader("Look ma, **bold text**, *italics*, ..."))
|
||||||
|
|
||||||
|
// or
|
||||||
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic",
|
||||||
|
strings.NewReader("Look ma, **bold text**, *italics*, ..."))
|
||||||
|
req.Header.Set("Markdown", "yes")
|
||||||
|
http.DefaultClient.Do(req)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "PowerShell"
|
||||||
|
``` powershell
|
||||||
|
$Request = @{
|
||||||
|
Method = "POST"
|
||||||
|
URI = "https://ntfy.sh/mytopic"
|
||||||
|
Body = "Look ma, **bold text**, *italics*, ..."
|
||||||
|
Headers = @{
|
||||||
|
Markdown = "yes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Invoke-RestMethod @Request
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Python"
|
||||||
|
``` python
|
||||||
|
requests.post("https://ntfy.sh/mytopic",
|
||||||
|
data="Look ma, **bold text**, *italics*, ..."
|
||||||
|
headers={ "Markdown": "yes" }))
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "PHP"
|
||||||
|
``` php-inline
|
||||||
|
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST', // PUT also works
|
||||||
|
'header' => 'Content-Type: text/markdown', // !
|
||||||
|
'content' => 'Look ma, **bold text**, *italics*, ...'
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's what that looks like in the web app:
|
||||||
|
|
||||||
|
<figure markdown>
|
||||||
|
{ width=500 }
|
||||||
|
<figcaption>Markdown formatting in the web app</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
## Scheduled delivery
|
## Scheduled delivery
|
||||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
|
@ -930,7 +1034,7 @@ is the only required one:
|
||||||
$Request = @{
|
$Request = @{
|
||||||
Method = "POST"
|
Method = "POST"
|
||||||
URI = "https://ntfy.sh"
|
URI = "https://ntfy.sh"
|
||||||
Body = @{
|
Body = ConvertTo-JSON @{
|
||||||
Topic = "mytopic"
|
Topic = "mytopic"
|
||||||
Title = "Low disk space alert"
|
Title = "Low disk space alert"
|
||||||
Message = "Disk space is low at 5.1 GB"
|
Message = "Disk space is low at 5.1 GB"
|
||||||
|
@ -939,7 +1043,7 @@ is the only required one:
|
||||||
FileName = "diskspace.jpg"
|
FileName = "diskspace.jpg"
|
||||||
Tags = @("warning", "cd")
|
Tags = @("warning", "cd")
|
||||||
Click = "https://homecamera.lan/xasds1h2xsSsa/"
|
Click = "https://homecamera.lan/xasds1h2xsSsa/"
|
||||||
Actions = ConvertTo-JSON @(
|
Actions = @(
|
||||||
@{
|
@{
|
||||||
Action = "view"
|
Action = "view"
|
||||||
Label = "Admin panel"
|
Label = "Admin panel"
|
||||||
|
@ -1004,6 +1108,7 @@ all the supported fields:
|
||||||
| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications |
|
| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications |
|
||||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) |
|
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) |
|
||||||
| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) |
|
| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) |
|
||||||
|
| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted |
|
||||||
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
|
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
|
||||||
| `filename` | - | *string* | `file.jpg` | File name of the attachment |
|
| `filename` | - | *string* | `file.jpg` | File name of the attachment |
|
||||||
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
|
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
|
||||||
|
@ -1026,7 +1131,7 @@ As of today, the following actions are supported:
|
||||||
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
|
||||||
|
|
||||||
Here's an example of what that a notification with actions can look like:
|
Here's an example of what a notification with actions can look like:
|
||||||
|
|
||||||
<figure markdown>
|
<figure markdown>
|
||||||
{ width=500 }
|
{ width=500 }
|
||||||
|
@ -1815,10 +1920,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||||
$Request = @{
|
$Request = @{
|
||||||
Method = "POST"
|
Method = "POST"
|
||||||
URI = "https://ntfy.sh"
|
URI = "https://ntfy.sh"
|
||||||
Body = @{
|
Body = ConvertTo-Json -Depth 3 @{
|
||||||
Topic = "wifey"
|
Topic = "wifey"
|
||||||
Message = "Your wife requested you send a picture of yourself."
|
Message = "Your wife requested you send a picture of yourself."
|
||||||
Actions = ConvertTo-Json -Depth 3 @(
|
Actions = @(
|
||||||
@{
|
@{
|
||||||
Action = "broadcast"
|
Action = "broadcast"
|
||||||
Label = "Take picture"
|
Label = "Take picture"
|
||||||
|
@ -1968,7 +2073,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' =>
|
'header' =>
|
||||||
"Content-Type: text/plain\r\n" .
|
"Content-Type: text/plain\r\n" .
|
||||||
"Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
|
'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
|
||||||
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
||||||
]
|
]
|
||||||
]));
|
]));
|
||||||
|
@ -2095,10 +2200,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||||
$Request = @{
|
$Request = @{
|
||||||
Method = "POST"
|
Method = "POST"
|
||||||
URI = "https://ntfy.sh"
|
URI = "https://ntfy.sh"
|
||||||
Body = @{
|
Body = ConvertTo-Json -Depth 3 @{
|
||||||
Topic = "myhome"
|
Topic = "myhome"
|
||||||
Message = "Garage door has been open for 15 minutes. Close it?"
|
Message = "Garage door has been open for 15 minutes. Close it?"
|
||||||
Actions = ConvertTo-Json -Depth 3 @(
|
Actions = @(
|
||||||
@{
|
@{
|
||||||
Action = "http"
|
Action = "http"
|
||||||
Label = "Close door"
|
Label = "Close door"
|
||||||
|
@ -2183,7 +2288,7 @@ You can define which URL to open when a notification is clicked. This may be use
|
||||||
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
|
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
|
||||||
the web browser (or the app) and open the website.
|
the web browser (or the app) and open the website.
|
||||||
|
|
||||||
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`).
|
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its alias `Click`).
|
||||||
If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
|
If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
|
||||||
by another app, the responsible app may open.
|
by another app, the responsible app may open.
|
||||||
|
|
||||||
|
@ -3493,6 +3598,7 @@ table in their canonical form.
|
||||||
| `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) |
|
| `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) |
|
||||||
| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) |
|
| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) |
|
||||||
| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
|
| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
|
||||||
|
| `X-Markdown` | `Markdown`, `md` | Enable [Markdown formatting](#markdown-formatting) in the notification body |
|
||||||
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
|
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
|
||||||
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
|
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
|
||||||
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
|
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
|
||||||
|
@ -3502,3 +3608,4 @@ table in their canonical form.
|
||||||
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |
|
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |
|
||||||
| `X-Poll-ID` | `Poll-ID` | Internal parameter, used for [iOS push notifications](config.md#ios-instant-notifications) |
|
| `X-Poll-ID` | `Poll-ID` | Internal parameter, used for [iOS push notifications](config.md#ios-instant-notifications) |
|
||||||
| `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics |
|
| `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics |
|
||||||
|
| `Content-Type` | - | If set to `text/markdown`, [Markdown formatting](#markdown-formatting) is enabled |
|
||||||
|
|
108
docs/releases.md
|
@ -2,6 +2,85 @@
|
||||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
|
## ntfy server v2.7.0
|
||||||
|
Released August 17, 2023
|
||||||
|
|
||||||
|
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
|
||||||
|
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
|
||||||
|
internationalization support, a CLI auth bug.
|
||||||
|
|
||||||
|
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
|
||||||
|
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
|
||||||
|
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
|
||||||
|
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
|
||||||
|
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||||
|
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
|
||||||
|
**Security:** ⚠️
|
||||||
|
|
||||||
|
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||||
|
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
|
||||||
|
|
||||||
|
## ntfy server v2.6.2
|
||||||
|
Released June 30, 2023
|
||||||
|
|
||||||
|
With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA)
|
||||||
|
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
|
||||||
|
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
|
||||||
|
push notifications, and an app badge with the unread notification count. Note that for self-hosted servers,
|
||||||
|
[Web Push](config.md#web-push) must be configured.
|
||||||
|
|
||||||
|
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
|
||||||
|
|
||||||
|
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
|
||||||
|
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
|
||||||
|
|
||||||
|
❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||||
|
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
|
||||||
|
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
|
||||||
|
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
|
||||||
|
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
|
||||||
|
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
|
||||||
|
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
|
||||||
|
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
|
||||||
|
**Maintenance:**
|
||||||
|
|
||||||
|
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||||
|
|
||||||
|
**Changes in tarball/zip naming:**
|
||||||
|
Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release
|
||||||
|
archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy:
|
||||||
|
|
||||||
|
- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip`
|
||||||
|
- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz`
|
||||||
|
- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz`
|
||||||
|
|
||||||
## ntfy server v2.5.0
|
## ntfy server v2.5.0
|
||||||
Released May 18, 2023
|
Released May 18, 2023
|
||||||
|
|
||||||
|
@ -31,7 +110,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||||
## ntfy server v2.4.0
|
## ntfy server v2.4.0
|
||||||
Released Apr 26, 2023
|
Released Apr 26, 2023
|
||||||
|
|
||||||
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`,
|
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
|
||||||
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
|
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
|
||||||
|
|
||||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||||
|
@ -1194,7 +1273,7 @@ Released Dec 28, 2021
|
||||||
|
|
||||||
**Features & bug fixes:**
|
**Features & bug fixes:**
|
||||||
|
|
||||||
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
|
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
|
||||||
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
||||||
* Fixing the Santa bug #65
|
* Fixing the Santa bug #65
|
||||||
|
|
||||||
|
@ -1204,6 +1283,16 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||||
|
|
||||||
## Not released yet
|
## Not released yet
|
||||||
|
|
||||||
|
### ntfy server v2.8.0 (UNRELEASED)
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
|
||||||
|
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
|
||||||
|
* Add special logic to ignore `Priority` header if it resembled a RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
|
||||||
|
* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||||
|
* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
|
||||||
|
|
||||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
@ -1219,18 +1308,3 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||||
**Additional languages:**
|
**Additional languages:**
|
||||||
|
|
||||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
|
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
|
||||||
|
|
||||||
### ntfy server v2.6.0 (UNRELEASED)
|
|
||||||
|
|
||||||
**Bug fixes:**
|
|
||||||
|
|
||||||
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
|
|
||||||
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
|
|
||||||
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
|
|
||||||
|
|
||||||
**Maintenance:**
|
|
||||||
|
|
||||||
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
|
|
BIN
docs/static/img/cdio-setup.jpg
vendored
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/pwa-badge.png
vendored
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
docs/static/img/pwa-install-chrome-android-menu.jpg
vendored
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/pwa-install-chrome-android-popup.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
docs/static/img/pwa-install-chrome-android.jpg
vendored
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
docs/static/img/pwa-install-firefox-android-menu.jpg
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/pwa-install-firefox-android-popup.jpg
vendored
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
docs/static/img/pwa-install-macos-safari-add-to-dock.png
vendored
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
docs/static/img/pwa-install-safari-ios-add-icon.jpg
vendored
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/pwa-install-safari-ios-button.jpg
vendored
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
docs/static/img/pwa-install-safari-ios-menu.jpg
vendored
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
docs/static/img/pwa-install.png
vendored
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
docs/static/img/pwa.png
vendored
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
docs/static/img/web-markdown.png
vendored
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
docs/static/img/web-pin.png
vendored
Before Width: | Height: | Size: 18 KiB |
BIN
docs/static/img/web-subscribe.png
vendored
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 96 KiB |
|
@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c
|
||||||
## Install + configure
|
## Install + configure
|
||||||
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
|
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
|
||||||
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
|
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
|
||||||
by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You
|
by creating `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user). You
|
||||||
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
|
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
|
||||||
|
|
||||||
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
|
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
|
||||||
|
|
|
@ -12,6 +12,9 @@ You can get the Android app from both [Google Play](https://play.google.com/stor
|
||||||
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
|
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
|
||||||
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||||
|
|
||||||
|
Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app.
|
||||||
|
The PWA is a website that you can add to your home screen, and it will behave just like a native app.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
|
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
|
||||||
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.
|
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.
|
||||||
|
|
69
docs/subscribe/pwa.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Using the progressive web app (PWA)
|
||||||
|
While ntfy doesn't have a native desktop app, it is built as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA)
|
||||||
|
and thus can be **installed on both desktop and mobile devices**.
|
||||||
|
|
||||||
|
This gives it its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux, home screen icon on iOS, and
|
||||||
|
launcher icon on Android), a standalone window, push notifications, and an app badge with the unread notification count.
|
||||||
|
|
||||||
|
Web app installation is **supported on** (see [compatibility table](https://caniuse.com/web-app-manifest) for details):
|
||||||
|
|
||||||
|
- **Chrome:** Android, Windows, Linux, macOS
|
||||||
|
- **Safari:** iOS 16.4+, macOS 14+
|
||||||
|
- **Firefox:** Android, as well as on Windows/Linux [via an extension](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/)
|
||||||
|
- **Edge:** Windows
|
||||||
|
|
||||||
|
Note that for self-hosted servers, [Web Push](../config.md#web-push) must be configured for the PWA to work.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Chrome on Desktop
|
||||||
|
To install and register the web app via Chrome, click the "install app" icon. After installation, you can find the app in your
|
||||||
|
app drawer:
|
||||||
|
|
||||||
|
<div id="pwa-screenshots-chrome-safari-desktop" class="screenshots">
|
||||||
|
<a href="../../static/img/pwa-install.png"><img src="../../static/img/pwa-install.png"/></a>
|
||||||
|
<a href="../../static/img/pwa.png"><img src="../../static/img/pwa.png"/></a>
|
||||||
|
<a href="../../static/img/pwa-badge.png"><img src="../../static/img/pwa-badge.png"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Safari on macOS
|
||||||
|
To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.
|
||||||
|
|
||||||
|
<div id="pwa-screenshots-safari-desktop" class="screenshots">
|
||||||
|
<a href="../../static/img/pwa-install-macos-safari-add-to-dock.png"><img src="../../static/img/pwa-install-macos-safari-add-to-dock.png"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Chrome/Firefox on Android
|
||||||
|
For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
|
||||||
|
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
|
||||||
|
and on your home screen.
|
||||||
|
|
||||||
|
<div id="pwa-screenshots-chrome-android" class="screenshots">
|
||||||
|
<a href="../../static/img/pwa-install-chrome-android.jpg"><img src="../../static/img/pwa-install-chrome-android.jpg"/></a>
|
||||||
|
<a href="../../static/img/pwa-install-chrome-android-menu.jpg"><img src="../../static/img/pwa-install-chrome-android-menu.jpg"/></a>
|
||||||
|
<a href="../../static/img/pwa-install-chrome-android-popup.jpg"><img src="../../static/img/pwa-install-chrome-android-popup.jpg"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
For Firefox, select "Install" in the menu, and then click "Add" to add an icon to your home screen:
|
||||||
|
|
||||||
|
<div id="pwa-screenshots-firefox-android" class="screenshots">
|
||||||
|
<a href="../../static/img/pwa-install-firefox-android-menu.jpg"><img src="../../static/img/pwa-install-firefox-android-menu.jpg"/></a>
|
||||||
|
<a href="../../static/img/pwa-install-firefox-android-popup.jpg"><img src="../../static/img/pwa-install-firefox-android-popup.jpg"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Safari on iOS
|
||||||
|
On iOS Safari, tap on the Share menu, then tap "Add to Home Screen":
|
||||||
|
|
||||||
|
<div id="pwa-screenshots-safari-ios" class="screenshots">
|
||||||
|
<a href="../../static/img/pwa-install-safari-ios-button.jpg"><img src="../../static/img/pwa-install-safari-ios-button.jpg"/></a>
|
||||||
|
<a href="../../static/img/pwa-install-safari-ios-menu.jpg"><img src="../../static/img/pwa-install-safari-ios-menu.jpg"/></a>
|
||||||
|
<a href="../../static/img/pwa-install-safari-ios-add-icon.jpg"><img src="../../static/img/pwa-install-safari-ios-add-icon.jpg"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Background notifications
|
||||||
|
Background notifications via web push are enabled by default and cannot be turned off when the app is installed, as notifications would
|
||||||
|
not be delivered reliably otherwise. You can mute topics you don't want to receive notifications for.
|
||||||
|
|
||||||
|
On desktop, you generally need either your browser or the web app open to receive notifications, though the ntfy tab doesn't need to be
|
||||||
|
open. On mobile, you don't need to have the web app open to receive notifications. Look at the [web docs](./web.md#background-notifications)
|
||||||
|
for a detailed breakdown.
|
|
@ -1,27 +1,75 @@
|
||||||
# Subscribe from the Web UI
|
# Subscribe from the web app
|
||||||
You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, **notifications will
|
The web app lets you subscribe and publish messages to ntfy topics. For ntfy.sh, the web app is available at [ntfy.sh/app](https://ntfy.sh/app).
|
||||||
pop up as desktop notifications**. Simply type in the topic name and click the *Subscribe* button. The browser will
|
To subscribe, simply type in the topic name and click the *Subscribe* button. **After subscribing, messages published to the topic
|
||||||
keep a connection open and listen for incoming notifications.
|
will appear in the web app, and pop up as a notification.**
|
||||||
|
|
||||||
|
<div id="subscribe-screenshots" class="screenshots">
|
||||||
|
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Publish messages
|
||||||
To learn how to send messages, check out the [publishing page](../publish.md).
|
To learn how to send messages, check out the [publishing page](../publish.md).
|
||||||
|
|
||||||
<div id="web-screenshots" class="screenshots">
|
<div id="web-screenshots" class="screenshots">
|
||||||
<a href="../../static/img/web-detail.png"><img src="../../static/img/web-detail.png"/></a>
|
<a href="../../static/img/web-detail.png"><img src="../../static/img/web-detail.png"/></a>
|
||||||
<a href="../../static/img/web-notification.png"><img src="../../static/img/web-notification.png"/></a>
|
<a href="../../static/img/web-notification.png"><img src="../../static/img/web-notification.png"/></a>
|
||||||
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend,
|
## Topic reservations
|
||||||
is to pin the tab so that it's always open, but sort of out of the way:
|
|
||||||
|
|
||||||
<figure markdown>
|
|
||||||
{ width=500 }
|
|
||||||
<figcaption>Pin web app to move it out of the way</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
If topic reservations are enabled, you can claim ownership over topics and define access to it:
|
If topic reservations are enabled, you can claim ownership over topics and define access to it:
|
||||||
|
|
||||||
<div id="reserve-screenshots" class="screenshots">
|
<div id="reserve-screenshots" class="screenshots">
|
||||||
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
|
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
|
||||||
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
|
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Notification features and browser support
|
||||||
|
|
||||||
|
- Emoji tags are supported in all browsers
|
||||||
|
|
||||||
|
- [Click](../publish.md#click-action) actions are supported in all browsers
|
||||||
|
|
||||||
|
- Only Chrome, Edge, and Opera support displaying view and http [actions](../publish.md#action-buttons) in notifications.
|
||||||
|
|
||||||
|
Their presentation is platform specific.
|
||||||
|
|
||||||
|
Note that HTTP actions are performed using fetch and thus are limited to the [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||||
|
rules, which means that any URL you include needs to respond to a [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)
|
||||||
|
with headers allowing the origin of the ntfy web app (`Access-Control-Allow-Origin: https://ntfy.sh`) or `*`.
|
||||||
|
|
||||||
|
- Only Chrome, Edge, and Opera support displaying [images](../publish.md#attachments) in notifications.
|
||||||
|
|
||||||
|
Look at the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility)
|
||||||
|
for more info.
|
||||||
|
|
||||||
|
## Background notifications
|
||||||
|
While subscribing, you have the option to enable background notifications on supported browsers (see "Settings" tab).
|
||||||
|
|
||||||
|
Note: If you add the web app to your homescreen (as a progressive web app, more info in the [installed web app](pwa.md)
|
||||||
|
docs), you cannot turn these off, as notifications would not be delivered reliably otherwise. You can mute topics you don't want to receive
|
||||||
|
notifications for.
|
||||||
|
|
||||||
|
**If background notifications are off:** This requires an active ntfy tab to be open to receive notifications.
|
||||||
|
These are typically instantaneous, and will appear as a system notification. If you don't see these, check that your browser
|
||||||
|
is allowed to show notifications (for example in System Settings on macOS). If you don't want to enable background notifications,
|
||||||
|
**pinning the ntfy tab on your browser** is a good solution to leave it running.
|
||||||
|
|
||||||
|
**If background notifications are on:** This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active
|
||||||
|
ntfy tab open, but in some cases you may need to keep your browser open. Background notifications are only supported on the
|
||||||
|
same server hosting the web app. You cannot use another server, but can instead subscribe on the other server itself.
|
||||||
|
|
||||||
|
If the ntfy app is not opened for more than a week, background notifications will be paused. You can resume them
|
||||||
|
by opening the app again, and will get a warning notification before they are paused.
|
||||||
|
|
||||||
|
| Browser | Platform | Browser Running | Browser Not Running | Restrictions |
|
||||||
|
|---------|----------|-----------------|---------------------|---------------------------------------------------------|
|
||||||
|
| Chrome | Desktop | ✅ | ❌ | |
|
||||||
|
| Firefox | Desktop | ✅ | ❌ | |
|
||||||
|
| Edge | Desktop | ✅ | ❌ | |
|
||||||
|
| Opera | Desktop | ✅ | ❌ | |
|
||||||
|
| Safari | Desktop | ✅ | ✅ | requires Safari 16.1, macOS 13 Ventura |
|
||||||
|
| Chrome | Android | ✅ | ✅ | |
|
||||||
|
| Firefox | Android | ✅ | ✅ | |
|
||||||
|
| Safari | iOS | ⚠️ | ⚠️ | requires iOS 16.4, only when app is added to homescreen |
|
||||||
|
|
||||||
|
(Browsers below 1% usage not shown, look at the [Push API](https://caniuse.com/push-api) for more info)
|
||||||
|
|
89
go.mod
|
@ -1,74 +1,79 @@
|
||||||
module heckel.io/ntfy
|
module git.zio.sh/astra/ntfy/v2
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.9.0 // indirect
|
cloud.google.com/go/firestore v1.14.0 // indirect
|
||||||
cloud.google.com/go/storage v1.30.1 // indirect
|
cloud.google.com/go/storage v1.34.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||||
github.com/emersion/go-smtp v0.16.0
|
github.com/emersion/go-smtp v0.18.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2
|
github.com/gabriel-vasile/mimetype v1.4.3
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.18
|
||||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
|
github.com/olebedev/when v1.0.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/urfave/cli/v2 v2.25.3
|
github.com/urfave/cli/v2 v2.25.7
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/oauth2 v0.8.0 // indirect
|
golang.org/x/oauth2 v0.13.0 // indirect
|
||||||
golang.org/x/sync v0.2.0
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/term v0.8.0
|
golang.org/x/term v0.13.0
|
||||||
golang.org/x/time v0.3.0
|
golang.org/x/time v0.4.0
|
||||||
google.golang.org/api v0.122.0
|
google.golang.org/api v0.149.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839
|
||||||
|
|
||||||
require github.com/pkg/errors v0.9.1 // indirect
|
require github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
firebase.google.com/go/v4 v4.11.0
|
firebase.google.com/go/v4 v4.12.1
|
||||||
github.com/prometheus/client_golang v1.15.1
|
github.com/SherClockHolmes/webpush-go v1.3.0
|
||||||
github.com/stripe/stripe-go/v74 v74.18.0
|
github.com/prometheus/client_golang v1.17.0
|
||||||
|
github.com/stripe/stripe-go/v74 v74.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.110.2 // indirect
|
cloud.google.com/go v0.110.10 // indirect
|
||||||
cloud.google.com/go/compute v1.19.3 // indirect
|
cloud.google.com/go/compute v1.23.3 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
cloud.google.com/go/iam v1.0.1 // indirect
|
cloud.google.com/go/iam v1.1.5 // indirect
|
||||||
cloud.google.com/go/longrunning v0.4.2 // indirect
|
cloud.google.com/go/longrunning v0.5.4 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
github.com/google/s2a-go v0.1.3 // indirect
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.4.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.43.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.9.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/net v0.10.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.14.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/appengine/v2 v2.0.3 // indirect
|
google.golang.org/appengine/v2 v2.0.5 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||||
google.golang.org/grpc v1.55.0 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||||
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
206
go.sum
|
@ -1,44 +1,38 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
||||||
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
|
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
|
||||||
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
|
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||||
cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
|
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||||
cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
|
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
|
cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw=
|
||||||
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
|
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
|
||||||
cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU=
|
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
||||||
cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8=
|
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||||
cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE=
|
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
|
||||||
cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ=
|
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
|
||||||
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
|
cloud.google.com/go/storage v1.34.1 h1:H2Af2dU5J0PF7A5B+ECFIce+RqxVnrVilO+cu0TS3MI=
|
||||||
cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
|
cloud.google.com/go/storage v1.34.1/go.mod h1:VN1ElqqvR9adg1k9xlkUJ55cMOP1/QjnNNuT5xQL6dY=
|
||||||
firebase.google.com/go/v4 v4.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk=
|
firebase.google.com/go/v4 v4.12.1 h1:tDNvobifGsx/1HSFLnM0fmNfx/CDZSgsTO2KhZtgpcs=
|
||||||
firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
firebase.google.com/go/v4 v4.12.1/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
||||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
|
||||||
|
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
@ -46,17 +40,16 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
|
||||||
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
@ -66,16 +59,13 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
@ -88,44 +78,41 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
||||||
github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
|
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||||
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||||
github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
|
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||||
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
|
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
|
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
|
||||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us=
|
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||||
github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc=
|
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
@ -133,86 +120,86 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw=
|
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
||||||
github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||||
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
|
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||||
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
||||||
|
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
@ -220,35 +207,35 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
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=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY=
|
||||||
google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
|
google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
|
||||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
google.golang.org/appengine/v2 v2.0.3 h1:AyY/mipuqiyCIAqOevfmu5fMDc5/9P/QggWfCQYdkSA=
|
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
|
||||||
google.golang.org/appengine/v2 v2.0.3/go.mod h1:2Z0TTdcXxnHdXzmp8drrmOExUDM2WQgyT33c6JDUlJM=
|
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 h1:HJMDndgxest5n2y77fnErkM62iUsptE/H8p0dC2Huo4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
|
||||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
@ -260,12 +247,11 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
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=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -3,7 +3,7 @@ package log
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
2
main.go
|
@ -2,8 +2,8 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/cmd"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/cmd"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
|
@ -82,6 +82,7 @@ nav:
|
||||||
- "Subscribing":
|
- "Subscribing":
|
||||||
- "From your phone": subscribe/phone.md
|
- "From your phone": subscribe/phone.md
|
||||||
- "From the Web app": subscribe/web.md
|
- "From the Web app": subscribe/web.md
|
||||||
|
- "From the Desktop": subscribe/pwa.md
|
||||||
- "From the CLI": subscribe/cli.md
|
- "From the CLI": subscribe/cli.md
|
||||||
- "Using the API": subscribe/api.md
|
- "Using the API": subscribe/api.md
|
||||||
- "Self-hosting":
|
- "Self-hosting":
|
||||||
|
|
|
@ -25,9 +25,9 @@ elif [[ "$1" == *.md ]]; then
|
||||||
|
|
||||||
<!-- This file was generated by scripts/emoji-convert.sh -->
|
<!-- This file was generated by scripts/emoji-convert.sh -->
|
||||||
|
|
||||||
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||||
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
||||||
[tagging and emojis page](../publish/#tags-emojis).
|
[tagging and emojis page](publish.md#tags-emojis).
|
||||||
|
|
||||||
<table class=\"remove-md-box emoji-table\"><tr>
|
<table class=\"remove-md-box emoji-table\"><tr>
|
||||||
" > "$1"
|
" > "$1"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines default config settings (excluding limits, see below)
|
// Defines default config settings (excluding limits, see below)
|
||||||
|
@ -22,6 +23,12 @@ const (
|
||||||
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines default Web Push settings
|
||||||
|
const (
|
||||||
|
DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour
|
||||||
|
DefaultWebPushExpiryDuration = 9 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
// Defines all global and per-visitor limits
|
// Defines all global and per-visitor limits
|
||||||
// - message size limit: the max number of bytes for a message
|
// - message size limit: the max number of bytes for a message
|
||||||
// - total topic limit: max number of topics overall
|
// - total topic limit: max number of topics overall
|
||||||
|
@ -146,6 +153,13 @@ type Config struct {
|
||||||
EnableMetrics bool
|
EnableMetrics bool
|
||||||
AccessControlAllowOrigin string // CORS header field to restrict access from web clients
|
AccessControlAllowOrigin string // CORS header field to restrict access from web clients
|
||||||
Version string // injected by App
|
Version string // injected by App
|
||||||
|
WebPushPrivateKey string
|
||||||
|
WebPushPublicKey string
|
||||||
|
WebPushFile string
|
||||||
|
WebPushEmailAddress string
|
||||||
|
WebPushStartupQueries string
|
||||||
|
WebPushExpiryDuration time.Duration
|
||||||
|
WebPushExpiryWarningDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig instantiates a default new server config
|
// NewConfig instantiates a default new server config
|
||||||
|
@ -227,5 +241,11 @@ func NewConfig() *Config {
|
||||||
EnableReservations: false,
|
EnableReservations: false,
|
||||||
AccessControlAllowOrigin: "*",
|
AccessControlAllowOrigin: "*",
|
||||||
Version: "",
|
Version: "",
|
||||||
|
WebPushPrivateKey: "",
|
||||||
|
WebPushPublicKey: "",
|
||||||
|
WebPushFile: "",
|
||||||
|
WebPushEmailAddress: "",
|
||||||
|
WebPushExpiryDuration: DefaultWebPushExpiryDuration,
|
||||||
|
WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package server_test
|
package server_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/server"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -114,6 +114,9 @@ var (
|
||||||
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||||
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||||
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil}
|
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil}
|
||||||
|
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
|
||||||
|
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
|
||||||
|
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||||
|
@ -138,5 +141,6 @@ var (
|
||||||
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil}
|
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil}
|
||||||
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil}
|
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil}
|
||||||
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil}
|
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil}
|
||||||
|
errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil}
|
||||||
errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil}
|
errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil}
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,8 +3,8 @@ package server
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
|
@ -3,8 +3,8 @@ package server
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
|
@ -2,10 +2,10 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
@ -29,6 +29,7 @@ const (
|
||||||
tagResetter = "resetter"
|
tagResetter = "resetter"
|
||||||
tagWebsocket = "websocket"
|
tagWebsocket = "websocket"
|
||||||
tagMatrix = "matrix"
|
tagMatrix = "matrix"
|
||||||
|
tagWebPush = "webpush"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -45,6 +45,7 @@ const (
|
||||||
attachment_deleted INT NOT NULL,
|
attachment_deleted INT NOT NULL,
|
||||||
sender TEXT NOT NULL,
|
sender TEXT NOT NULL,
|
||||||
user TEXT NOT NULL,
|
user TEXT NOT NULL,
|
||||||
|
content_type TEXT NOT NULL,
|
||||||
encoding TEXT NOT NULL,
|
encoding TEXT NOT NULL,
|
||||||
published INT NOT NULL
|
published INT NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -63,43 +64,43 @@ const (
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
insertMessageQuery = `
|
insertMessageQuery = `
|
||||||
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, encoding, published)
|
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
|
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
|
||||||
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
|
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
|
||||||
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
|
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
|
||||||
selectMessagesByIDQuery = `
|
selectMessagesByIDQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE mid = ?
|
WHERE mid = ?
|
||||||
`
|
`
|
||||||
selectMessagesSinceTimeQuery = `
|
selectMessagesSinceTimeQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ? AND published = 1
|
WHERE topic = ? AND time >= ? AND published = 1
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesSinceTimeIncludeScheduledQuery = `
|
selectMessagesSinceTimeIncludeScheduledQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ?
|
WHERE topic = ? AND time >= ?
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesSinceIDQuery = `
|
selectMessagesSinceIDQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND id > ? AND published = 1
|
WHERE topic = ? AND id > ? AND published = 1
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesSinceIDIncludeScheduledQuery = `
|
selectMessagesSinceIDIncludeScheduledQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND (id > ? OR published = 0)
|
WHERE topic = ? AND (id > ? OR published = 0)
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesDueQuery = `
|
selectMessagesDueQuery = `
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE time <= ? AND published = 0
|
WHERE time <= ? AND published = 0
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
|
@ -121,7 +122,7 @@ const (
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
const (
|
const (
|
||||||
currentSchemaVersion = 11
|
currentSchemaVersion = 12
|
||||||
createSchemaVersionTableQuery = `
|
createSchemaVersionTableQuery = `
|
||||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
|
@ -240,6 +241,11 @@ const (
|
||||||
);
|
);
|
||||||
INSERT INTO stats (key, value) VALUES ('messages', 0);
|
INSERT INTO stats (key, value) VALUES ('messages', 0);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// 11 -> 12
|
||||||
|
migrate11To12AlterMessagesTableQuery = `
|
||||||
|
ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT('');
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -255,6 +261,7 @@ var (
|
||||||
8: migrateFrom8,
|
8: migrateFrom8,
|
||||||
9: migrateFrom9,
|
9: migrateFrom9,
|
||||||
10: migrateFrom10,
|
10: migrateFrom10,
|
||||||
|
11: migrateFrom11,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -270,7 +277,7 @@ func newSqliteCache(filename, startupQueries string, cacheDuration time.Duration
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := setupDB(db, startupQueries, cacheDuration); err != nil {
|
if err := setupMessagesDB(db, startupQueries, cacheDuration); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var queue *util.BatchingQueue[*message]
|
var queue *util.BatchingQueue[*message]
|
||||||
|
@ -384,6 +391,7 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||||
attachmentDeleted, // Always zero
|
attachmentDeleted, // Always zero
|
||||||
sender,
|
sender,
|
||||||
m.User,
|
m.User,
|
||||||
|
m.ContentType,
|
||||||
m.Encoding,
|
m.Encoding,
|
||||||
published,
|
published,
|
||||||
)
|
)
|
||||||
|
@ -656,7 +664,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||||
func readMessage(rows *sql.Rows) (*message, error) {
|
func readMessage(rows *sql.Rows) (*message, error) {
|
||||||
var timestamp, expires, attachmentSize, attachmentExpires int64
|
var timestamp, expires, attachmentSize, attachmentExpires int64
|
||||||
var priority int
|
var priority int
|
||||||
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, encoding string
|
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&id,
|
&id,
|
||||||
×tamp,
|
×tamp,
|
||||||
|
@ -676,6 +684,7 @@ func readMessage(rows *sql.Rows) (*message, error) {
|
||||||
&attachmentURL,
|
&attachmentURL,
|
||||||
&sender,
|
&sender,
|
||||||
&user,
|
&user,
|
||||||
|
&contentType,
|
||||||
&encoding,
|
&encoding,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -706,22 +715,23 @@ func readMessage(rows *sql.Rows) (*message, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &message{
|
return &message{
|
||||||
ID: id,
|
ID: id,
|
||||||
Time: timestamp,
|
Time: timestamp,
|
||||||
Expires: expires,
|
Expires: expires,
|
||||||
Event: messageEvent,
|
Event: messageEvent,
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
Title: title,
|
Title: title,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Click: click,
|
Click: click,
|
||||||
Icon: icon,
|
Icon: icon,
|
||||||
Actions: actions,
|
Actions: actions,
|
||||||
Attachment: att,
|
Attachment: att,
|
||||||
Sender: senderIP, // Must parse assuming database must be correct
|
Sender: senderIP, // Must parse assuming database must be correct
|
||||||
User: user,
|
User: user,
|
||||||
Encoding: encoding,
|
ContentType: contentType,
|
||||||
|
Encoding: encoding,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -749,7 +759,7 @@ func (c *messageCache) Close() error {
|
||||||
return c.db.Close()
|
return c.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error {
|
func setupMessagesDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error {
|
||||||
// Run startup queries
|
// Run startup queries
|
||||||
if startupQueries != "" {
|
if startupQueries != "" {
|
||||||
if _, err := db.Exec(startupQueries); err != nil {
|
if _, err := db.Exec(startupQueries); err != nil {
|
||||||
|
@ -929,7 +939,7 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateFrom10(db *sql.DB, cacheDuration time.Duration) error {
|
func migrateFrom10(db *sql.DB, _ time.Duration) error {
|
||||||
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11")
|
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11")
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -944,3 +954,19 @@ func migrateFrom10(db *sql.DB, cacheDuration time.Duration) error {
|
||||||
}
|
}
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateFrom11(db *sql.DB, _ time.Duration) error {
|
||||||
|
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 11 to 12")
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
if _, err := tx.Exec(migrate11To12AlterMessagesTableQuery); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(updateSchemaVersion, 12); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
|
@ -9,13 +9,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -32,6 +25,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is the main server, providing the UI and API for ntfy
|
// Server is the main server, providing the UI and API for ntfy
|
||||||
|
@ -52,6 +53,7 @@ type Server struct {
|
||||||
messagesHistory []int64 // Last n values of the messages counter, used to determine rate
|
messagesHistory []int64 // Last n values of the messages counter, used to determine rate
|
||||||
userManager *user.Manager // Might be nil!
|
userManager *user.Manager // Might be nil!
|
||||||
messageCache *messageCache // Database that stores the messages
|
messageCache *messageCache // Database that stores the messages
|
||||||
|
webPush *webPushStore // Database that stores web push subscriptions
|
||||||
fileCache *fileCache // File system based cache that stores attachments
|
fileCache *fileCache // File system based cache that stores attachments
|
||||||
stripe stripeAPI // Stripe API, can be replaced with a mock
|
stripe stripeAPI // Stripe API, can be replaced with a mock
|
||||||
priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!)
|
priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!)
|
||||||
|
@ -76,11 +78,15 @@ var (
|
||||||
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
||||||
|
|
||||||
webConfigPath = "/config.js"
|
webConfigPath = "/config.js"
|
||||||
|
webManifestPath = "/manifest.webmanifest"
|
||||||
|
webRootHTMLPath = "/app.html"
|
||||||
|
webServiceWorkerPath = "/sw.js"
|
||||||
accountPath = "/account"
|
accountPath = "/account"
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
metricsPath = "/metrics"
|
metricsPath = "/metrics"
|
||||||
apiHealthPath = "/v1/health"
|
apiHealthPath = "/v1/health"
|
||||||
apiStatsPath = "/v1/stats"
|
apiStatsPath = "/v1/stats"
|
||||||
|
apiWebPushPath = "/v1/webpush"
|
||||||
apiTiersPath = "/v1/tiers"
|
apiTiersPath = "/v1/tiers"
|
||||||
apiUsersPath = "/v1/users"
|
apiUsersPath = "/v1/users"
|
||||||
apiUsersAccessPath = "/v1/users/access"
|
apiUsersAccessPath = "/v1/users/access"
|
||||||
|
@ -151,6 +157,13 @@ func New(conf *Config) (*Server, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var webPush *webPushStore
|
||||||
|
if conf.WebPushPublicKey != "" {
|
||||||
|
webPush, err = newWebPushStore(conf.WebPushFile, conf.WebPushStartupQueries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
topics, err := messageCache.Topics()
|
topics, err := messageCache.Topics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -190,6 +203,7 @@ func New(conf *Config) (*Server, error) {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
config: conf,
|
config: conf,
|
||||||
messageCache: messageCache,
|
messageCache: messageCache,
|
||||||
|
webPush: webPush,
|
||||||
fileCache: fileCache,
|
fileCache: fileCache,
|
||||||
firebaseClient: firebaseClient,
|
firebaseClient: firebaseClient,
|
||||||
smtpSender: mailer,
|
smtpSender: mailer,
|
||||||
|
@ -342,6 +356,9 @@ func (s *Server) closeDatabases() {
|
||||||
s.userManager.Close()
|
s.userManager.Close()
|
||||||
}
|
}
|
||||||
s.messageCache.Close()
|
s.messageCache.Close()
|
||||||
|
if s.webPush != nil {
|
||||||
|
s.webPush.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle is the main entry point for all HTTP requests
|
// handle is the main entry point for all HTTP requests
|
||||||
|
@ -416,6 +433,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.handleHealth(w, r, v)
|
return s.handleHealth(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||||
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
|
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
|
||||||
|
} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
|
||||||
|
return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
|
||||||
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
|
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
|
||||||
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
|
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
|
||||||
|
@ -470,6 +489,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v)
|
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v)
|
||||||
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
|
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
|
||||||
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v)
|
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v)
|
||||||
|
} else if r.Method == http.MethodPost && apiWebPushPath == r.URL.Path {
|
||||||
|
return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushUpdate))(w, r, v)
|
||||||
|
} else if r.Method == http.MethodDelete && apiWebPushPath == r.URL.Path {
|
||||||
|
return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushDelete))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
|
||||||
return s.handleStats(w, r, v)
|
return s.handleStats(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
|
||||||
|
@ -478,7 +501,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.handleMatrixDiscovery(w)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
||||||
return s.handleMetrics(w, r, v)
|
return s.handleMetrics(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) {
|
||||||
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
||||||
|
@ -552,7 +575,9 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
EnableCalls: s.config.TwilioAccount != "",
|
EnableCalls: s.config.TwilioAccount != "",
|
||||||
EnableEmails: s.config.SMTPSenderFrom != "",
|
EnableEmails: s.config.SMTPSenderFrom != "",
|
||||||
EnableReservations: s.config.EnableReservations,
|
EnableReservations: s.config.EnableReservations,
|
||||||
|
EnableWebPush: s.config.WebPushPublicKey != "",
|
||||||
BillingContact: s.config.BillingContact,
|
BillingContact: s.config.BillingContact,
|
||||||
|
WebPushPublicKey: s.config.WebPushPublicKey,
|
||||||
DisallowedTopics: s.config.DisallowedTopics,
|
DisallowedTopics: s.config.DisallowedTopics,
|
||||||
}
|
}
|
||||||
b, err := json.MarshalIndent(response, "", " ")
|
b, err := json.MarshalIndent(response, "", " ")
|
||||||
|
@ -564,6 +589,25 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleWebManifest serves the web app manifest for the progressive web app (PWA)
|
||||||
|
func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
|
response := &webManifestResponse{
|
||||||
|
Name: "ntfy web",
|
||||||
|
Description: "ntfy lets you send push notifications via scripts from any computer or phone",
|
||||||
|
ShortName: "ntfy",
|
||||||
|
Scope: "/",
|
||||||
|
StartURL: s.config.WebRoot,
|
||||||
|
Display: "standalone",
|
||||||
|
BackgroundColor: "#ffffff",
|
||||||
|
ThemeColor: "#317f6f",
|
||||||
|
Icons: []*webManifestIcon{
|
||||||
|
{SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"},
|
||||||
|
{SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.writeJSONWithContentType(w, response, "application/manifest+json")
|
||||||
|
}
|
||||||
|
|
||||||
// handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set,
|
// handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set,
|
||||||
// and listen-metrics-http is not set.
|
// and listen-metrics-http is not set.
|
||||||
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
|
@ -763,6 +807,9 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
||||||
if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
|
if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
|
||||||
go s.forwardPollRequest(v, m)
|
go s.forwardPollRequest(v, m)
|
||||||
}
|
}
|
||||||
|
if s.config.WebPushPublicKey != "" {
|
||||||
|
go s.publishToWebPushEndpoints(v, m)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later")
|
logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later")
|
||||||
}
|
}
|
||||||
|
@ -963,6 +1010,10 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||||
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
||||||
|
if markdown || strings.ToLower(contentType) == "text/markdown" {
|
||||||
|
m.ContentType = "text/markdown"
|
||||||
|
}
|
||||||
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
||||||
if unifiedpush {
|
if unifiedpush {
|
||||||
firebase = false
|
firebase = false
|
||||||
|
@ -1692,6 +1743,9 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
||||||
if s.config.UpstreamBaseURL != "" {
|
if s.config.UpstreamBaseURL != "" {
|
||||||
go s.forwardPollRequest(v, m)
|
go s.forwardPollRequest(v, m)
|
||||||
}
|
}
|
||||||
|
if s.config.WebPushPublicKey != "" {
|
||||||
|
go s.publishToWebPushEndpoints(v, m)
|
||||||
|
}
|
||||||
if err := s.messageCache.MarkPublished(m); err != nil {
|
if err := s.messageCache.MarkPublished(m); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1735,6 +1789,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||||
if m.Icon != "" {
|
if m.Icon != "" {
|
||||||
r.Header.Set("X-Icon", m.Icon)
|
r.Header.Set("X-Icon", m.Icon)
|
||||||
}
|
}
|
||||||
|
if m.Markdown {
|
||||||
|
r.Header.Set("X-Markdown", "yes")
|
||||||
|
}
|
||||||
if len(m.Actions) > 0 {
|
if len(m.Actions) > 0 {
|
||||||
actionsStr, err := json.Marshal(m.Actions)
|
actionsStr, err := json.Marshal(m.Actions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1912,7 +1969,11 @@ func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) writeJSON(w http.ResponseWriter, v any) error {
|
func (s *Server) writeJSON(w http.ResponseWriter, v any) error {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
return s.writeJSONWithContentType(w, v, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeJSONWithContentType(w http.ResponseWriter, v any, contentType string) error {
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -144,6 +144,27 @@
|
||||||
# smtp-server-domain:
|
# smtp-server-domain:
|
||||||
# smtp-server-addr-prefix:
|
# smtp-server-addr-prefix:
|
||||||
|
|
||||||
|
# Web Push support (background notifications for browsers)
|
||||||
|
#
|
||||||
|
# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users
|
||||||
|
# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push
|
||||||
|
# endpoint, which will then forward it to the browser.
|
||||||
|
#
|
||||||
|
# You must configure web-push-public/private key, web-push-file, and web-push-email-address below to enable Web Push.
|
||||||
|
# Run "ntfy webpush keys" to generate the keys.
|
||||||
|
#
|
||||||
|
# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
|
||||||
|
# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
|
||||||
|
# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
|
||||||
|
# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com`
|
||||||
|
# - web-push-startup-queries is an optional list of queries to run on startup`
|
||||||
|
#
|
||||||
|
# web-push-public-key:
|
||||||
|
# web-push-private-key:
|
||||||
|
# web-push-file:
|
||||||
|
# web-push-email-address:
|
||||||
|
# web-push-startup-queries:
|
||||||
|
|
||||||
# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
|
# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
|
||||||
#
|
#
|
||||||
# - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
|
# - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
|
||||||
|
@ -321,6 +342,10 @@
|
||||||
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
|
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
|
||||||
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
|
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
|
||||||
#
|
#
|
||||||
|
# Check your permissions:
|
||||||
|
# If you are running ntfy with systemd, make sure this log file is owned by the
|
||||||
|
# ntfy user and group by running: chown ntfy.ntfy <filename>.
|
||||||
|
#
|
||||||
# Example (good for production):
|
# Example (good for production):
|
||||||
# log-level: info
|
# log-level: info
|
||||||
# log-format: json
|
# log-format: json
|
||||||
|
|
|
@ -2,9 +2,9 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/user"
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -170,6 +170,11 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
|
||||||
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
|
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
|
||||||
return errHTTPBadRequestIncorrectPasswordConfirmation
|
return errHTTPBadRequestIncorrectPasswordConfirmation
|
||||||
}
|
}
|
||||||
|
if s.webPush != nil && u.ID != "" {
|
||||||
|
if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil {
|
||||||
|
logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
if u.Billing.StripeSubscriptionID != "" {
|
if u.Billing.StripeSubscriptionID != "" {
|
||||||
logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name)
|
logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name)
|
||||||
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {
|
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {
|
||||||
|
|
|
@ -2,10 +2,10 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"heckel.io/ntfy/user"
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
firebase "firebase.google.com/go/v4"
|
firebase "firebase.google.com/go/v4"
|
||||||
"firebase.google.com/go/v4/messaging"
|
"firebase.google.com/go/v4/messaging"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,17 +144,18 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
|
||||||
}
|
}
|
||||||
if allowForward {
|
if allowForward {
|
||||||
data = map[string]string{
|
data = map[string]string{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"time": fmt.Sprintf("%d", m.Time),
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
"event": m.Event,
|
"event": m.Event,
|
||||||
"topic": m.Topic,
|
"topic": m.Topic,
|
||||||
"priority": fmt.Sprintf("%d", m.Priority),
|
"priority": fmt.Sprintf("%d", m.Priority),
|
||||||
"tags": strings.Join(m.Tags, ","),
|
"tags": strings.Join(m.Tags, ","),
|
||||||
"click": m.Click,
|
"click": m.Click,
|
||||||
"icon": m.Icon,
|
"icon": m.Icon,
|
||||||
"title": m.Title,
|
"title": m.Title,
|
||||||
"message": m.Message,
|
"message": m.Message,
|
||||||
"encoding": m.Encoding,
|
"content_type": m.ContentType,
|
||||||
|
"encoding": m.Encoding,
|
||||||
}
|
}
|
||||||
if len(m.Actions) > 0 {
|
if len(m.Actions) > 0 {
|
||||||
actions, err := json.Marshal(m.Actions)
|
actions, err := json.Marshal(m.Actions)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/user"
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -182,6 +182,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||||
"title": "some title",
|
"title": "some title",
|
||||||
"message": "this is a message",
|
"message": "this is a message",
|
||||||
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||||
|
"content_type": "",
|
||||||
"encoding": "",
|
"encoding": "",
|
||||||
"attachment_name": "some file.jpg",
|
"attachment_name": "some file.jpg",
|
||||||
"attachment_type": "image/jpeg",
|
"attachment_type": "image/jpeg",
|
||||||
|
@ -203,6 +204,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||||
"title": "some title",
|
"title": "some title",
|
||||||
"message": "this is a message",
|
"message": "this is a message",
|
||||||
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||||
|
"content_type": "",
|
||||||
"encoding": "",
|
"encoding": "",
|
||||||
"attachment_name": "some file.jpg",
|
"attachment_name": "some file.jpg",
|
||||||
"attachment_type": "image/jpeg",
|
"attachment_type": "image/jpeg",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ func (s *Server) execManager() {
|
||||||
s.pruneTokens()
|
s.pruneTokens()
|
||||||
s.pruneAttachments()
|
s.pruneAttachments()
|
||||||
s.pruneMessages()
|
s.pruneMessages()
|
||||||
|
s.pruneAndNotifyWebPushSubscriptions()
|
||||||
|
|
||||||
// Message count per topic
|
// Message count per topic
|
||||||
var messagesCached int
|
var messagesCached int
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
|
@ -3,7 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
|
@ -58,6 +58,15 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
if s.config.WebRoot == "" || s.config.WebPushPublicKey == "" {
|
||||||
|
return errHTTPNotFound
|
||||||
|
}
|
||||||
|
return next(w, r, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) ensureUserManager(next handleFunc) handleFunc {
|
func (s *Server) ensureUserManager(next handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if s.userManager == nil {
|
if s.userManager == nil {
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stripe/stripe-go/v74"
|
"github.com/stripe/stripe-go/v74"
|
||||||
portalsession "github.com/stripe/stripe-go/v74/billingportal/session"
|
portalsession "github.com/stripe/stripe-go/v74/billingportal/session"
|
||||||
"github.com/stripe/stripe-go/v74/checkout/session"
|
"github.com/stripe/stripe-go/v74/checkout/session"
|
||||||
|
@ -11,9 +14,6 @@ import (
|
||||||
"github.com/stripe/stripe-go/v74/price"
|
"github.com/stripe/stripe-go/v74/price"
|
||||||
"github.com/stripe/stripe-go/v74/subscription"
|
"github.com/stripe/stripe-go/v74/subscription"
|
||||||
"github.com/stripe/stripe-go/v74/webhook"
|
"github.com/stripe/stripe-go/v74/webhook"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
|
@ -2,12 +2,12 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stripe/stripe-go/v74"
|
"github.com/stripe/stripe-go/v74"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -22,9 +22,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
@ -238,6 +239,12 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
rr = request(t, s, "GET", "/config.js", "", nil)
|
rr = request(t, s, "GET", "/config.js", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/sw.js", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/app.html", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
@ -250,6 +257,35 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
|
|
||||||
rr = request(t, s2, "GET", "/config.js", "", nil)
|
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/sw.js", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/app.html", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPushEnabled(t *testing.T) {
|
||||||
|
conf := newTestConfig(t)
|
||||||
|
conf.WebRoot = "" // Disable web app
|
||||||
|
s := newTestServer(t, conf)
|
||||||
|
|
||||||
|
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
conf2 := newTestConfig(t)
|
||||||
|
s2 := newTestServer(t, conf2)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
conf3 := newTestConfigWithWebPush(t)
|
||||||
|
s3 := newTestServer(t, conf3)
|
||||||
|
|
||||||
|
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_PublishLargeMessage(t *testing.T) {
|
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||||
|
@ -293,6 +329,27 @@ func TestServer_PublishPriority(t *testing.T) {
|
||||||
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
|
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
|
"Priority": "u=4",
|
||||||
|
"X-Priority": "5",
|
||||||
|
})
|
||||||
|
require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
|
||||||
|
|
||||||
|
response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{
|
||||||
|
"Priority": "u=9",
|
||||||
|
})
|
||||||
|
require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
|
||||||
|
|
||||||
|
response = request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
|
"p": "2",
|
||||||
|
"priority": "u=9, i",
|
||||||
|
})
|
||||||
|
require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
|
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
|
||||||
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
|
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
|
||||||
|
|
||||||
|
@ -455,6 +512,8 @@ func TestServer_PublishAtAndPrune(t *testing.T) {
|
||||||
messages := toMessages(t, response.Body.String())
|
messages := toMessages(t, response.Body.String())
|
||||||
require.Equal(t, 1, len(messages)) // Not affected by pruning
|
require.Equal(t, 1, len(messages)) // Not affected by pruning
|
||||||
require.Equal(t, "a message", messages[0].Message)
|
require.Equal(t, "a message", messages[0].Message)
|
||||||
|
|
||||||
|
time.Sleep(time.Second) // FIXME CI failing not sure why
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_PublishAndMultiPoll(t *testing.T) {
|
func TestServer_PublishAndMultiPoll(t *testing.T) {
|
||||||
|
@ -1482,6 +1541,39 @@ func TestServer_PublishActions_AndPoll(t *testing.T) {
|
||||||
require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
|
require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishMarkdown(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
|
||||||
|
"Content-Type": "text/markdown",
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "**make this bold**", m.Message)
|
||||||
|
require.Equal(t, "text/markdown", m.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishMarkdown_QueryParam(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "**make this bold**", m.Message)
|
||||||
|
require.Equal(t, "text/markdown", m.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
|
||||||
|
"Content-Type": "not-markdown",
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "", m.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishAsJSON(t *testing.T) {
|
func TestServer_PublishAsJSON(t *testing.T) {
|
||||||
s := newTestServer(t, newTestConfig(t))
|
s := newTestServer(t, newTestConfig(t))
|
||||||
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
|
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
|
||||||
|
@ -1499,12 +1591,25 @@ func TestServer_PublishAsJSON(t *testing.T) {
|
||||||
require.Equal(t, "google.pdf", m.Attachment.Name)
|
require.Equal(t, "google.pdf", m.Attachment.Name)
|
||||||
require.Equal(t, "http://ntfy.sh", m.Click)
|
require.Equal(t, "http://ntfy.sh", m.Click)
|
||||||
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
|
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
|
||||||
|
require.Equal(t, "", m.ContentType)
|
||||||
|
|
||||||
require.Equal(t, 4, m.Priority)
|
require.Equal(t, 4, m.Priority)
|
||||||
require.True(t, m.Time > time.Now().Unix()+29*60)
|
require.True(t, m.Time > time.Now().Unix()+29*60)
|
||||||
require.True(t, m.Time < time.Now().Unix()+31*60)
|
require.True(t, m.Time < time.Now().Unix()+31*60)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishAsJSON_Markdown(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}`
|
||||||
|
response := request(t, s, "PUT", "/", body, nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "mytopic", m.Topic)
|
||||||
|
require.Equal(t, "**This is bold**", m.Message)
|
||||||
|
require.Equal(t, "text/markdown", m.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) {
|
func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) {
|
||||||
// Publishing as JSON follows a different path. This ensures that rate
|
// Publishing as JSON follows a different path. This ensures that rate
|
||||||
// limiting works for this endpoint as well
|
// limiting works for this endpoint as well
|
||||||
|
@ -2591,19 +2696,33 @@ func newTestConfig(t *testing.T) *Config {
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestConfigWithAuthFile(t *testing.T) *Config {
|
func configureAuth(t *testing.T, conf *Config) *Config {
|
||||||
conf := newTestConfig(t)
|
|
||||||
conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
|
conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
|
||||||
conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
|
conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
|
||||||
conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot
|
conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTestConfigWithAuthFile(t *testing.T) *Config {
|
||||||
|
conf := newTestConfig(t)
|
||||||
|
conf = configureAuth(t, conf)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestConfigWithWebPush(t *testing.T) *Config {
|
||||||
|
conf := newTestConfig(t)
|
||||||
|
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
||||||
|
require.Nil(t, err)
|
||||||
|
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
|
||||||
|
conf.WebPushEmailAddress = "testing@example.com"
|
||||||
|
conf.WebPushPrivateKey = privateKey
|
||||||
|
conf.WebPushPublicKey = publicKey
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
func newTestServer(t *testing.T, config *Config) *Server {
|
func newTestServer(t *testing.T, config *Config) *Server {
|
||||||
server, err := New(config)
|
server, err := New(config)
|
||||||
if err != nil {
|
require.Nil(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/user"
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
171
server/server_webpush.go
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
webPushTopicSubscribeLimit = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
webPushAllowedEndpointsPatterns = []string{
|
||||||
|
"https://*.google.com/",
|
||||||
|
"https://*.googleapis.com/",
|
||||||
|
"https://*.mozilla.com/",
|
||||||
|
"https://*.mozaws.net/",
|
||||||
|
"https://*.windows.com/",
|
||||||
|
"https://*.microsoft.com/",
|
||||||
|
"https://*.apple.com/",
|
||||||
|
}
|
||||||
|
webPushAllowedEndpointsRegex *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i, pattern := range webPushAllowedEndpointsPatterns {
|
||||||
|
webPushAllowedEndpointsPatterns[i] = strings.ReplaceAll(strings.ReplaceAll(pattern, ".", "\\."), "*", ".+")
|
||||||
|
}
|
||||||
|
allPatterns := fmt.Sprintf("^(%s)", strings.Join(webPushAllowedEndpointsPatterns, "|"))
|
||||||
|
webPushAllowedEndpointsRegex = regexp.MustCompile(allPatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
|
if err != nil || req.Endpoint == "" || req.P256dh == "" || req.Auth == "" {
|
||||||
|
return errHTTPBadRequestWebPushSubscriptionInvalid
|
||||||
|
} else if !webPushAllowedEndpointsRegex.MatchString(req.Endpoint) {
|
||||||
|
return errHTTPBadRequestWebPushEndpointUnknown
|
||||||
|
} else if len(req.Topics) > webPushTopicSubscribeLimit {
|
||||||
|
return errHTTPBadRequestWebPushTopicCountTooHigh
|
||||||
|
}
|
||||||
|
topics, err := s.topicsFromIDs(req.Topics...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.userManager != nil {
|
||||||
|
u := v.User()
|
||||||
|
for _, t := range topics {
|
||||||
|
if err := s.userManager.Authorize(u, t.ID, user.PermissionRead); err != nil {
|
||||||
|
logvr(v, r).With(t).Err(err).Debug("Access to topic %s not authorized", t.ID)
|
||||||
|
return errHTTPForbidden.With(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.webPush.UpsertSubscription(req.Endpoint, req.Auth, req.P256dh, v.MaybeUserID(), v.IP(), req.Topics); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.writeJSON(w, newSuccessResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
|
req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false)
|
||||||
|
if err != nil || req.Endpoint == "" {
|
||||||
|
return errHTTPBadRequestWebPushSubscriptionInvalid
|
||||||
|
}
|
||||||
|
if err := s.webPush.RemoveSubscriptionsByEndpoint(req.Endpoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.writeJSON(w, newSuccessResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||||
|
subscriptions, err := s.webPush.SubscriptionsForTopic(m.Topic)
|
||||||
|
if err != nil {
|
||||||
|
logvm(v, m).Err(err).With(v, m).Warn("Unable to publish web push messages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Tag(tagWebPush).With(v, m).Debug("Publishing web push message to %d subscribers", len(subscriptions))
|
||||||
|
payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m))
|
||||||
|
if err != nil {
|
||||||
|
log.Tag(tagWebPush).Err(err).With(v, m).Warn("Unable to marshal expiring payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, subscription := range subscriptions {
|
||||||
|
if err := s.sendWebPushNotification(subscription, payload, v, m); err != nil {
|
||||||
|
log.Tag(tagWebPush).Err(err).With(v, m, subscription).Warn("Unable to publish web push message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) pruneAndNotifyWebPushSubscriptions() {
|
||||||
|
if s.config.WebPushPublicKey == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := s.pruneAndNotifyWebPushSubscriptionsInternal(); err != nil {
|
||||||
|
log.Tag(tagWebPush).Err(err).Warn("Unable to prune or notify web push subscriptions")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error {
|
||||||
|
// Expire old subscriptions
|
||||||
|
if err := s.webPush.RemoveExpiredSubscriptions(s.config.WebPushExpiryDuration); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Notify subscriptions that will expire soon
|
||||||
|
subscriptions, err := s.webPush.SubscriptionsExpiring(s.config.WebPushExpiryWarningDuration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(subscriptions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(newWebPushSubscriptionExpiringPayload())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
warningSent := make([]*webPushSubscription, 0)
|
||||||
|
for _, subscription := range subscriptions {
|
||||||
|
if err := s.sendWebPushNotification(subscription, payload); err != nil {
|
||||||
|
log.Tag(tagWebPush).Err(err).With(subscription).Warn("Unable to publish expiry imminent warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
warningSent = append(warningSent, subscription)
|
||||||
|
}
|
||||||
|
if err := s.webPush.MarkExpiryWarningSent(warningSent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Tag(tagWebPush).Debug("Expired old subscriptions and published %d expiry imminent warnings", len(subscriptions))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendWebPushNotification(sub *webPushSubscription, message []byte, contexters ...log.Contexter) error {
|
||||||
|
log.Tag(tagWebPush).With(sub).With(contexters...).Debug("Sending web push message")
|
||||||
|
payload := &webpush.Subscription{
|
||||||
|
Endpoint: sub.Endpoint,
|
||||||
|
Keys: webpush.Keys{
|
||||||
|
Auth: sub.Auth,
|
||||||
|
P256dh: sub.P256dh,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := webpush.SendNotification(message, payload, &webpush.Options{
|
||||||
|
Subscriber: s.config.WebPushEmailAddress,
|
||||||
|
VAPIDPublicKey: s.config.WebPushPublicKey,
|
||||||
|
VAPIDPrivateKey: s.config.WebPushPrivateKey,
|
||||||
|
Urgency: webpush.UrgencyHigh, // iOS requires this to ensure delivery
|
||||||
|
TTL: int(s.config.CacheDuration.Seconds()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Tag(tagWebPush).With(sub).With(contexters...).Err(err).Debug("Unable to publish web push message, removing endpoint")
|
||||||
|
if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (resp.StatusCode < 200 || resp.StatusCode > 299) && resp.StatusCode != 429 {
|
||||||
|
log.Tag(tagWebPush).With(sub).With(contexters...).Field("response_code", resp.StatusCode).Debug("Unable to publish web push message, unexpected response")
|
||||||
|
if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errHTTPInternalErrorWebPushUnableToPublish.With(sub).With(contexters...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
256
server/server_webpush_test.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_WebPush_Disabled(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 404, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicAdd(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||||
|
|
||||||
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Len(t, subs, 1)
|
||||||
|
require.Equal(t, subs[0].Endpoint, testWebPushEndpoint)
|
||||||
|
require.Equal(t, subs[0].P256dh, "p256dh-key")
|
||||||
|
require.Equal(t, subs[0].Auth, "auth-key")
|
||||||
|
require.Equal(t, subs[0].UserID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil)
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
topicList := make([]string, 51)
|
||||||
|
for i := range topicList {
|
||||||
|
topicList[i] = util.RandomString(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
addSubscription(t, s, testWebPushEndpoint, "test-topic")
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||||
|
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_Delete(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
addSubscription(t, s, testWebPushEndpoint, "test-topic")
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
|
||||||
|
response := request(t, s, "DELETE", "/v1/webpush", fmt.Sprintf(`{"endpoint":"%s"}`, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||||
|
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
|
||||||
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||||
|
config.AuthDefault = user.PermissionDenyAll
|
||||||
|
s := newTestServer(t, config)
|
||||||
|
|
||||||
|
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "ben"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||||
|
|
||||||
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, subs, 1)
|
||||||
|
require.True(t, strings.HasPrefix(subs[0].UserID, "u_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
|
||||||
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||||
|
config.AuthDefault = user.PermissionDenyAll
|
||||||
|
s := newTestServer(t, config)
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
|
||||||
|
require.Equal(t, 403, response.Code)
|
||||||
|
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
|
||||||
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||||
|
s := newTestServer(t, config)
|
||||||
|
|
||||||
|
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||||
|
|
||||||
|
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "ben"),
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||||
|
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
|
||||||
|
request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("ben", "ben"),
|
||||||
|
})
|
||||||
|
// should've been deleted with the account
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_Publish(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
var received atomic.Bool
|
||||||
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "/push-receive", r.URL.Path)
|
||||||
|
require.Equal(t, "high", r.Header.Get("Urgency"))
|
||||||
|
require.Equal(t, "", r.Header.Get("Topic"))
|
||||||
|
received.Store(true)
|
||||||
|
}))
|
||||||
|
defer pushService.Close()
|
||||||
|
|
||||||
|
addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
|
||||||
|
request(t, s, "POST", "/test-topic", "web push test", nil)
|
||||||
|
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return received.Load()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
var received atomic.Bool
|
||||||
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
w.WriteHeader(http.StatusGone)
|
||||||
|
received.Store(true)
|
||||||
|
}))
|
||||||
|
defer pushService.Close()
|
||||||
|
|
||||||
|
addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc")
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
requireSubscriptionCount(t, s, "test-topic-abc", 1)
|
||||||
|
|
||||||
|
request(t, s, "POST", "/test-topic", "web push test", nil)
|
||||||
|
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return received.Load()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint
|
||||||
|
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||||
|
requireSubscriptionCount(t, s, "test-topic-abc", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_WebPush_Expiry(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||||
|
|
||||||
|
var received atomic.Bool
|
||||||
|
|
||||||
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(``))
|
||||||
|
received.Store(true)
|
||||||
|
}))
|
||||||
|
defer pushService.Close()
|
||||||
|
|
||||||
|
addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
|
||||||
|
_, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
s.pruneAndNotifyWebPushSubscriptions()
|
||||||
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||||
|
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return received.Load()
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
s.pruneAndNotifyWebPushSubscriptions()
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
||||||
|
require.Nil(t, err)
|
||||||
|
return len(subs) == 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadForTopics(t *testing.T, topics []string, endpoint string) string {
|
||||||
|
topicsJSON, err := json.Marshal(topics)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
return fmt.Sprintf(`{
|
||||||
|
"topics": %s,
|
||||||
|
"endpoint": "%s",
|
||||||
|
"p256dh": "p256dh-key",
|
||||||
|
"auth": "auth-key"
|
||||||
|
}`, topicsJSON, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSubscription(t *testing.T, s *Server, endpoint string, topics ...string) {
|
||||||
|
require.Nil(t, s.webPush.UpsertSubscription(endpoint, "kSC3T8aN1JCQxxPdrFLrZg", "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE", "u_123", netip.MustParseAddr("1.2.3.4"), topics)) // Test auth and p256dh
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) {
|
||||||
|
subs, err := s.webPush.SubscriptionsForTopic(topic)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, subs, expectedLength)
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mailer interface {
|
type mailer interface {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"heckel.io/ntfy/log"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
117
server/types.go
|
@ -1,13 +1,14 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/user"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"heckel.io/ntfy/util"
|
"git.zio.sh/astra/ntfy/v2/log"
|
||||||
|
"git.zio.sh/astra/ntfy/v2/user"
|
||||||
|
|
||||||
|
"git.zio.sh/astra/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of possible events
|
// List of possible events
|
||||||
|
@ -24,23 +25,24 @@ const (
|
||||||
|
|
||||||
// message represents a message published to a topic
|
// message represents a message published to a topic
|
||||||
type message struct {
|
type message struct {
|
||||||
ID string `json:"id"` // Random message ID
|
ID string `json:"id"` // Random message ID
|
||||||
Time int64 `json:"time"` // Unix time in seconds
|
Time int64 `json:"time"` // Unix time in seconds
|
||||||
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
||||||
Event string `json:"event"` // One of the above
|
Event string `json:"event"` // One of the above
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Click string `json:"click,omitempty"`
|
Click string `json:"click,omitempty"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Actions []*action `json:"actions,omitempty"`
|
Actions []*action `json:"actions,omitempty"`
|
||||||
Attachment *attachment `json:"attachment,omitempty"`
|
Attachment *attachment `json:"attachment,omitempty"`
|
||||||
PollID string `json:"poll_id,omitempty"`
|
PollID string `json:"poll_id,omitempty"`
|
||||||
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
|
||||||
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
||||||
User string `json:"-"` // Username of the uploader, used to associated attachments
|
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
||||||
|
User string `json:"-"` // UserID of the uploader, used to associated attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *message) Context() log.Context {
|
func (m *message) Context() log.Context {
|
||||||
|
@ -99,6 +101,7 @@ type publishMessage struct {
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Actions []action `json:"actions"`
|
Actions []action `json:"actions"`
|
||||||
Attach string `json:"attach"`
|
Attach string `json:"attach"`
|
||||||
|
Markdown bool `json:"markdown"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Call string `json:"call"`
|
Call string `json:"call"`
|
||||||
|
@ -397,7 +400,9 @@ type apiConfigResponse struct {
|
||||||
EnableCalls bool `json:"enable_calls"`
|
EnableCalls bool `json:"enable_calls"`
|
||||||
EnableEmails bool `json:"enable_emails"`
|
EnableEmails bool `json:"enable_emails"`
|
||||||
EnableReservations bool `json:"enable_reservations"`
|
EnableReservations bool `json:"enable_reservations"`
|
||||||
|
EnableWebPush bool `json:"enable_web_push"`
|
||||||
BillingContact string `json:"billing_contact"`
|
BillingContact string `json:"billing_contact"`
|
||||||
|
WebPushPublicKey string `json:"web_push_public_key"`
|
||||||
DisallowedTopics []string `json:"disallowed_topics"`
|
DisallowedTopics []string `json:"disallowed_topics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,3 +467,75 @@ type apiStripeSubscriptionDeletedEvent struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Customer string `json:"customer"`
|
Customer string `json:"customer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiWebPushUpdateSubscriptionRequest struct {
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Auth string `json:"auth"`
|
||||||
|
P256dh string `json:"p256dh"`
|
||||||
|
Topics []string `json:"topics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of possible Web Push events (see sw.js)
|
||||||
|
const (
|
||||||
|
webPushMessageEvent = "message"
|
||||||
|
webPushExpiringEvent = "subscription_expiring"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webPushPayload struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
SubscriptionID string `json:"subscription_id"`
|
||||||
|
Message *message `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebPushPayload(subscriptionID string, message *message) *webPushPayload {
|
||||||
|
return &webPushPayload{
|
||||||
|
Event: webPushMessageEvent,
|
||||||
|
SubscriptionID: subscriptionID,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type webPushControlMessagePayload struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload {
|
||||||
|
return &webPushControlMessagePayload{
|
||||||
|
Event: webPushExpiringEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type webPushSubscription struct {
|
||||||
|
ID string
|
||||||
|
Endpoint string
|
||||||
|
Auth string
|
||||||
|
P256dh string
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webPushSubscription) Context() log.Context {
|
||||||
|
return map[string]any{
|
||||||
|
"web_push_subscription_id": w.ID,
|
||||||
|
"web_push_subscription_user_id": w.UserID,
|
||||||
|
"web_push_subscription_endpoint": w.Endpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Manifest
|
||||||
|
type webManifestResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
StartURL string `json:"start_url"`
|
||||||
|
Display string `json:"display"`
|
||||||
|
BackgroundColor string `json:"background_color"`
|
||||||
|
ThemeColor string `json:"theme_color"`
|
||||||
|
Icons []*webManifestIcon `json:"icons"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webManifestIcon struct {
|
||||||
|
SRC string `json:"src"`
|
||||||
|
Sizes string `json:"sizes"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|